Files
palladium/docs/Assessment_Pattern_V1-00.md
Robert Helewka a967f73d09 feat: add master notebook library scaffolding and review tooling
Add CLAUDE.md defining the Palladium master notebook conventions and
Red Panda Approval criteria, plus a review-notebook slash command for
LLM-driven notebook review.

Expand .gitignore to block client/engagement documents and generated
exports, keeping masters client-clean while allowing text/image sources.

Normalize slider widget numeric values from floats to integers in
notebook JSON.
2026-07-31 16:16:07 +00:00

12 KiB

Assessment Pattern v1.0.0

How Palladium ships Assessments: reusable workshop instruments (diagnostics, discovery workshops, maturity assessments) built as Mercury notebook deliverables. An Assessment is a master — client-clean, undated, maintained in this repo — that becomes a client deliverable only as an engagement copy made outside the repo. Reference implementation: assessments/CX_Discovery_Workshop/.

🐾 Red Panda Approval™

This pattern follows Red Panda Approval standards (see CLAUDE.md for the rubric).

Audience note: written to be loaded whole by an LLM agent building or modifying an assessment. This document holds what assessments ADD to the Mercury Notebook Deliverable Pattern — the mechanics every master obeys (reactivity contract, gate, stage/backstage, packaging, appendix) live there and are not restated here. CLAUDE.md is the always-on contract and takes precedence where documents disagree.


What an Assessment is

Study Assessment
Reproduces a dated publication / engagement — (it IS the instrument)
Naming YYYYMM_… (dated) Instrument_Name (undated, living)
"Numbers" dollars (NPV/ROI/payback) qualitative state (status, scores, coverage)
Client data overlay on a published anchor the engagement-data cell, filled per copy
Lifecycle frozen once delivered master evolves; copies freeze

The three-layer contract (from CLAUDE.md), which assessments realize most completely:

  1. Mercury — the polished client-facing stage a workshop runs on.
  2. Jupyter cells — the consultant's surface: workshop content and client data are edited in tagged notebook cells, never in .py.
  3. Python modules — the per-assessment engine: schema + session logic, reusable, typed (mypy --strict), and pinned by tests.

Master → engagement copy lifecycle

The master in assessments/ MUST stay client-clean: placeholder engagement data, generic content, nothing a client said. Running one for a client means copying it out (checklist below); the copy acquires client data, becomes confidential, and never merges back. Content improvements discovered on an engagement are hand-carried back to the master as clean edits.


Anatomy

assessments/<Instrument_Name>/
├── notebooks/<instrument>.ipynb   # THE deliverable — content + data + presentation
├── <instrumentlib>/               # the engine — CODE ONLY, no content
│   ├── session.py                 #   schema, vocabulary, session logic, export payload
│   └── staging.py                 #   stage/backstage (+ backstage_md) — self-contained copy
├── tests/                         # content/engagement pins (read from the notebook) + engine pins
├── scripts/export_report.py       # execute once → exports/*.html + LLM-ready *.md
├── docs/                          # source material (.md/.png only — see .gitignore)
├── exports/                       # generated — never committed
├── config.toml                    # Mercury app shell (brand theme, welcome)
└── pyproject.toml                 # whole toolchain as core deps (Mercury Pattern §7)

Masters are runtime-self-contained: the engine package (including its own staging.py copy) lives in the master, so an engagement copy runs standalone with no dependency back on Palladium. Repo-level machinery (the structural test suite, the review prompt) is maintenance tooling — an engagement copy never needs it.


The notebook-first content model

Everything a consultant edits lives in tagged cells of the deliverable notebook. The tag taxonomy (enforced by tests/test_notebooks.py from the repo root):

Tag Count What it holds Position rule
topic-bank exactly 1 the instrument's content: topics, prompts, scoring bands… above the widgets
engagement-data exactly 1 client facts for THIS session (placeholders in the master) above the widgets
presentation any setup, widget construction, stage rendering
gate exactly 1 the verification gate (Mercury Pattern §4) after the analytics
data-appendix exactly 1 the machine-readable session record last code cell

Rules for content cells (topic-bank, engagement-data):

  • Self-contained — no magics, no shell escapes, imports only from the assessment's own engine. Why: tests and scripts exec the cell by its tag without a kernel; a %magic breaks that contract.
  • Stable keys — slugs (key="channels") are identities that widgets, captured notes, and the export JSON key off. NEVER renumber or rename casually; a title edit renames its sidebar widget label, which resets that widget's state mid-session (Mercury's widget cache is label-keyed).
  • Verbatim anchor discipline — when the content structures a source document (a survey, a question bank), wording tracks the source; the cell's own comment block carries the editing rules so they travel with the content.
  • Above the widgets — both content cells sit above the widget cell, so Mercury never re-runs them on a sidebar change (it re-runs only cells BELOW the changed widget).

The engagement-data cell contract

