# 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/`](../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](Mercury_Notebook_Pattern_V1-00.md) β€” 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// β”œβ”€β”€ notebooks/.ipynb # THE deliverable β€” content + data + presentation β”œβ”€β”€ / # 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 ```python # ── 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: ```python 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: ```python # 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`). - **Staging** β€” `backstage`/`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`](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.