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.
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:
- Mercury — the polished client-facing stage a workshop runs on.
- Jupyter cells — the consultant's surface: workshop content and client
data are edited in tagged notebook cells, never in
.py. - 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
%magicbreaks 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
clientis non-empty, so the master's stage is unchanged), the export preamble, and the appendix JSONmeta. - Extend the dict per instrument (e.g.
current_spend,agent_countfor 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.pyin 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). - Staging —
backstage/backstage_mdrender 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:
nbconvert --executeto a temp copy (never combine--executewith tag-stripping in one call — a cell could be removed before it runs).- HTML from the executed copy — full presentation, human review.
- 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. - Prepend a generated preamble: what the document is, the engagement line (read
from the
engagement-datacell 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:
- Copy the master directory out of Palladium to your engagement location (it is self-contained — no repo machinery comes along or is needed).
- Rename it
YYYYMM_Client_Instrument(e.g.202608_Acme_CX_Discovery). - Provision:
python -m venv .venv && .venv/bin/pip install -e ".[dev]"(editable installs pin absolute paths — a copied venv is broken; always recreate). - Fill the
engagement-datacell — and only that cell — with the client facts. - Verify:
pytestand the headless gate (jupyter nbconvert --to notebook --execute --inplace notebooks/*.ipynb) — both stay green by design (shape-only pins). - 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 apresentation-tagged code cell (JupyterLab and the HTML export show it; the stage and the LLM.mdexport 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.