# ── Engagement data — EDIT PER ENGAGEMENT (tagged: engagement-data) ──
ENGAGEMENT: dict[str, object] = {
    "client": "",           # e.g. "Acme Corp" — shown on the stage board header
    "workshop_date": "",    # ISO date, e.g. "2026-08-12"
    "facilitator": "",
    "attendees": (),        # tuple of "Name — role" strings
}
  • The master ships placeholders (empty strings / empty tuple); the engagement copy ships real values. Everything that checks this cell — the gate, pytest, the repo suite — pins shape only (key set, types), NEVER emptiness or a value, so master and filled copy both stay green.
  • Where the values flow: the stage board header (rendered only when client is non-empty, so the master's stage is unchanged), the export preamble, and the appendix JSON meta.
  • Extend the dict per instrument (e.g. current_spend, agent_count for a sizing assessment) — client data belongs HERE, in the cell, not in the engine and not hardcoded in presentation cells.

Engine parameterization

The engine package holds schema + logic only, and every session-level function takes the content as its first argument — the engine never imports content:

SESSION = build_session(TOPICS, STATUS_BY_TOPIC, NOTES_BY_TOPIC, DONE_SUBTOPICS)

Why: content stays in the notebook (editable), logic stays testable — tests feed the real bank from the tagged cell, or synthetic banks for edge cases. Widgets are built by a runtime loop over the content (with per-widget distinct labels — Mercury's cache is label-keyed), so a content edit propagates to sidebar, board, script, gate, and export with zero further wiring.


Testing an Assessment

All layers of the Mercury Pattern's testing section apply; assessments add the exec-by-tag recipe for content:

# tests/conftest.py — the content fixtures read the NOTEBOOK, no kernel
def tagged_cell_ns(tag: str) -> dict[str, Any]:
    nb = nbformat.read(NOTEBOOK, as_version=4)
    cells = [c for c in nb.cells if tag in c.metadata.get("tags", [])]
    assert len(cells) == 1
    ns: dict[str, Any] = {}
    exec(compile(cells[0].source, f"{NOTEBOOK.name} [{tag}]", "exec"), ns)
    return ns
  • Content pins — counts, key order, uniqueness, well-formedness of the bank (tests/test_topics.py in the reference). Re-pin deliberately on content changes.
  • Engagement pins — shape of ENGAGEMENT, its flow into the export meta, and the cell's above-the-widgets position (tests/test_engagement.py).
  • Engine pins — session logic against hand-checked values (tests/test_session.py).
  • Stagingbackstage/backstage_md render only off stage.
  • Gate + repo suite — the in-notebook gate re-counts the bank on every execution; make check-notebooks (repo root) enforces the tag taxonomy from outside.

Export handoff (the LLM-input artifact)

scripts/export_report.py — execute ONCE, convert twice, post-process:

  1. nbconvert --execute to a temp copy (never combine --execute with tag-stripping in one call — a cell could be removed before it runs).
  2. HTML from the executed copy — full presentation, human review.
  3. Markdown from the executed copy with TagRemovePreprocessor.remove_cell_tags={"presentation"} — the LLM artifact keeps content, script, gate, and appendix; drops setup/widget/board source and widget-repr noise.
  4. Prepend a generated preamble: what the document is, the engagement line (read from the engagement-data cell via nbformat), how to read it, and the instruction that the final fenced JSON block is the source of truth.

The appendix cell emits its table + JSON through backstage_md() (one text/markdown display), so the .md export carries a clean fenced ```json block as the last thing in the file — not an indented text blob.

Stage polish

config.toml carries the brand (docs/brand.md) — the reference's file is the copy-me. Beyond the palette: name the deliverable notebook in [welcome] message, keep the footer attributed, set the brand state colors (success_color, warning_color, danger_color), and keep web-safe font stacks (no font_url — a client screen must not depend on a network font fetch). Engine palettes may deviate deliberately (the reference's on-board complete-green #00a34c is darker than brand Success #00cb5d for contrast on white cards) — note such choices where they live.


Copy-out checklist (canonical)

Running an assessment for a client — the master never touches client data:

  1. Copy the master directory out of Palladium to your engagement location (it is self-contained — no repo machinery comes along or is needed).
  2. Rename it YYYYMM_Client_Instrument (e.g. 202608_Acme_CX_Discovery).
  3. Provision: python -m venv .venv && .venv/bin/pip install -e ".[dev]" (editable installs pin absolute paths — a copied venv is broken; always recreate).
  4. Fill the engagement-data cell — and only that cell — with the client facts.
  5. Verify: pytest and the headless gate (jupyter nbconvert --to notebook --execute --inplace notebooks/*.ipynb) — both stay green by design (shape-only pins).
  6. The copy is now confidential: it never merges back, never returns to this repo. Improvements you discover on the engagement are hand-carried to the master as clean, client-free edits.

Assessment anti-patterns

  • Facilitator prose in markdown cells — markdown ALWAYS renders on the Mercury stage; run-books, "backstage" explanations, and section headings for hidden sections all leak to the client. The stage gets a client-neutral title and the board; facilitator orientation goes through backstage_md() in a presentation-tagged code cell (JupyterLab and the HTML export show it; the stage and the LLM .md export don't).
  • Client data in a master — a client's name in assessments/ means the copy-out step was skipped; scrub and move it out.
  • Content in .py — topic banks, prompts, survey text are content; content lives in tagged cells. (This includes notebook generators that hold content as Python strings — the same mistake one indirection deeper.)
  • Untagged content cells — the repo suite, the tests, and the export pipeline all find content by tag; an untagged content cell is invisible to all three.
  • Emptiness pins — asserting ENGAGEMENT["client"] == "" breaks every filled engagement copy; pin shape, never values.
  • Widget labels derived from unstable text — labels come from content titles; editing a title mid-session resets that widget. Edit titles between sessions.