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

237 lines
12 KiB
Markdown

# 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/<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
```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.