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.
This commit is contained in:
13
.claude/commands/review-notebook.md
Normal file
13
.claude/commands/review-notebook.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
description: LLM review of a master notebook (content, cross-cell, staging, tags)
|
||||||
|
argument-hint: <path/to/notebook.ipynb>
|
||||||
|
---
|
||||||
|
|
||||||
|
Read `docs/Notebook_Review_Prompt_V1-00.md` (repo root: the Palladium repository
|
||||||
|
this command lives in) and apply it, in full, to the notebook at: $ARGUMENTS
|
||||||
|
|
||||||
|
Follow the prompt exactly: read the pattern docs it names, read the notebook's raw
|
||||||
|
`.ipynb` JSON (cell sources, tags, and outputs — not a rendered view), and open the
|
||||||
|
master's engine package for cross-checks where the prompt directs. Produce ONLY the
|
||||||
|
findings report in the prompt's output format — no preamble, no summary of what you
|
||||||
|
did.
|
||||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -7,8 +7,26 @@
|
|||||||
|
|
||||||
# Palladium-specific
|
# Palladium-specific
|
||||||
.env
|
.env
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Generated exports — never committed (masters and assessments)
|
||||||
studies/*/exports/*
|
studies/*/exports/*
|
||||||
!studies/*/exports/.gitkeep
|
!studies/*/exports/.gitkeep
|
||||||
|
assessments/*/exports/*
|
||||||
|
!assessments/*/exports/.gitkeep
|
||||||
|
|
||||||
|
# Client / engagement documents must NEVER be committed — masters stay
|
||||||
|
# client-clean (see CLAUDE.md, Confidentiality). Text/image source material
|
||||||
|
# is allowed; binary documents are blocked by default. ONE exception: a
|
||||||
|
# Study's published, public source PDF — add with `git add -f` and say so
|
||||||
|
# in the commit message.
|
||||||
|
studies/*/docs/*
|
||||||
|
!studies/*/docs/*.md
|
||||||
|
!studies/*/docs/*.png
|
||||||
|
assessments/*/docs/*
|
||||||
|
!assessments/*/docs/*.md
|
||||||
|
!assessments/*/docs/*.png
|
||||||
|
*SOW*
|
||||||
|
|
||||||
# IPython
|
# IPython
|
||||||
profile_default/
|
profile_default/
|
||||||
|
|||||||
261
CLAUDE.md
Normal file
261
CLAUDE.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# CLAUDE.md — Palladium (library of master notebooks)
|
||||||
|
|
||||||
|
🐾 Palladium is a **library of master notebooks** for consulting delivery: Mercury-served
|
||||||
|
Jupyter deliverables a client watches on a live call. "Library" means the copies in this
|
||||||
|
repo are **masters** — client-clean, reusable, maintained — it does **not** mean drafts.
|
||||||
|
A master becomes a client deliverable only as an **engagement copy made outside this
|
||||||
|
repo**, where it acquires client data and becomes confidential. The notebook a client
|
||||||
|
can open and trust is the whole product.
|
||||||
|
|
||||||
|
## 🐾 Red Panda Approval™ — for a consulting master library
|
||||||
|
|
||||||
|
Don't satisfy a checklist — satisfy the red pandas. Ask of each change: *does this
|
||||||
|
earn approval?*
|
||||||
|
|
||||||
|
1. **Rebuildable From Nothing** — inside any master: `python -m venv .venv` →
|
||||||
|
`pip install -e ".[dev]"` → `pytest` → headless gate → `mercury --working-dir .`
|
||||||
|
brings up the deliverable with no manual step. And a master **copied out of the
|
||||||
|
repo runs standalone** — self-contained engine, own `staging.py`, no import back
|
||||||
|
into Palladium. The ability to stand a workshop up on short notice is the product.
|
||||||
|
2. **Elegant Simplicity** — these are workshop tools, not enterprise platforms; the
|
||||||
|
obvious solution, done well. A content cell should read like the survey it came
|
||||||
|
from, not like a framework.
|
||||||
|
3. **Observable & Debuggable** — a failing gate names the number that drifted;
|
||||||
|
diagnostics flow through `backstage()`, never onto the client-facing stage; the
|
||||||
|
exports show the whole session state so a failure is diagnosable after the call.
|
||||||
|
4. **Consistent Patterns** — the three-layer contract, widget-pairs, tagged cells,
|
||||||
|
per-master engine packages. Match the pattern docs, not personal taste.
|
||||||
|
5. **Actually Works** — "pytest was green" is not "the board renders on the stage
|
||||||
|
with the engine's numbers." The gate under headless `nbconvert --execute` plus an
|
||||||
|
actual Mercury render are the proof.
|
||||||
|
|
||||||
|
Criteria 1 and 5 are **externally verifiable** — the rebuild works or it doesn't; the
|
||||||
|
stage renders or it doesn't. Verify them, don't assert them. Criteria 2–4 are
|
||||||
|
judgement calls: when in doubt, match what the repo already does rather than grading
|
||||||
|
your own elegance.
|
||||||
|
|
||||||
|
> If a paw print isn't leading the response, the rest of this file probably isn't
|
||||||
|
> being honoured either. Lead with one. 🐾
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The three-layer contract
|
||||||
|
|
||||||
|
Every master separates three concerns; the split IS the architecture:
|
||||||
|
|
||||||
|
| Layer | Audience | Holds | Never holds |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Mercury** (the stage) | the client | the polished board/case, sidebar controls | diagnostics, question scripts, internals |
|
||||||
|
| **Notebook cells** (backstage) | the consultant | **content** (topic banks, prompts) and **client data** (engagement facts, spend, headcount) in **tagged cells**; presentation code | math |
|
||||||
|
| **Python modules** (the engine) | tests + the notebook | reusable logic, schema, calculations — typed, `mypy --strict`, pinned | **content — NEVER** |
|
||||||
|
|
||||||
|
The rule that has teeth: **content never lives in `.py`** — not as constants, not as a
|
||||||
|
notebook generator's string blocks. The notebook is the document the consultant edits;
|
||||||
|
hiding its content in importable modules defeats the point of a notebook. Mechanics:
|
||||||
|
[docs/Mercury_Notebook_Pattern_V1-00.md](docs/Mercury_Notebook_Pattern_V1-00.md);
|
||||||
|
per-type contracts: [docs/Assessment_Pattern_V1-00.md](docs/Assessment_Pattern_V1-00.md)
|
||||||
|
and [docs/Study_Pattern_V1-00.md](docs/Study_Pattern_V1-00.md).
|
||||||
|
|
||||||
|
## Taxonomy
|
||||||
|
|
||||||
|
- **Study** (`studies/YYYYMM_…`, dated) — reproduction of a dated base document
|
||||||
|
(Forrester TEI or similar); verbatim anchor + client overlay; the date is the
|
||||||
|
publication's, so it stays.
|
||||||
|
- **Assessment** (`assessments/Instrument_Name`, undated) — reusable workshop
|
||||||
|
instrument; a living master. Reference implementation:
|
||||||
|
`assessments/CX_Discovery_Workshop/`.
|
||||||
|
- **Engagement copy** (`YYYYMM_Client_Instrument`, stamped at copy-time) — a master
|
||||||
|
copied OUT of this repo for a client; confidential; never merges back.
|
||||||
|
- **`template/MercuryNotebook/`** — copy-me scaffold for new studies (py-engine model
|
||||||
|
only — see Known liabilities).
|
||||||
|
- **`core/`** — the shared Athena toolkit; masters do NOT import it.
|
||||||
|
|
||||||
|
## Before working in a master: check where you are
|
||||||
|
|
||||||
|
**Look at the path.** Which master's root are you under? Each master has its **own
|
||||||
|
venv** — `.venv` inside the master directory, provisioned by `pip install -e ".[dev]"`
|
||||||
|
there; the repo-root `.venv` (from `make setup`) serves only `core/` and the notebook
|
||||||
|
structural suite. Running a master's pytest from the wrong venv is the classic
|
||||||
|
"missing module" ghost.
|
||||||
|
|
||||||
|
- `MERCURY_CONFIG_DIR` present in the environment = **the stage is live** (a client
|
||||||
|
may be looking). Its presence is the stage signal `staging.py` keys off — never set
|
||||||
|
it manually except to simulate the stage in a test.
|
||||||
|
- Only masters live here. **If a notebook in this repo contains a real client's name,
|
||||||
|
something is wrong** — stop and flag it (see Confidentiality).
|
||||||
|
|
||||||
|
## Risk tier: CONSULTING MASTERS — what you may run
|
||||||
|
|
||||||
|
**Free** — run without asking:
|
||||||
|
`pytest`, `mypy`, `ruff`, `make test`, `make check-notebooks`,
|
||||||
|
`jupyter nbconvert --execute` on a master, `mercury --working-dir .` locally,
|
||||||
|
`python scripts/export_report.py`, and any read-only git.
|
||||||
|
|
||||||
|
**Show first** — produce the diff/output, present it, wait for a human "go":
|
||||||
|
content edits to `topic-bank`/`engagement-data` cells or anchor-adjacent wording;
|
||||||
|
re-pinning a gate or test after a content/engine change (show the pin diff and the
|
||||||
|
KPI moves honestly); moving or renaming a master; `.gitignore` changes; `git commit`.
|
||||||
|
|
||||||
|
**Forbidden without explicit go-ahead:**
|
||||||
|
`git push`; editing `*_VERBATIM` anchors; committing any client document (SOW, quote,
|
||||||
|
NDA'd vendor deck); putting a client's name or data into a master; history rewrites;
|
||||||
|
deleting a study or assessment.
|
||||||
|
|
||||||
|
> The one clause that always applies: *"the user asked me to update the workshop" is
|
||||||
|
> not explicit go-ahead for a Show-first change.* Explicit go-ahead is the user seeing
|
||||||
|
> the specific diff and saying yes to **that**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conventions (always-on)
|
||||||
|
|
||||||
|
### Done means the stage shows the engine's numbers, not that the file saved
|
||||||
|
|
||||||
|
The cell is a wish; the executed notebook is the fact. An edit that is syntactically
|
||||||
|
perfect has changed **nothing** until the notebook re-runs — and the committed outputs
|
||||||
|
now lie about the deliverable.
|
||||||
|
|
||||||
|
1. **Pin it** — `pytest` + `mypy` in the master's venv.
|
||||||
|
2. **Execute it** — `jupyter nbconvert --to notebook --execute --inplace
|
||||||
|
notebooks/*.ipynb`; the gate passing headless is the study's smoke test.
|
||||||
|
3. **Export it** — `python scripts/export_report.py`; the `.md` must carry the
|
||||||
|
appendix and its final JSON block.
|
||||||
|
4. **Read it back from the real system** — serve with Mercury (or open the exported
|
||||||
|
HTML) and look at the board; then check what depends on the change: test pins,
|
||||||
|
gate pins, README counts, pattern docs, the export JSON. A master is a *chain* —
|
||||||
|
a renamed content key breaks widgets, notes, and the export two hops downstream.
|
||||||
|
|
||||||
|
### Confidentiality — masters stay client-clean
|
||||||
|
|
||||||
|
There is no Palladium without this rule; it is what makes the library shareable.
|
||||||
|
|
||||||
|
- Masters carry **placeholder** engagement data, published or synthetic numbers, and
|
||||||
|
no client statements. Client personalization happens in an **engagement copy** made
|
||||||
|
outside the repo (copy → rename `YYYYMM_Client_…` → fresh venv → fill
|
||||||
|
`engagement-data` → verify; canonical checklist in the Assessment Pattern) —
|
||||||
|
**before** any client data is entered, and the copy never merges back.
|
||||||
|
- **Never commit a client document** — SOWs, quotes, pricing decks, NDA'd vendor
|
||||||
|
material. `.gitignore` blocks binary documents under every master's `docs/` and
|
||||||
|
anything matching `*SOW*`; that is a guardrail, not permission — the rule is the
|
||||||
|
rule even where the pattern has a hole. The ONE exception: a Study's **published,
|
||||||
|
public** source PDF, added deliberately with `git add -f` and named in the commit
|
||||||
|
message.
|
||||||
|
- Improvements discovered on an engagement come back to the master as **clean edits**
|
||||||
|
(content/logic only, client facts stripped).
|
||||||
|
|
||||||
|
### Notebooks
|
||||||
|
|
||||||
|
- Widget-pair rule, gate cell, stage/backstage, data appendix, packaging: the
|
||||||
|
[Mercury pattern](docs/Mercury_Notebook_Pattern_V1-00.md) is the contract — read it
|
||||||
|
before editing any notebook.
|
||||||
|
- Tagged cells are the consultant's surface: `topic-bank` (content),
|
||||||
|
`engagement-data` (client facts; placeholders in masters), `presentation`, `gate`,
|
||||||
|
`data-appendix`. Tags are load-bearing — tests, the structural suite, and the
|
||||||
|
export pipeline all find cells by tag.
|
||||||
|
- Content-cell keys are **stable identities** (widgets, notes, exports key off them);
|
||||||
|
a title edit renames its sidebar widget label, which resets that widget's state
|
||||||
|
mid-session.
|
||||||
|
- `assessments/CX_AI_Diagnostic/notebooks/diagnostic.ipynb` is **generated** by its
|
||||||
|
`scripts/build_notebook.py` — never hand-edit it (and don't "fix" it to
|
||||||
|
notebook-first casually; that's a recorded redesign).
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
- One venv per master; `pip install -e ".[dev]"` provisions everything (whole
|
||||||
|
toolchain as core deps — never a `requirements.txt` in a master).
|
||||||
|
- Engines are `mypy --strict` clean; new logic lands as engine code + pins **before**
|
||||||
|
the notebook section that renders it.
|
||||||
|
- `staging.py` is copied per master (self-containment beats DRY here — an engagement
|
||||||
|
copy must run alone). The mypy-strict variant in
|
||||||
|
`assessments/CX_Discovery_Workshop/discoverylib/staging.py` (with `backstage_md`)
|
||||||
|
is the canonical form for new masters.
|
||||||
|
|
||||||
|
## Always-on anti-patterns
|
||||||
|
|
||||||
|
- **Content:** never move workshop content into `.py` — including generators that
|
||||||
|
hold cell sources as strings.
|
||||||
|
- **Client data:** never in a master; never in a commit; a client name in
|
||||||
|
`assessments/` or `template/` means the copy-out step was skipped.
|
||||||
|
- **Anchors:** never edit `*_VERBATIM` — overlay corrections, don't rewrite the
|
||||||
|
record.
|
||||||
|
- **Widgets:** never read `.value` in the defining cell; widget cells emit no output.
|
||||||
|
- **Surfaces:** never build a parallel UI (Streamlit twin, second app) — the notebook
|
||||||
|
is the surface.
|
||||||
|
- **Exports:** never commit generated exports; `exports/` is gitignored on purpose.
|
||||||
|
- **Emptiness pins:** never assert that engagement placeholders are empty — the
|
||||||
|
filled engagement copy must stay green.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- Repo root: `/home/robert/notebook/git/palladium` · remote:
|
||||||
|
`ssh://git@git.helu.ca:22022/r/palladium.git` (Robert's Gitea) · branch `main`.
|
||||||
|
- Root venv: `make setup` (core/Athena layer + the notebook structural suite;
|
||||||
|
`make test`, `make check-notebooks`). One venv **per master** besides it.
|
||||||
|
- Python ≥3.10 per master (`CX_AI_Diagnostic` pins ≥3.11; root core is ≥3.11);
|
||||||
|
Mercury 3.2.x; per-master mypy is strict; repo-wide lint is ruff (root
|
||||||
|
`pyproject.toml`).
|
||||||
|
- Athena onboarding: `00_setup.ipynb` (writes `.env`; sandbox
|
||||||
|
`https://athena.ouranos.helu.ca`).
|
||||||
|
|
||||||
|
## Known liabilities (flag, don't silently fix)
|
||||||
|
|
||||||
|
If you find a known non-compliant choice, raise it rather than quietly fixing it or
|
||||||
|
quietly leaving it. Live ones worth knowing:
|
||||||
|
|
||||||
|
- **`studies/202607_CTM_GenesysCX/` is a real-client engagement study inside the
|
||||||
|
library** — committed confidential vendor/client PDFs in `docs/` (also in git
|
||||||
|
history), plus a signed SOW PDF sitting on disk untracked-and-ignored (the `*SOW*`
|
||||||
|
ignore hides it from `git status` — it is still there). Grandfathered: do not add
|
||||||
|
more material, do not use it as precedent; extraction to an engagement archive is
|
||||||
|
a recorded follow-up. Two of its notebooks are dirty in the working tree —
|
||||||
|
pre-existing, leave them.
|
||||||
|
- **`assessments/CX_AI_Diagnostic/` pre-dates the notebook-first model** — generated
|
||||||
|
notebook, content in YAML configs. Redesign pending; until then its shape is
|
||||||
|
intentional. Its `mypy` is also not clean in a fresh venv (11 pre-existing errors:
|
||||||
|
missing pandas/PyYAML/plotly stubs, unannotated functions) even though its
|
||||||
|
`pyproject.toml` declares strict mode — pytest and the headless execute are green;
|
||||||
|
fix the typing with the redesign, not piecemeal.
|
||||||
|
- **TEI twins + CTM pre-date the tagged-cell taxonomy** — the structural suite
|
||||||
|
grandfathers them by name in `tests/nbcheck.py`, each with its reason.
|
||||||
|
- **`template/MercuryNotebook/` encodes only the py-engine model** (and its
|
||||||
|
`staging.py` lacks the mypy-strict `backstage_md` variant); rework is a recorded
|
||||||
|
follow-up. Its `exports/*.{html,md}` are tracked — predates the exports rule.
|
||||||
|
- **`docs/brand.md` references a `brand_dark.md` that does not exist.**
|
||||||
|
- **`core.bootstrap.init(study=…)` imports `studies.<slug>.config`** — vestigial
|
||||||
|
(no study ships a `config.py`; `studies/__init__.py` exists to serve it). Don't
|
||||||
|
remove the `__init__.py` without retiring that path.
|
||||||
|
|
||||||
|
## Identified opportunities (recorded, not built)
|
||||||
|
|
||||||
|
- A private installable Palladium package on the Gitea (CI exists there) — would
|
||||||
|
replace per-master `staging.py` copies; revisit when the duplication bites.
|
||||||
|
- A copy-out helper script (`scripts/new_engagement.py`) — after the manual checklist
|
||||||
|
has been exercised a few times.
|
||||||
|
- CTM extraction to a confidential engagement archive.
|
||||||
|
- TEI + AI Diagnostic redesigns to the notebook-first model (each moves its notebook
|
||||||
|
from GRANDFATHERED to NOTEBOOK_FIRST in `tests/nbcheck.py`).
|
||||||
|
- Template rework: an Assessment template derived from the Discovery reference;
|
||||||
|
staging.py mypy-strict retrofit across masters.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
**Read the pattern for your master type before working in it — they hold the detail
|
||||||
|
this file summarises:**
|
||||||
|
|
||||||
|
- [docs/Assessment_Pattern_V1-00.md](docs/Assessment_Pattern_V1-00.md) — assessments:
|
||||||
|
notebook-first content, engagement-data cell, copy-out checklist
|
||||||
|
- [docs/Study_Pattern_V1-00.md](docs/Study_Pattern_V1-00.md) — studies: verbatim
|
||||||
|
anchor + overlay, reproduction gate, published-PDF exception
|
||||||
|
|
||||||
|
Everything else:
|
||||||
|
|
||||||
|
- [docs/Mercury_Notebook_Pattern_V1-00.md](docs/Mercury_Notebook_Pattern_V1-00.md) —
|
||||||
|
the shared mechanics (reactivity, gate, staging, appendix, packaging)
|
||||||
|
- [docs/Notebook_Review_Prompt_V1-00.md](docs/Notebook_Review_Prompt_V1-00.md) — the
|
||||||
|
LLM review; run as `/review-notebook <path>`
|
||||||
|
- [docs/brand.md](docs/brand.md) — NTT DATA palette for stages and exports
|
||||||
|
- [docs/Athena_TEI.md](docs/Athena_TEI.md) + `docs/Athena API.yaml` — the core/Athena
|
||||||
|
layer
|
||||||
|
- [README.md](README.md) — taxonomy, quick start, engagement workflow
|
||||||
10
Makefile
10
Makefile
@@ -4,7 +4,7 @@ VENV := .venv
|
|||||||
PY := $(VENV)/bin/python
|
PY := $(VENV)/bin/python
|
||||||
PIP := $(VENV)/bin/pip
|
PIP := $(VENV)/bin/pip
|
||||||
|
|
||||||
.PHONY: setup lab test lint format clean
|
.PHONY: setup lab test check-notebooks lint format clean
|
||||||
|
|
||||||
## One-time: create venv, install deps + palladium (editable)
|
## One-time: create venv, install deps + palladium (editable)
|
||||||
setup:
|
setup:
|
||||||
@@ -19,10 +19,16 @@ setup:
|
|||||||
lab:
|
lab:
|
||||||
$(VENV)/bin/jupyter lab
|
$(VENV)/bin/jupyter lab
|
||||||
|
|
||||||
## Run the test suite (no Athena connection needed — HTTP is mocked)
|
## Run the test suite (no Athena connection needed — HTTP is mocked).
|
||||||
|
## Includes the notebook structural suite (tests/test_notebooks.py).
|
||||||
test:
|
test:
|
||||||
$(PY) -m pytest tests/ -v
|
$(PY) -m pytest tests/ -v
|
||||||
|
|
||||||
|
## Structural checks for every master notebook (kernel-free; -rs shows
|
||||||
|
## why grandfathered notebooks skip the notebook-first tier)
|
||||||
|
check-notebooks:
|
||||||
|
$(PY) -m pytest tests/test_notebooks.py -rs
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
$(VENV)/bin/ruff check .
|
$(VENV)/bin/ruff check .
|
||||||
|
|
||||||
|
|||||||
508
README.md
508
README.md
@@ -1,425 +1,139 @@
|
|||||||
# Palladium
|
# Palladium
|
||||||
|
|
||||||
**TEI (Total Economic Impact) Calculator** — The strategic artifact that protects the business case.
|
**A library of master notebooks for consulting delivery** — workshop instruments and
|
||||||
|
business-case studies, built as Mercury-served Jupyter deliverables.
|
||||||
|
|
||||||
Palladium is a Jupyter-notebook toolkit for building Total Economic Impact analyses. Each study is a self-contained Mercury-served notebook deliverable (math in a study package, verification gate, LLM-readable exports); a small shared `core/` talks to [Athena](https://athena.nttdata.com) for client/opportunity context and server-side TEI tooling.
|
> *In Greek mythology, the Palladium was a sacred artifact of Athena that protected
|
||||||
|
> Troy. Whoever possessed it held strategic advantage. In our ecosystem, Palladium
|
||||||
|
> protects the deal — transforming discovery inputs into a case no CFO can ignore.*
|
||||||
|
|
||||||
> *In Greek mythology, the Palladium was a sacred artifact of Athena that protected Troy. Whoever possessed it held strategic advantage. In our ecosystem, Palladium protects the deal — transforming discovery inputs into a financial case no CFO can ignore.*
|
Every master obeys one **three-layer contract** (the always-on rules live in
|
||||||
|
[`CLAUDE.md`](CLAUDE.md)):
|
||||||
|
|
||||||
## Architecture
|
- **Mercury** is the polished, client-facing interface — the screen you share in a
|
||||||
|
workshop, remote or in person.
|
||||||
|
- **Jupyter notebook cells** are the consultant's interface — workshop **content**
|
||||||
|
and **client data** are edited in tagged cells, never in `.py` files.
|
||||||
|
- **Python modules** hold reusable logic and calculations worth calling as a module,
|
||||||
|
typed and covered by validation tests.
|
||||||
|
|
||||||
```
|
## Taxonomy
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Palladium │
|
|
||||||
│ │
|
|
||||||
│ studies/YYYYMM_TEI_Vendor_Product/ ← self-contained study │
|
|
||||||
│ studies/YYYYMM_Client_EngagementName/ (own venv + engine) │
|
|
||||||
│ ├─ <studylib>/ ← ALL math, verbatim anchors │
|
|
||||||
│ ├─ notebooks/ ← THE deliverable (Mercury-served) │
|
|
||||||
│ ├─ tests/ ← pinned acceptance numbers │
|
|
||||||
│ └─ exports/ ← .html/.md + JSON appendix (for LLMs, │
|
|
||||||
│ and the Athena repository roadmap) │
|
|
||||||
│ │
|
|
||||||
│ core/ ← shared Athena toolkit (studies do NOT import it) │
|
|
||||||
│ tei_client → ──────────────────────────► Athena API │
|
|
||||||
│ calculations · export · cli · bootstrap │
|
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Components
|
| Kind | What it is | Naming | Lives |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Study** | Reproduction of a dated base document (Forrester TEI or similar), personalized as an overlay | `YYYYMM_TEI_Vendor_Product` / `YYYYMM_Client_Engagement` (dated) | `studies/` |
|
||||||
|
| **Assessment** | Reusable workshop instrument (discovery workshop, diagnostic) | `Instrument_Name` (undated, living) | `assessments/` |
|
||||||
|
| **Engagement copy** | A master copied out for a client engagement — acquires client data, becomes **confidential** | `YYYYMM_Client_Instrument`, stamped at copy-time | **outside this repo** |
|
||||||
|
|
||||||
| Component | Purpose |
|
Masters in this repo stay **client-clean**: placeholder engagement data, published or
|
||||||
|-----------|---------|
|
synthetic numbers, nothing a client said. Patterns:
|
||||||
| **`studies/`** | One self-contained folder per engagement — own venv, engine package, Mercury notebook, tests, exports |
|
[Assessment](docs/Assessment_Pattern_V1-00.md) ·
|
||||||
| **`template/`** | Copy-me study scaffold — start here for new studies |
|
[Study](docs/Study_Pattern_V1-00.md) ·
|
||||||
| **`core/tei_client`** | Python API client for Athena's TEI endpoints |
|
[shared Mercury mechanics](docs/Mercury_Notebook_Pattern_V1-00.md).
|
||||||
| **`core/calculations`** | Financial logic — NPV, ROI, payback, risk adjustment, scenarios |
|
|
||||||
| **`core/export`** | Builds the structured JSON envelope consumed by the report pipeline |
|
|
||||||
| **`core/cli`** | `python -m palladium` command-line interface |
|
|
||||||
|
|
||||||
> **All studies follow the [Mercury Notebook Deliverable Pattern](docs/Mercury_Notebook_Pattern_V1-00.md)**:
|
## Repository layout
|
||||||
> the notebook *is* the artifact — self-contained study package, Mercury-served,
|
|
||||||
> gate-verified, LLM-exportable. Start from [`template/MercuryNotebook/`](template/MercuryNotebook/).
|
|
||||||
> The Streamlit `app/` and `core/notebook_helpers` were retired when the last
|
|
||||||
> legacy study migrated (git history keeps them).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start — Jupyter Lab first
|
|
||||||
|
|
||||||
Palladium is a **Jupyter Lab-first** environment. Everything starts from a
|
|
||||||
notebook; the CLI is a companion, not a prerequisite.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/nttdata/palladium.git
|
|
||||||
cd palladium
|
|
||||||
make setup # venv + deps + editable install (one time)
|
|
||||||
make lab # launches Jupyter Lab
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open **`00_setup.ipynb`** at the repo root. It will:
|
|
||||||
|
|
||||||
1. Prompt for your Athena API key (hidden input) and save it to `.env`
|
|
||||||
2. Test the connection
|
|
||||||
3. Show what report templates and tools exist in the instance
|
|
||||||
|
|
||||||
Current target instance: **https://athena.ouranos.helu.ca** (Ouranos sandbox —
|
|
||||||
no production data, safe to experiment).
|
|
||||||
|
|
||||||
From any root-level notebook, the Athena connection is one import (pattern
|
|
||||||
studies are self-contained and never import `core`):
|
|
||||||
|
|
||||||
```python
|
|
||||||
from core.bootstrap import init
|
|
||||||
|
|
||||||
pal = init() # loads .env, builds client, tests it
|
|
||||||
pal.client.list_reports()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
All credentials and IDs live in `<repo>/.env` (gitignored). `00_setup.ipynb`
|
|
||||||
writes it for you; to do it by hand:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env
|
|
||||||
ATHENA_BASE_URL=https://athena.ouranos.helu.ca
|
|
||||||
ATHENA_API_KEY=your-api-key-here
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verify Connection
|
|
||||||
|
|
||||||
In a notebook: `init()` prints the connection status. From a shell:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m palladium test
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Run a study end-to-end
|
|
||||||
|
|
||||||
Each new study is self-contained under the
|
|
||||||
[Mercury Notebook Deliverable Pattern](docs/Mercury_Notebook_Pattern_V1-00.md).
|
|
||||||
The reference TEI study is the February 2026 Forrester *Total Economic
|
|
||||||
Impact™ Of Amazon Connect* (pattern Variant 4 — composite reproduction):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd studies/202602_TEI_Amazon_Connect
|
|
||||||
python -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]"
|
|
||||||
mercury --working-dir notebooks/ # serve the deliverable (the stage)
|
|
||||||
python scripts/export_report.py # export .html/.md report sources
|
|
||||||
```
|
|
||||||
|
|
||||||
Its notebook reproduces the published totals within the PDF's rounding —
|
|
||||||
**NPV $78.7M • ROI 342% • Payback <6 months** — and the verification gate
|
|
||||||
asserts it on every headless run. See the study's README for details.
|
|
||||||
|
|
||||||
`studies/202512_TEI_Genesys_CX_Cloud/` follows the same shape (**NPV $10.8M
|
|
||||||
• ROI 266%**), with one signature input: the Genesys AI Experience token
|
|
||||||
line the published study models at $0, priced live from the client's quote.
|
|
||||||
`studies/202607_CTM_GenesysCX/` is the full multi-notebook reference
|
|
||||||
implementation.
|
|
||||||
|
|
||||||
### CLI
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test connection
|
|
||||||
python -m palladium test
|
|
||||||
|
|
||||||
# List TEI tool instances
|
|
||||||
python -m palladium list
|
|
||||||
|
|
||||||
# List available report templates
|
|
||||||
python -m palladium reports
|
|
||||||
|
|
||||||
# Show financial summary for a tool
|
|
||||||
python -m palladium summary <public_id>
|
|
||||||
|
|
||||||
# Trigger server-side recalculation
|
|
||||||
python -m palladium calculate <public_id>
|
|
||||||
|
|
||||||
# Export for the report pipeline
|
|
||||||
python -m palladium export <public_id> -o export.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pytest tests/ -v
|
|
||||||
```
|
|
||||||
|
|
||||||
The root suite covers the API client (mocked HTTP), the financial math, and
|
|
||||||
the export envelope shape; the Amazon Connect verbatim anchor is asserted
|
|
||||||
against the published Forrester totals. Each study additionally carries its
|
|
||||||
own pinned suite (`cd studies/<slug> && pytest`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new study
|
|
||||||
|
|
||||||
Copy the template, not an existing study:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp -r template/MercuryNotebook studies/YYYYMM_TEI_Vendor_Product
|
|
||||||
```
|
|
||||||
|
|
||||||
Then follow `template/MercuryNotebook/README.md`: rename `studylib/` to
|
|
||||||
your study package (underscores only — dashes break Python imports),
|
|
||||||
replace the toy model, re-pin the tests, rework the notebook.
|
|
||||||
`studies/202602_TEI_Amazon_Connect/` is the worked TEI example;
|
|
||||||
`studies/202607_CTM_GenesysCX/` is the full multi-notebook reference.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TEI Methodology
|
|
||||||
|
|
||||||
Palladium implements the Forrester TEI™ framework.
|
|
||||||
|
|
||||||
### Benefit Categories
|
|
||||||
|
|
||||||
Benefits are quantified across categories, risk-adjusted, and discounted to present value:
|
|
||||||
|
|
||||||
| Category | Examples |
|
|
||||||
|----------|----------|
|
|
||||||
| **Cost Savings** | Legacy license elimination, reduced headcount, lower telecom |
|
|
||||||
| **Productivity** | Reduced handle time, faster training, automated QA |
|
|
||||||
| **Revenue** | Improved retention, better conversion, new channel revenue |
|
|
||||||
| **Risk Reduction** | Compliance automation, reduced legal exposure, audit readiness |
|
|
||||||
|
|
||||||
### Risk Adjustment
|
|
||||||
|
|
||||||
Each benefit carries a risk-adjustment factor (0–50%) reflecting implementation uncertainty.
|
|
||||||
A 20% risk adjustment on a $10M benefit yields a risk-adjusted value of $8M.
|
|
||||||
**Costs** are risk-adjusted **upward** by the same factor (higher risk → higher modelled cost).
|
|
||||||
|
|
||||||
### Financial Metrics
|
|
||||||
|
|
||||||
| Metric | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| **NPV** | Net Present Value — total risk-adjusted benefits minus costs, discounted |
|
|
||||||
| **ROI** | Return on Investment — `(benefits − costs) / costs × 100` |
|
|
||||||
| **Payback** | Months until cumulative benefits exceed cumulative costs |
|
|
||||||
|
|
||||||
The initial investment (year 0) is **not** discounted. Year-N cashflows are
|
|
||||||
discounted at the end of the year: `PV = CF_n / (1 + r)^n`. This matches
|
|
||||||
the Forrester methodology used in the published studies.
|
|
||||||
|
|
||||||
### Scenario Analysis
|
|
||||||
|
|
||||||
Three scenarios model uncertainty in adoption and realization
|
|
||||||
(see `core.calculations.SCENARIOS`):
|
|
||||||
|
|
||||||
| Scenario | Adoption | Risk delta | Effect |
|
|
||||||
|----------|----------|------------|--------|
|
|
||||||
| Conservative | 80% | +10pp on benefits | Lower benefits, higher modelled cost |
|
|
||||||
| Moderate | 100% | 0 | Base case (= published study) |
|
|
||||||
| Aggressive | 115% | –5pp on benefits | Higher benefits, lower padding on cost |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
```
|
||||||
palladium/
|
palladium/
|
||||||
├── 00_setup.ipynb # ← START HERE: credentials + connection
|
├── CLAUDE.md # the always-on contract (rubric, layers, risk tiers)
|
||||||
├── Makefile # make setup / lab / test
|
├── assessments/
|
||||||
├── core/ # Shared, study-agnostic Python package
|
│ ├── CX_Discovery_Workshop/ # ★ reference implementation (notebook-first model)
|
||||||
│ ├── bootstrap.py # one-import notebook setup (init, save_credentials)
|
│ └── CX_AI_Diagnostic/ # capability diagnostic (pre-redesign: generated notebook)
|
||||||
│ ├── tei_client/ # Athena API client
|
├── studies/
|
||||||
│ │ ├── client.py # TEIClient with all /api/v1/tei/ methods
|
│ ├── 202512_TEI_Genesys_CX_Cloud/ # Forrester TEI reproduction — NPV $10.8M · ROI 266%
|
||||||
│ │ └── models.py # Optional dataclasses for typed access
|
│ ├── 202602_TEI_Amazon_Connect/ # Forrester TEI reproduction — NPV $78.7M · ROI 342%
|
||||||
│ ├── calculations/ # Pure-python financial math
|
│ └── 202607_CTM_GenesysCX/ # client engagement study (grandfathered — see CLAUDE.md)
|
||||||
│ │ ├── npv.py
|
├── template/MercuryNotebook/ # copy-me scaffold (py-engine model; see "Adding a master")
|
||||||
│ │ ├── roi.py
|
├── docs/ # the pattern docs, brand.md, review prompt, Athena reference
|
||||||
│ │ ├── payback.py
|
├── core/ # shared Athena toolkit (masters do NOT import it)
|
||||||
│ │ └── scenarios.py
|
├── tests/ # core tests + the notebook structural suite (all masters)
|
||||||
│ ├── export/
|
├── 00_setup.ipynb # Athena credentials + connection (core layer)
|
||||||
│ │ └── report_data.py # JSON envelope for the report pipeline
|
└── Makefile # make setup / lab / test / check-notebooks
|
||||||
│ └── cli/
|
|
||||||
│ └── main.py # `python -m palladium ...`
|
|
||||||
├── palladium/ # CLI shim (just exposes `python -m palladium`)
|
|
||||||
│ └── __main__.py
|
|
||||||
├── template/
|
|
||||||
│ └── MercuryNotebook/ # copy-me pattern scaffold (runnable)
|
|
||||||
├── studies/ # One self-contained folder per engagement
|
|
||||||
│ ├── 202512_TEI_Genesys_CX_Cloud/ # CX Cloud TEI — pattern Variant 4
|
|
||||||
│ │ ├── README.md # NPV $10.8M · ROI 266% + the $0 AI-token line
|
|
||||||
│ │ ├── teicalc/ # self-contained engine (anchor/model/overlay)
|
|
||||||
│ │ ├── notebooks/business_case.ipynb
|
|
||||||
│ │ ├── tests/ · scripts/ · config.toml · pyproject.toml
|
|
||||||
│ │ └── docs/ # Forrester PDF + Genesys token-metering notes
|
|
||||||
│ ├── 202602_TEI_Amazon_Connect/ # Amazon Connect TEI — pattern Variant 4
|
|
||||||
│ │ ├── README.md # NPV $78.7M · ROI 342%, reproduced + gated
|
|
||||||
│ │ ├── teicalc/ # self-contained engine (anchor/model/overlay)
|
|
||||||
│ │ ├── notebooks/business_case.ipynb
|
|
||||||
│ │ ├── tests/ · scripts/ · config.toml · pyproject.toml
|
|
||||||
│ │ ├── exports/ # generated; .gitignored
|
|
||||||
│ │ └── docs/
|
|
||||||
│ │ └── 202602_TEI Report Amazon Connect.pdf
|
|
||||||
│ └── 202607_CTM_GenesysCX/ # CTM × Genesys study — pattern reference impl
|
|
||||||
├── tests/ # root tests for core/
|
|
||||||
│ ├── test_client.py
|
|
||||||
│ ├── test_calculations.py
|
|
||||||
│ └── test_export.py
|
|
||||||
├── Athena API.yaml # OpenAPI reference
|
|
||||||
├── .env.example
|
|
||||||
├── requirements.txt
|
|
||||||
├── pyproject.toml
|
|
||||||
└── README.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Each master is **self-contained**: its own engine package, venv, tests, Mercury
|
||||||
|
config, and export script — a copy of the directory runs standalone.
|
||||||
|
|
||||||
## Athena Integration
|
## Quick start — run a master
|
||||||
|
|
||||||
Palladium connects to Athena's TEI module for data persistence and cross-tool reporting.
|
|
||||||
|
|
||||||
### API Endpoints Used
|
|
||||||
|
|
||||||
All endpoints are under `/api/v1/tei/` and require `Authorization: Api-Key {key}`.
|
|
||||||
|
|
||||||
| Endpoint | Purpose |
|
|
||||||
|----------|---------|
|
|
||||||
| `GET /api/v1/tei/reports/` | List available TEI report templates |
|
|
||||||
| `GET /api/v1/tei/reports/{public_id}/` | Get a report template |
|
|
||||||
| `GET /api/v1/tei/reports/{public_id}/fields/` | Get field definitions for a template |
|
|
||||||
| `POST /api/v1/tei/tools/` | Create a new TEI tool instance |
|
|
||||||
| `GET /api/v1/tei/tools/{public_id}/` | Get instance metadata |
|
|
||||||
| `PATCH /api/v1/tei/tools/{public_id}/` | Update name/status |
|
|
||||||
| `GET /api/v1/tei/tools/{public_id}/values/` | Get current field values |
|
|
||||||
| `PUT /api/v1/tei/tools/{public_id}/values/` | Bulk-update values |
|
|
||||||
| `PATCH /api/v1/tei/tools/{public_id}/values/{field_key}/` | Patch a single value |
|
|
||||||
| `POST /api/v1/tei/tools/{public_id}/calculate/` | Trigger calculation |
|
|
||||||
| `GET /api/v1/tei/tools/{public_id}/summary/` | Get financial summary |
|
|
||||||
| `GET /api/v1/tei/tools/{public_id}/versions/` | List version snapshots |
|
|
||||||
| `POST /api/v1/tei/tools/{public_id}/versions/` | Save a new version |
|
|
||||||
| `GET /api/v1/tei/tools/{public_id}/versions/{n}/` | Get a specific version |
|
|
||||||
| `GET /api/v1/tei/tools/{public_id}/export/` | Export for the report pipeline |
|
|
||||||
| `GET /api/v1/tei/summary/` | Aggregate NPV across all tools |
|
|
||||||
|
|
||||||
### Object model
|
|
||||||
|
|
||||||
| Athena object | Notes |
|
|
||||||
|---|---|
|
|
||||||
| **Opportunity** | Top-level sales record. Owns one or more **Proposals**. |
|
|
||||||
| **Proposal** | A specific bid/offer to a client. **A TEI tool is linked to a Proposal.** |
|
|
||||||
| **Engagement** | Optional — for active client engagements. A TEI tool may also link here. |
|
|
||||||
| **TEIReport** | Template (e.g. *Amazon Connect 2026*) — defines fields, discount rate, analysis horizon. |
|
|
||||||
| **TEITool** | Instance of a Report bound to a Proposal — holds values, summaries, versions. |
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
```
|
|
||||||
Authorization: Api-Key {your-api-key}
|
|
||||||
```
|
|
||||||
|
|
||||||
API keys are provisioned in Athena's admin interface per user/service account.
|
|
||||||
|
|
||||||
### Methodology conventions (Palladium ↔ Athena)
|
|
||||||
|
|
||||||
Two places where the Forrester methodology and the Athena TEI API differ, and
|
|
||||||
how Palladium bridges them:
|
|
||||||
|
|
||||||
| Topic | Athena behaviour | Palladium convention |
|
|
||||||
|---|---|---|
|
|
||||||
| **Cost risk adjustment** | Costs are never risk-adjusted server-side | Cost values are pushed pre-multiplied by `(1 + risk_adj)`; field-level adjustment stays 0 |
|
|
||||||
| **Year-0 "Initial" costs** | No year-0 concept; non-annual values are folded into Year 1 | Each cost gets a companion non-annual `<key>_initial` field. `TEIClient` folds them back into an `initial` key on read. Athena discounts these as Year 1 (Forrester doesn't discount Year 0) — expect ≈0.15% drift on cost PV |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Report Pipeline Integration
|
|
||||||
|
|
||||||
Palladium's export produces structured JSON consumed by the LLM report generation pipeline:
|
|
||||||
|
|
||||||
```
|
|
||||||
Palladium Export (JSON)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Peitho — LLM generates HTML (following HTML_DOCUMENT_FORMAT.md)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
html2docx converts to native Word
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Professional TEI Report (.docx)
|
|
||||||
```
|
|
||||||
|
|
||||||
The export envelope (`core.export.build_report_data`) includes:
|
|
||||||
- All benefit categories with risk-adjusted values
|
|
||||||
- All cost categories with yearly breakdown (and Initial column)
|
|
||||||
- Financial summary (NPV, ROI, payback, yearly cashflow)
|
|
||||||
- Conservative / moderate / aggressive scenario analysis
|
|
||||||
- Metadata (study slug, proposal, engagement, generator stamp)
|
|
||||||
- The raw Athena `/export/` payload for reference
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version Management
|
|
||||||
|
|
||||||
Athena keeps version history for TEI tools, driven through the API
|
|
||||||
(`core.tei_client`: `save_version` / `list_versions` / `get_version`):
|
|
||||||
|
|
||||||
1. **Save Version** — Snapshots current values + summary with a descriptive note
|
|
||||||
2. **View History** — All versions with headline metrics (NPV, ROI)
|
|
||||||
3. **Compare Versions** — Side-by-side diff of value changes between any two versions
|
|
||||||
4. **Restore Version** — Load a previous version's values as the current state
|
|
||||||
|
|
||||||
Version notes should capture:
|
|
||||||
- Assumptions made and their sources
|
|
||||||
- Which scenario the version represents
|
|
||||||
- What changed since the previous version
|
|
||||||
- Client confirmations or corrections
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pytest tests/ -v
|
git clone ssh://git@git.helu.ca:22022/r/palladium.git && cd palladium
|
||||||
|
|
||||||
|
cd assessments/CX_Discovery_Workshop
|
||||||
|
python -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
mercury --working-dir . # the stage — share this screen with the client
|
||||||
|
jupyter lab # backstage — edit content/data cells, see the script
|
||||||
|
pytest # content + engagement + engine pins
|
||||||
|
python scripts/export_report.py # exports/*.html + LLM-ready *.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Tests are designed to run without an Athena connection — HTTP is mocked
|
Every master's README carries its own specifics; the commands are the same shape in
|
||||||
and the calculation suite uses the Amazon Connect seed data to verify the
|
all of them.
|
||||||
Forrester numbers reproduce within rounding.
|
|
||||||
|
|
||||||
### Code Style
|
## Client engagements — copy out, never in place
|
||||||
|
|
||||||
|
A master never touches client data. To run one for a client (full checklist in the
|
||||||
|
[Assessment Pattern](docs/Assessment_Pattern_V1-00.md)):
|
||||||
|
|
||||||
|
1. **Copy** the master directory out of Palladium to your engagement location.
|
||||||
|
2. **Rename** it `YYYYMM_Client_Instrument` (e.g. `202608_Acme_CX_Discovery`).
|
||||||
|
3. **Provision** a fresh venv there (`pip install -e ".[dev]"` — copied venvs are broken).
|
||||||
|
4. **Fill the `engagement-data` cell** with the client facts.
|
||||||
|
5. **Verify** — `pytest` + the headless gate stay green by design.
|
||||||
|
|
||||||
|
The copy is now **confidential**: it lives with the engagement, never merges back.
|
||||||
|
Improvements found on engagements are hand-carried to the master as clean edits.
|
||||||
|
`.gitignore` blocks client documents repo-wide — see `CLAUDE.md` § Confidentiality.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Two mechanisms guard every master:
|
||||||
|
|
||||||
|
**Deterministic** — per master (in its venv): engine pins, content pins read from the
|
||||||
|
notebook's tagged cells, the stage/backstage test, and the in-notebook verification
|
||||||
|
gate under headless `nbconvert --execute`. Repo-wide (root venv):
|
||||||
|
`make check-notebooks` runs the kernel-free structural suite over every committed
|
||||||
|
notebook — parses, cleanly executed, tag taxonomy present ([tests/nbcheck.py](tests/nbcheck.py)
|
||||||
|
classifies every notebook; grandfathered ones skip with a recorded reason).
|
||||||
|
|
||||||
|
**LLM review** — [`docs/Notebook_Review_Prompt_V1-00.md`](docs/Notebook_Review_Prompt_V1-00.md)
|
||||||
|
hunts what deterministic checks can't: content oddities, cross-cell rot, stale
|
||||||
|
numbers, stage leakage. Run it in-repo with `/review-notebook <path>` (Claude Code),
|
||||||
|
or paste the prompt into any LLM alongside the `.ipynb` JSON.
|
||||||
|
|
||||||
|
## Adding a new master
|
||||||
|
|
||||||
|
For an **Assessment**, start from the reference implementation and its pattern doc —
|
||||||
|
`assessments/CX_Discovery_Workshop/` + [Assessment Pattern](docs/Assessment_Pattern_V1-00.md).
|
||||||
|
For a **Study**, copy `template/MercuryNotebook/` and follow the
|
||||||
|
[Study Pattern](docs/Study_Pattern_V1-00.md) (note: the template still encodes the
|
||||||
|
py-engine model; its notebook-first rework is a recorded follow-up). Either way:
|
||||||
|
underscores in names (never dashes — directories are Python packages), and register
|
||||||
|
the new notebook in [tests/nbcheck.py](tests/nbcheck.py) — the completeness test
|
||||||
|
fails until you classify it.
|
||||||
|
|
||||||
|
## Athena / core (the TEI toolkit)
|
||||||
|
|
||||||
|
The `core/` package talks to [Athena](https://athena.nttdata.com) for
|
||||||
|
client/opportunity context and server-side TEI tooling — masters do **not** import
|
||||||
|
it. Start at **`00_setup.ipynb`** (`make setup && make lab`): it prompts for the API
|
||||||
|
key, writes `.env`, and tests the connection (current target:
|
||||||
|
`https://athena.ouranos.helu.ca`, the Ouranos sandbox).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ruff check .
|
python -m palladium test | list | reports | summary <id> | calculate <id> | export <id> -o export.json
|
||||||
ruff format .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adding a New Benefit Category
|
The export JSON feeds the report pipeline: **Palladium → Peitho (LLM → HTML) →
|
||||||
|
html2docx → .docx**. Full API reference, object model, calculation logic, and the
|
||||||
|
Palladium↔Athena methodology bridges: [`docs/Athena_TEI.md`](docs/Athena_TEI.md) +
|
||||||
|
[`docs/Athena API.yaml`](docs/Athena%20API.yaml). Root tests (`make test`) cover the
|
||||||
|
client (mocked HTTP), the financial math, the export envelope — and the notebook
|
||||||
|
structural suite.
|
||||||
|
|
||||||
1. Define the field in Athena's TEI Report admin (field name, type, category, defaults)
|
## Related projects
|
||||||
2. The field automatically appears in Palladium via the API — no client changes
|
|
||||||
3. Update notebook prose if category-specific commentary is needed
|
|
||||||
4. If the report template exposes a new structure, extend the envelope in
|
|
||||||
`core/export/report_data.py`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
|
||||||
|---------|---------|---------|
|
|
||||||
| `requests` | ≥2.31 | HTTP client for Athena API |
|
|
||||||
| `python-dotenv` | ≥1.0 | Environment configuration |
|
|
||||||
| `jupyter` | ≥1.0 | Notebook environment |
|
|
||||||
| `pandas` | ≥2.0 | Data manipulation |
|
|
||||||
| `plotly` | ≥5.18 | Interactive visualizations |
|
|
||||||
| `numpy` | ≥1.26 | Financial calculations |
|
|
||||||
| `pytest` | ≥7.4 | Testing |
|
|
||||||
| `ruff` | ≥0.1 | Linting and formatting |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Related Projects
|
|
||||||
|
|
||||||
| Project | Relationship |
|
| Project | Relationship |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# 202607 — CX Exploration & Discovery Workshop
|
# CX Exploration & Discovery Workshop
|
||||||
|
|
||||||
A **live facilitation aid** for a CX discovery session, built on the
|
An **Assessment** — a reusable workshop master (see
|
||||||
|
[`docs/Assessment_Pattern_V1-00.md`](../../docs/Assessment_Pattern_V1-00.md))
|
||||||
|
— and a **live facilitation aid** for a CX discovery session, built on the
|
||||||
[Mercury Notebook Deliverable Pattern](../../docs/Mercury_Notebook_Pattern_V1-00.md).
|
[Mercury Notebook Deliverable Pattern](../../docs/Mercury_Notebook_Pattern_V1-00.md).
|
||||||
Unlike the TEI business-case studies, this deliverable computes no dollars —
|
Unlike the TEI business-case studies, this deliverable computes no dollars —
|
||||||
its "numbers" are **topic status and progress**. The Mercury stage is the
|
its "numbers" are **topic status and progress**. The Mercury stage is the
|
||||||
@@ -32,36 +34,44 @@ structured form of the source survey.
|
|||||||
|
|
||||||
## Where the content lives: in the notebook
|
## Where the content lives: in the notebook
|
||||||
|
|
||||||
The topic bank — every topic, sub-topic, and facilitator prompt — lives in
|
Two tagged cells of
|
||||||
the **`topic-bank` cell** of
|
[`notebooks/cx_discovery.ipynb`](notebooks/cx_discovery.ipynb) carry
|
||||||
[`notebooks/cx_discovery.ipynb`](notebooks/cx_discovery.ipynb) (the code cell
|
everything a consultant edits — **in Jupyter, never in a `.py` file**:
|
||||||
tagged `topic-bank`, right under the title). **Content is edited there, in
|
|
||||||
Jupyter — never in a `.py` file.** It is the study's verbatim anchor: wording
|
- **`topic-bank`** — the workshop content: every topic, sub-topic, and
|
||||||
tracks the source survey
|
facilitator prompt. It is the study's verbatim anchor: wording tracks the
|
||||||
[`docs/cx_discovery_survey.md`](docs/cx_discovery_survey.md) (the original
|
source survey [`docs/cx_discovery_survey.md`](docs/cx_discovery_survey.md)
|
||||||
`cxxm.md`), and the `key` slugs are stable identities the sidebar widgets,
|
(the original `cxxm.md`), and the `key` slugs are stable identities the
|
||||||
captured notes, and JSON export all key off — never renumber or rename them
|
sidebar widgets, captured notes, and JSON export all key off — never
|
||||||
casually. The cell's own comment block carries the full editing rules.
|
renumber or rename them casually. The cell's own comment block carries the
|
||||||
|
full editing rules.
|
||||||
|
- **`engagement-data`** — the client facts (client, workshop date,
|
||||||
|
facilitator, attendees). **Placeholders in this master**; filled in the
|
||||||
|
engagement copy. The values flow to the stage board header, the export
|
||||||
|
preamble, and the JSON appendix `meta`.
|
||||||
|
|
||||||
`discoverylib/` holds **code only**: the `Topic`/`SubTopic` schema, the
|
`discoverylib/` holds **code only**: the `Topic`/`SubTopic` schema, the
|
||||||
status vocabulary, and the session engine — every engine function takes the
|
status vocabulary, and the session engine — every engine function takes the
|
||||||
bank as its first argument. The ~42 sidebar widgets are built by a runtime
|
bank as its first argument. The ~42 sidebar widgets are built by a runtime
|
||||||
loop over `TOPICS`, so board, checklist, script, gate, and export all pick up
|
loop over `TOPICS`, so board, checklist, script, gate, and export all pick up
|
||||||
a content edit automatically. The test suite reads the tagged cell straight
|
a content edit automatically. The test suite reads the tagged cells straight
|
||||||
out of the notebook (no kernel) and pins the content, so `pytest` guards the
|
out of the notebook (no kernel) and pins content shape, so `pytest` guards
|
||||||
bank exactly as shipped.
|
the bank exactly as shipped.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
notebooks/cx_discovery.ipynb # THE deliverable — content (topic-bank cell) + presentation
|
notebooks/cx_discovery.ipynb # THE deliverable — content + data + presentation
|
||||||
|
# tagged cells: topic-bank · engagement-data ·
|
||||||
|
# presentation ×4 · gate · data-appendix
|
||||||
|
# stage shows ONLY: title · progress line · board
|
||||||
discoverylib/ # the engine — code only, no content
|
discoverylib/ # the engine — code only, no content
|
||||||
session.py # Topic/SubTopic schema, status vocabulary, progress, export payload
|
session.py # Topic/SubTopic schema, status vocabulary, progress, export payload
|
||||||
staging.py # stage/backstage detection (copied verbatim)
|
staging.py # stage/backstage detection (+ backstage_md for the appendix)
|
||||||
scripts/export_report.py # nbconvert → exports/*.html + *.md
|
scripts/export_report.py # execute once → exports/*.html + LLM-ready *.md
|
||||||
tests/ # content pins (read from the notebook) + engine pins + staging test
|
tests/ # content + engagement pins (read from the notebook), engine pins, staging
|
||||||
docs/cx_discovery_survey.md # source survey (the original cxxm.md)
|
docs/cx_discovery_survey.md # source survey (the original cxxm.md)
|
||||||
exports/ # generated report sources
|
exports/ # generated report sources (never committed)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
@@ -72,11 +82,17 @@ pip install -e ".[dev]"
|
|||||||
|
|
||||||
mercury --working-dir . # serve the stage (share this screen)
|
mercury --working-dir . # serve the stage (share this screen)
|
||||||
jupyter lab # analyst / facilitator view
|
jupyter lab # analyst / facilitator view
|
||||||
pytest # content pins + engine pins + stage/backstage
|
pytest # content + engagement + engine + staging pins
|
||||||
jupyter nbconvert --to notebook --execute --inplace notebooks/cx_discovery.ipynb # gate
|
jupyter nbconvert --to notebook --execute --inplace notebooks/cx_discovery.ipynb # gate
|
||||||
python scripts/export_report.py # exports/*.html + *.md for the LLM handoff
|
python scripts/export_report.py # exports/*.html + *.md for the LLM handoff
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `.md` export opens with a generated preamble (what the document is, the
|
||||||
|
engagement line, how to read it) and ends with the **Session state (JSON)**
|
||||||
|
block — the machine source of truth for the write-up. Presentation cells
|
||||||
|
(setup, the facilitator run-book, widgets, board) are stripped from it; the
|
||||||
|
HTML export keeps the full presentation for human review.
|
||||||
|
|
||||||
## Extending
|
## Extending
|
||||||
|
|
||||||
New or reshaped discovery content is an edit to the notebook's `topic-bank`
|
New or reshaped discovery content is an edit to the notebook's `topic-bank`
|
||||||
@@ -92,3 +108,9 @@ Add a topic and the sidebar controls, board, checklist, script, gate, and
|
|||||||
export all pick it up — they're all derived from `TOPICS` at runtime. New
|
export all pick it up — they're all derived from `TOPICS` at runtime. New
|
||||||
*logic* (not content) goes in `discoverylib/session.py` with pins in
|
*logic* (not content) goes in `discoverylib/session.py` with pins in
|
||||||
`tests/test_session.py`.
|
`tests/test_session.py`.
|
||||||
|
|
||||||
|
**Running this for a client?** Don't fill client data into this master —
|
||||||
|
follow the copy-out checklist in
|
||||||
|
[`docs/Assessment_Pattern_V1-00.md`](../../docs/Assessment_Pattern_V1-00.md):
|
||||||
|
copy the directory out of Palladium, stamp it `YYYYMM_Client_CX_Discovery`,
|
||||||
|
fill the `engagement-data` cell there, and treat the copy as confidential.
|
||||||
@@ -5,18 +5,18 @@
|
|||||||
[main]
|
[main]
|
||||||
title = "CX Discovery Workshop"
|
title = "CX Discovery Workshop"
|
||||||
favicon_emoji = "🧭"
|
favicon_emoji = "🧭"
|
||||||
footer = "CX Exploration & Discovery Workshop"
|
footer = "CX Exploration & Discovery Workshop · NTT DATA"
|
||||||
notebooks_button_label = "Workshops"
|
notebooks_button_label = "Workshops"
|
||||||
|
|
||||||
[welcome]
|
[welcome]
|
||||||
header = "CX Discovery Workshop"
|
header = "CX Discovery Workshop"
|
||||||
message = """
|
message = """
|
||||||
The live visual for a CX exploration & discovery session. Topics and
|
The live visual for a CX exploration & discovery session — open
|
||||||
progress render on screen for the client; the facilitator drives the
|
**cx_discovery.ipynb**. Topics and progress render on screen for the
|
||||||
question script and captures notes backstage. Mark each topic's status in
|
client; the facilitator drives the question script and captures notes
|
||||||
the sidebar as the conversation moves — the board and the progress bar
|
backstage. Mark each topic's status in the sidebar as the conversation
|
||||||
update live. Afterward, export the captured notes for drafting with
|
moves — the board and the progress bar update live. Afterward, export the
|
||||||
`python scripts/export_report.py`.
|
captured notes for drafting with `python scripts/export_report.py`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[theme]
|
[theme]
|
||||||
@@ -46,6 +46,12 @@ focus_border_color = "#0072bc"
|
|||||||
hover_background_color = "#eef5fb"
|
hover_background_color = "#eef5fb"
|
||||||
selected_background_color = "#dcecfa"
|
selected_background_color = "#dcecfa"
|
||||||
|
|
||||||
|
# ── State colors — brand Success / Warning / Error (docs/brand.md) ──
|
||||||
|
success_color = "#00cb5d"
|
||||||
|
warning_color = "#ffc400"
|
||||||
|
danger_color = "#e42600"
|
||||||
|
slider_track_color = "#e2e6e9"
|
||||||
|
|
||||||
# ── Sidebar — clean white, hairline divider ──
|
# ── Sidebar — clean white, hairline divider ──
|
||||||
sidebar_background_color = "#ffffff"
|
sidebar_background_color = "#ffffff"
|
||||||
sidebar_text_color = "#2e404d"
|
sidebar_text_color = "#2e404d"
|
||||||
@@ -40,9 +40,9 @@ from .session import (
|
|||||||
subtopic_checklist,
|
subtopic_checklist,
|
||||||
subtopic_id,
|
subtopic_id,
|
||||||
)
|
)
|
||||||
from .staging import backstage, on_stage
|
from .staging import backstage, backstage_md, on_stage
|
||||||
|
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.3.0"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# schema
|
# schema
|
||||||
@@ -55,5 +55,5 @@ __all__ = [
|
|||||||
"ChecklistItem", "subtopic_checklist", "subtopic_id",
|
"ChecklistItem", "subtopic_checklist", "subtopic_id",
|
||||||
"TopicState", "build_session", "session_json",
|
"TopicState", "build_session", "session_json",
|
||||||
# staging
|
# staging
|
||||||
"on_stage", "backstage",
|
"on_stage", "backstage", "backstage_md",
|
||||||
]
|
]
|
||||||
@@ -196,6 +196,9 @@ class TopicState:
|
|||||||
notes: str
|
notes: str
|
||||||
subtopics_total: int
|
subtopics_total: int
|
||||||
subtopics_done: int
|
subtopics_done: int
|
||||||
|
# The covered sub-topic KEYS in canonical bank order — the export must
|
||||||
|
# say WHICH threads were discussed, not just how many.
|
||||||
|
subtopics_covered: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
def build_session(
|
def build_session(
|
||||||
@@ -216,8 +219,9 @@ def build_session(
|
|||||||
topic_states: list[TopicState] = []
|
topic_states: list[TopicState] = []
|
||||||
for t in topics:
|
for t in topics:
|
||||||
status = normalize_status(status_by_topic.get(t.key))
|
status = normalize_status(status_by_topic.get(t.key))
|
||||||
done = sum(
|
covered = tuple(
|
||||||
1 for st in t.subtopics if subtopic_id(t.key, st.key) in done_subtopics
|
st.key for st in t.subtopics
|
||||||
|
if subtopic_id(t.key, st.key) in done_subtopics
|
||||||
)
|
)
|
||||||
topic_states.append(
|
topic_states.append(
|
||||||
TopicState(
|
TopicState(
|
||||||
@@ -231,7 +235,8 @@ def build_session(
|
|||||||
color=STATUS_COLOR[status],
|
color=STATUS_COLOR[status],
|
||||||
notes=(notes_by_topic.get(t.key) or "").strip(),
|
notes=(notes_by_topic.get(t.key) or "").strip(),
|
||||||
subtopics_total=len(t.subtopics),
|
subtopics_total=len(t.subtopics),
|
||||||
subtopics_done=done,
|
subtopics_done=len(covered),
|
||||||
|
subtopics_covered=covered,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -255,7 +260,7 @@ def session_json(
|
|||||||
appendix does."""
|
appendix does."""
|
||||||
prog: Progress = session["progress"]
|
prog: Progress = session["progress"]
|
||||||
return {
|
return {
|
||||||
"study": "202607_CX_Discovery_Workshop",
|
"assessment": "CX_Discovery_Workshop",
|
||||||
"instrument": "CX Exploration & Discovery Workshop",
|
"instrument": "CX Exploration & Discovery Workshop",
|
||||||
"meta": meta or {},
|
"meta": meta or {},
|
||||||
"progress": {
|
"progress": {
|
||||||
@@ -278,6 +283,7 @@ def session_json(
|
|||||||
"minutes": ts.minutes,
|
"minutes": ts.minutes,
|
||||||
"subtopics_done": ts.subtopics_done,
|
"subtopics_done": ts.subtopics_done,
|
||||||
"subtopics_total": ts.subtopics_total,
|
"subtopics_total": ts.subtopics_total,
|
||||||
|
"subtopics_covered": list(ts.subtopics_covered),
|
||||||
"notes": ts.notes,
|
"notes": ts.notes,
|
||||||
}
|
}
|
||||||
for ts in session["topics"]
|
for ts in session["topics"]
|
||||||
@@ -28,3 +28,21 @@ def backstage(*args: object, **kwargs: Any) -> None:
|
|||||||
"""``print`` that renders only backstage (JupyterLab, nbconvert)."""
|
"""``print`` that renders only backstage (JupyterLab, nbconvert)."""
|
||||||
if not on_stage():
|
if not on_stage():
|
||||||
print(*args, **kwargs)
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def backstage_md(text: str) -> None:
|
||||||
|
"""Markdown that renders only backstage (JupyterLab, nbconvert).
|
||||||
|
|
||||||
|
Emitted as a ``text/markdown`` display, so nbconvert's markdown export
|
||||||
|
carries it verbatim — a stream ``print`` would be indented as a code
|
||||||
|
block. Falls back to ``print`` when IPython isn't importable (plain
|
||||||
|
pytest), where ``display`` itself already degrades to ``print``.
|
||||||
|
"""
|
||||||
|
if on_stage():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from IPython.display import Markdown, display
|
||||||
|
except ImportError:
|
||||||
|
print(text)
|
||||||
|
return
|
||||||
|
display(Markdown(text)) # type: ignore[no-untyped-call]
|
||||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "discoverylib"
|
name = "discoverylib"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
description = "CX Exploration & Discovery Workshop — live facilitation aid (Mercury Notebook Pattern)"
|
description = "CX Exploration & Discovery Workshop — live facilitation aid (Mercury Notebook Pattern)"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
# The notebook is the deliverable (served with Mercury, exported via
|
# The notebook is the deliverable (served with Mercury, exported via
|
||||||
123
assessments/CX_Discovery_Workshop/scripts/export_report.py
Normal file
123
assessments/CX_Discovery_Workshop/scripts/export_report.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"""Export the discovery notebook as an LLM-readable report source.
|
||||||
|
|
||||||
|
Executes the notebook ONCE (widget defaults — or whatever you've captured
|
||||||
|
and saved in the notebook), then converts the executed copy twice:
|
||||||
|
|
||||||
|
exports/cx_discovery.html — human-reviewable, full presentation
|
||||||
|
exports/cx_discovery.md — leanest LLM input: presentation-tagged cells
|
||||||
|
(setup, widgets, board) are stripped, a
|
||||||
|
framing preamble is prepended, and the data
|
||||||
|
appendix ends the file with one fenced JSON
|
||||||
|
block — the machine source of truth.
|
||||||
|
|
||||||
|
Engagement identity (client, date, facilitator) is read from the notebook's
|
||||||
|
``engagement-data`` cell and stamped into the preamble.
|
||||||
|
|
||||||
|
Run from the study root: python scripts/export_report.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
NOTEBOOK = ROOT / "notebooks" / "cx_discovery.ipynb"
|
||||||
|
EXPORTS = ROOT / "exports"
|
||||||
|
|
||||||
|
# Cells tagged with any of these never reach the .md export — they are
|
||||||
|
# stage presentation (source and widget-repr noise), not session record.
|
||||||
|
STRIP_TAGS_FROM_MD = '{"presentation"}'
|
||||||
|
|
||||||
|
|
||||||
|
def engagement_data() -> dict[str, Any]:
|
||||||
|
"""Exec the engagement-data cell (same trick as tests/conftest.py)."""
|
||||||
|
import nbformat
|
||||||
|
|
||||||
|
nb = nbformat.read(NOTEBOOK, as_version=4)
|
||||||
|
cells = [c for c in nb.cells
|
||||||
|
if "engagement-data" in c.metadata.get("tags", [])]
|
||||||
|
assert len(cells) == 1, "expected exactly one engagement-data cell"
|
||||||
|
ns: dict[str, Any] = {}
|
||||||
|
exec(compile(cells[0].source, f"{NOTEBOOK.name} [engagement-data]", "exec"), ns)
|
||||||
|
return ns["ENGAGEMENT"] # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
|
||||||
|
def preamble() -> str:
|
||||||
|
eng = engagement_data()
|
||||||
|
who = " · ".join(str(eng[k]).strip()
|
||||||
|
for k in ("client", "workshop_date", "facilitator")
|
||||||
|
if str(eng.get(k, "")).strip())
|
||||||
|
attendees = ", ".join(str(a) for a in eng.get("attendees", ()) or ())
|
||||||
|
if who:
|
||||||
|
line = who + (f" — attendees: {attendees}" if attendees else "")
|
||||||
|
else:
|
||||||
|
line = "master copy — placeholders; no engagement captured."
|
||||||
|
return "\n".join([
|
||||||
|
"<!-- Export preamble — generated by scripts/export_report.py -->",
|
||||||
|
"**What this is** — the exported record of a CX Exploration & Discovery",
|
||||||
|
"workshop session, produced from the Mercury-served notebook that ran the",
|
||||||
|
"session. It is LLM input for drafting the survey write-up or seeding a",
|
||||||
|
"business case.",
|
||||||
|
"",
|
||||||
|
f"**Engagement** — {line}",
|
||||||
|
"",
|
||||||
|
"**How to read it** — first the workshop content as annotated source (the",
|
||||||
|
"topic bank, then the engagement data), then the facilitation script",
|
||||||
|
"(every prompt, grouped by topic and sub-topic), the verification gate,",
|
||||||
|
"and finally the captured-session record: the per-topic table (status,",
|
||||||
|
"covered sub-topics, notes) and — last — **Session state (JSON)**, one",
|
||||||
|
"fenced `json` block. Where prose and JSON disagree, the JSON block is",
|
||||||
|
"the source of truth.",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] not in NOTEBOOK.name:
|
||||||
|
sys.exit(f"no notebook matches {sys.argv[1]!r}")
|
||||||
|
EXPORTS.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
executed = Path(tmp) / NOTEBOOK.name
|
||||||
|
# 1. Execute once — both formats convert the same session state.
|
||||||
|
# (Never combine --execute with TagRemovePreprocessor in one call:
|
||||||
|
# the cell could be stripped before it runs.)
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "nbconvert", "--execute",
|
||||||
|
"--to", "notebook", "--output", str(executed), str(NOTEBOOK)],
|
||||||
|
check=True, cwd=ROOT,
|
||||||
|
)
|
||||||
|
# 2. HTML — full presentation, human review artifact.
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "nbconvert", "--to", "html",
|
||||||
|
"--output-dir", str(EXPORTS), "--output", NOTEBOOK.stem,
|
||||||
|
str(executed)],
|
||||||
|
check=True, cwd=ROOT,
|
||||||
|
)
|
||||||
|
# 3. Markdown — LLM artifact: strip presentation cells.
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "nbconvert", "--to", "markdown",
|
||||||
|
"--output-dir", str(EXPORTS), "--output", NOTEBOOK.stem,
|
||||||
|
"--TagRemovePreprocessor.enabled=True",
|
||||||
|
f"--TagRemovePreprocessor.remove_cell_tags={STRIP_TAGS_FROM_MD}",
|
||||||
|
str(executed)],
|
||||||
|
check=True, cwd=ROOT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. Prepend the framing preamble to the markdown export.
|
||||||
|
md = EXPORTS / f"{NOTEBOOK.stem}.md"
|
||||||
|
md.write_text(preamble() + md.read_text(encoding="utf-8"), encoding="utf-8")
|
||||||
|
|
||||||
|
for p in sorted(EXPORTS.iterdir()):
|
||||||
|
if p.suffix in (".html", ".md"):
|
||||||
|
print(f"wrote {p.relative_to(ROOT)} ({p.stat().st_size / 1024:,.0f} KB)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Test plumbing: import path + the topic bank served from the notebook.
|
"""Test plumbing: import path + notebook content served from tagged cells.
|
||||||
|
|
||||||
The bank is CONTENT and lives in the notebook — the cell tagged
|
Content and client data live in the notebook, never in ``.py`` — the cells
|
||||||
``topic-bank`` in ``notebooks/cx_discovery.ipynb``; ``.py`` files hold code
|
tagged ``topic-bank`` and ``engagement-data`` in
|
||||||
only. The fixtures below read that cell with nbformat and exec it, so
|
``notebooks/cx_discovery.ipynb``. The fixtures below read those cells with
|
||||||
pytest pins the exact content the deliverable ships (no kernel needed —
|
nbformat and exec them, so pytest pins the exact content the deliverable
|
||||||
the cell is self-contained by contract).
|
ships (no kernel needed — tagged cells are self-contained by contract).
|
||||||
|
|
||||||
The sys.path insert makes discoverylib importable even without the study
|
The sys.path insert makes discoverylib importable even without the study
|
||||||
venv active (the normal setup is ``pip install -e ".[dev]"`` into the
|
venv active (the normal setup is ``pip install -e ".[dev]"`` into the
|
||||||
@@ -25,22 +25,27 @@ sys.path.insert(0, str(STUDY_ROOT))
|
|||||||
NOTEBOOK = STUDY_ROOT / "notebooks" / "cx_discovery.ipynb"
|
NOTEBOOK = STUDY_ROOT / "notebooks" / "cx_discovery.ipynb"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
def tagged_cell_ns(tag: str) -> dict[str, Any]:
|
||||||
def topic_bank() -> dict[str, Any]:
|
"""Exec the single cell carrying ``tag`` and return its namespace."""
|
||||||
"""The executed namespace of the notebook's topic-bank cell."""
|
|
||||||
import nbformat
|
import nbformat
|
||||||
|
|
||||||
nb = nbformat.read(NOTEBOOK, as_version=4)
|
nb = nbformat.read(NOTEBOOK, as_version=4)
|
||||||
cells = [c for c in nb.cells if "topic-bank" in c.metadata.get("tags", [])]
|
cells = [c for c in nb.cells if tag in c.metadata.get("tags", [])]
|
||||||
assert len(cells) == 1, (
|
assert len(cells) == 1, (
|
||||||
f"expected exactly one cell tagged 'topic-bank' in {NOTEBOOK.name}, "
|
f"expected exactly one cell tagged {tag!r} in {NOTEBOOK.name}, "
|
||||||
f"found {len(cells)}"
|
f"found {len(cells)}"
|
||||||
)
|
)
|
||||||
ns: dict[str, Any] = {}
|
ns: dict[str, Any] = {}
|
||||||
exec(compile(cells[0].source, f"{NOTEBOOK.name} [topic-bank]", "exec"), ns)
|
exec(compile(cells[0].source, f"{NOTEBOOK.name} [{tag}]", "exec"), ns)
|
||||||
return ns
|
return ns
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def topic_bank() -> dict[str, Any]:
|
||||||
|
"""The executed namespace of the notebook's topic-bank cell."""
|
||||||
|
return tagged_cell_ns("topic-bank")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def topics(topic_bank: dict[str, Any]) -> tuple[Any, ...]:
|
def topics(topic_bank: dict[str, Any]) -> tuple[Any, ...]:
|
||||||
"""The TOPICS tuple as the deliverable defines it."""
|
"""The TOPICS tuple as the deliverable defines it."""
|
||||||
@@ -50,3 +55,9 @@ def topics(topic_bank: dict[str, Any]) -> tuple[Any, ...]:
|
|||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def topic_by_key(topics: tuple[Any, ...]) -> dict[str, Any]:
|
def topic_by_key(topics: tuple[Any, ...]) -> dict[str, Any]:
|
||||||
return {t.key: t for t in topics}
|
return {t.key: t for t in topics}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def engagement() -> dict[str, Any]:
|
||||||
|
"""The ENGAGEMENT dict as the deliverable's engagement-data cell ships it."""
|
||||||
|
return tagged_cell_ns("engagement-data")["ENGAGEMENT"] # type: ignore[no-any-return]
|
||||||
44
assessments/CX_Discovery_Workshop/tests/test_engagement.py
Normal file
44
assessments/CX_Discovery_Workshop/tests/test_engagement.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""Engagement-data cell — shape pins for the client-facts cell.
|
||||||
|
|
||||||
|
The cell (tagged ``engagement-data``) carries client-specific session facts
|
||||||
|
and lives in the notebook, never in ``.py``. The MASTER ships placeholders;
|
||||||
|
an engagement copy ships real values — both must stay green, so these pins
|
||||||
|
check SHAPE only and never assert emptiness (or any particular value).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
from discoverylib import build_session, session_json
|
||||||
|
|
||||||
|
NOTEBOOK = (
|
||||||
|
pathlib.Path(__file__).resolve().parent.parent / "notebooks" / "cx_discovery.ipynb"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_keys_and_types(engagement):
|
||||||
|
assert set(engagement) == {"client", "workshop_date", "facilitator", "attendees"}
|
||||||
|
for key in ("client", "workshop_date", "facilitator"):
|
||||||
|
assert isinstance(engagement[key], str)
|
||||||
|
assert all(isinstance(a, str) for a in engagement["attendees"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_flows_into_export_meta(topics, engagement):
|
||||||
|
payload = session_json(build_session(topics, {}), meta=engagement)
|
||||||
|
assert payload["meta"]["client"] == engagement["client"]
|
||||||
|
assert payload["assessment"] == "CX_Discovery_Workshop"
|
||||||
|
json.dumps(payload) # the attendees tuple serializes as a JSON list
|
||||||
|
|
||||||
|
|
||||||
|
def test_cell_sits_above_the_widgets():
|
||||||
|
# Position rule: engagement data must never re-run on a sidebar change,
|
||||||
|
# so its cell precedes the widget-defining cell (Mercury re-runs only
|
||||||
|
# cells BELOW a changed widget's cell).
|
||||||
|
import nbformat
|
||||||
|
|
||||||
|
nb = nbformat.read(NOTEBOOK, as_version=4)
|
||||||
|
eng = next(i for i, c in enumerate(nb.cells)
|
||||||
|
if "engagement-data" in c.metadata.get("tags", []))
|
||||||
|
widgets = next(i for i, c in enumerate(nb.cells)
|
||||||
|
if c.cell_type == "code" and "mr.Select(" in c.source)
|
||||||
|
assert eng < widgets, "engagement-data cell must sit above the widget cell"
|
||||||
@@ -87,10 +87,14 @@ def test_build_session_shape_and_subtopic_done_count(topics, topic_by_key):
|
|||||||
assert ch.status == IN_PROGRESS
|
assert ch.status == IN_PROGRESS
|
||||||
assert ch.subtopics_done == 2
|
assert ch.subtopics_done == 2
|
||||||
assert ch.subtopics_total == len(topic_by_key["channels"].subtopics)
|
assert ch.subtopics_total == len(topic_by_key["channels"].subtopics)
|
||||||
|
# WHICH threads were covered, in canonical bank order — the export's
|
||||||
|
# whole purpose is naming them, not just counting them.
|
||||||
|
assert ch.subtopics_covered == ("voice_metrics", "outbound")
|
||||||
assert ch.notes == "6 channels; voice ~70%" # trimmed
|
assert ch.notes == "6 channels; voice ~70%" # trimmed
|
||||||
|
|
||||||
bg = next(ts for ts in s["topics"] if ts.key == "background")
|
bg = next(ts for ts in s["topics"] if ts.key == "background")
|
||||||
assert bg.status == COMPLETE and bg.subtopics_done == 0
|
assert bg.status == COMPLETE and bg.subtopics_done == 0
|
||||||
|
assert bg.subtopics_covered == ()
|
||||||
|
|
||||||
|
|
||||||
def test_session_json_is_plain_and_complete(topics):
|
def test_session_json_is_plain_and_complete(topics):
|
||||||
@@ -100,7 +104,7 @@ def test_session_json_is_plain_and_complete(topics):
|
|||||||
payload = session_json(build_session(topics, status, notes, done),
|
payload = session_json(build_session(topics, status, notes, done),
|
||||||
meta={"client": "Acme", "date": "2026-07-19"})
|
meta={"client": "Acme", "date": "2026-07-19"})
|
||||||
|
|
||||||
assert payload["study"] == "202607_CX_Discovery_Workshop"
|
assert payload["assessment"] == "CX_Discovery_Workshop"
|
||||||
assert payload["meta"]["client"] == "Acme"
|
assert payload["meta"]["client"] == "Acme"
|
||||||
assert payload["progress"]["completed"] == 1
|
assert payload["progress"]["completed"] == 1
|
||||||
assert payload["progress"]["fraction_complete"] == round(1 / 8, 4)
|
assert payload["progress"]["fraction_complete"] == round(1 / 8, 4)
|
||||||
@@ -111,6 +115,7 @@ def test_session_json_is_plain_and_complete(topics):
|
|||||||
assert bg["notes"] == "3 LOBs; PCI in scope"
|
assert bg["notes"] == "3 LOBs; PCI in scope"
|
||||||
ch = next(t for t in payload["topics"] if t["key"] == "channels")
|
ch = next(t for t in payload["topics"] if t["key"] == "channels")
|
||||||
assert ch["subtopics_done"] == 1
|
assert ch["subtopics_done"] == 1
|
||||||
|
assert ch["subtopics_covered"] == ["voice_metrics"]
|
||||||
|
|
||||||
# JSON-serializable (no dataclasses / sets leaked through)
|
# JSON-serializable (no dataclasses / sets leaked through)
|
||||||
import json
|
import json
|
||||||
28
assessments/CX_Discovery_Workshop/tests/test_staging.py
Normal file
28
assessments/CX_Discovery_Workshop/tests/test_staging.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Stage/backstage detection — Mercury kernels carry MERCURY_CONFIG_DIR."""
|
||||||
|
|
||||||
|
from discoverylib import staging
|
||||||
|
|
||||||
|
|
||||||
|
def test_backstage_prints_only_off_stage(monkeypatch, capsys):
|
||||||
|
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
|
||||||
|
assert not staging.on_stage()
|
||||||
|
staging.backstage("visible")
|
||||||
|
assert capsys.readouterr().out == "visible\n"
|
||||||
|
|
||||||
|
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
|
||||||
|
assert staging.on_stage()
|
||||||
|
staging.backstage("hidden")
|
||||||
|
assert capsys.readouterr().out == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_backstage_md_renders_only_off_stage(monkeypatch, capsys):
|
||||||
|
# Off stage it must emit SOMETHING (rich markdown under a kernel;
|
||||||
|
# IPython's display degrades to print under plain pytest) …
|
||||||
|
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
|
||||||
|
staging.backstage_md("**visible**")
|
||||||
|
assert capsys.readouterr().out != ""
|
||||||
|
|
||||||
|
# … and on stage, nothing at all.
|
||||||
|
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
|
||||||
|
staging.backstage_md("**hidden**")
|
||||||
|
assert capsys.readouterr().out == ""
|
||||||
453
cxxm.md
453
cxxm.md
@@ -1,453 +0,0 @@
|
|||||||
Contact Center Exploration & Discovery Workshops
|
|
||||||
The Contact Center Discovery and Exploration workshops allow us to gain a clear perspective of your contact center operations. The discovery workshop is focused on developing an understanding of the current state architecture and IT support.
|
|
||||||
|
|
||||||
I’ve attached a contact center survey to collect information and context in advance of the workshop and is also useful to develop business cases.
|
|
||||||
|
|
||||||
# CX Exploration
|
|
||||||
|
|
||||||
## Background
|
|
||||||
How many distinct lines of business are supported in the contact centre?
|
|
||||||
(eg, commercial, residential, retail, wholesale, etc)
|
|
||||||
Are you using contact center for internal Uses such as help desk, finance, HR, etc.?
|
|
||||||
|
|
||||||
What channels do you support?
|
|
||||||
Apps
|
|
||||||
Voice
|
|
||||||
Video
|
|
||||||
Chat
|
|
||||||
SMS
|
|
||||||
Email
|
|
||||||
Social Media
|
|
||||||
Digital Assistants
|
|
||||||
|
|
||||||
Payment card/PCI?
|
|
||||||
|
|
||||||
What are the availability SLAs or targets for your technology platform?
|
|
||||||
|
|
||||||
Mini org chart
|
|
||||||
|
|
||||||
Reporting structure
|
|
||||||
- Executive leader
|
|
||||||
|
|
||||||
What is the structure of decision making in their organization? Centralized or decentralized?
|
|
||||||
- Who are the key decision makers for implementing a plan for changes to the contact centre?
|
|
||||||
|
|
||||||
How important is CX & EX to your organization's strategy?
|
|
||||||
- Is there an executive that is accountable for CX? CXO, CDO?
|
|
||||||
|
|
||||||
Of your leading offers, what is the customer's top priority?
|
|
||||||
|
|
||||||
Competitive pressures?
|
|
||||||
|
|
||||||
Are their pending or recent acquisitions or spinoffs that require changes to their IT infrastructure and services?
|
|
||||||
|
|
||||||
What do they view as the problems in their contact centre today and what are the primary causes?
|
|
||||||
|
|
||||||
CX / Customer Success Leadership:
|
|
||||||
Are your contact centers managed by the same person?
|
|
||||||
- Managers:
|
|
||||||
- Team Leads:
|
|
||||||
- Agents:
|
|
||||||
|
|
||||||
Who carries the cost of contact centre agents and supervisors?
|
|
||||||
|
|
||||||
What revenue is generated by the contact center?
|
|
||||||
|
|
||||||
Do you have KPI targets for CX?:
|
|
||||||
CSAT:
|
|
||||||
NPS:
|
|
||||||
CES:
|
|
||||||
How are you doing?
|
|
||||||
|
|
||||||
Is there a VOC program in place?
|
|
||||||
• ROI/business value known?
|
|
||||||
• How do customers rate the experience they receive from your organization? 0-5
|
|
||||||
|
|
||||||
|
|
||||||
What are the key features of their corporate culture?
|
|
||||||
|
|
||||||
Is their a Voice of the Agent, or Employee satisfaction survey in place?
|
|
||||||
- How are they doing?
|
|
||||||
|
|
||||||
|
|
||||||
Automation strategy?
|
|
||||||
|
|
||||||
Any inflight projects?
|
|
||||||
- Any CX or EX improvement initiatives?
|
|
||||||
|
|
||||||
Decision
|
|
||||||
|
|
||||||
Channel management strategy?
|
|
||||||
|
|
||||||
Are your customer experiences personalized?
|
|
||||||
|
|
||||||
Do you have a clear set of design guidelines for CX?
|
|
||||||
Personas
|
|
||||||
Journey Mapping
|
|
||||||
Tools:
|
|
||||||
|
|
||||||
How are processes managed?
|
|
||||||
|
|
||||||
## CX Strategy
|
|
||||||
How is the value of Customer Experience defined within your organization?
|
|
||||||
- CX is a key BPI, recognized & measured as a financial value
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Strategic Value
|
|
||||||
What revenue is generated by the contact center?
|
|
||||||
|
|
||||||
What are the key capabilities / services that are delivered by the contact centre?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Proposition
|
|
||||||
How are you using CX innovation to create market disruption?
|
|
||||||
|
|
||||||
How are competitors using CX to create competitive differentiation?
|
|
||||||
|
|
||||||
|
|
||||||
Organization Structure & Operating Model
|
|
||||||
Is there an executive that is accountable for CX? CXO, CDO?
|
|
||||||
|
|
||||||
Is there a CX team?
|
|
||||||
Do the CX insights team regularly educate the business
|
|
||||||
|
|
||||||
How are decisions made in the organization, is it centralized or decentralized?
|
|
||||||
- Who are the key decision makers for investment and changes to the contact centre'?
|
|
||||||
- Channel management strategy?
|
|
||||||
|
|
||||||
|
|
||||||
Who carries the cost of contact centre agents and supervisors?
|
|
||||||
|
|
||||||
Are they managed by the same person?
|
|
||||||
|
|
||||||
|
|
||||||
What are the teams?
|
|
||||||
|
|
||||||
How are processes managed?
|
|
||||||
|
|
||||||
Describe your automation strategy. Are there any desires or goals related to the contact centre?
|
|
||||||
- Who is your leader for Data & AI?
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Are there any inflight projects that would impact the contact centre?
|
|
||||||
|
|
||||||
Are there recent or pending acquisitions or spinoffs?
|
|
||||||
|
|
||||||
Insight
|
|
||||||
How do you use analytics & data to generate a consolidated view of your customer experience?
|
|
||||||
|
|
||||||
|
|
||||||
Is there a VOC program in place?
|
|
||||||
• ROI/business value known?
|
|
||||||
• How do customers rate the experience they receive from your organization? 0-5
|
|
||||||
|
|
||||||
|
|
||||||
Approach
|
|
||||||
Continuous Improvement
|
|
||||||
How is customer insight used to drive CX improvement, loyalty and profitability?
|
|
||||||
How do you anticipate needs?
|
|
||||||
|
|
||||||
Do you have a clear set of CX design guidelines for CX?
|
|
||||||
Personas
|
|
||||||
Journey Mapping
|
|
||||||
Tools:
|
|
||||||
|
|
||||||
Do you have KPI targets for CX?:
|
|
||||||
CSAT:
|
|
||||||
NPS:
|
|
||||||
CES:
|
|
||||||
How are you doing?
|
|
||||||
|
|
||||||
|
|
||||||
Employee Engagement
|
|
||||||
Please describe you about your employee / Agent engagement strategy
|
|
||||||
|
|
||||||
How engaged are your people in delivering the customer experience?
|
|
||||||
|
|
||||||
Do you have a VoA/VoE program in place? How are you doing?
|
|
||||||
|
|
||||||
## Channels
|
|
||||||
|
|
||||||
May 6, 2025
|
|
||||||
2:52 PM
|
|
||||||
|
|
||||||
Inbound
|
|
||||||
|
|
||||||
Hours of operation
|
|
||||||
|
|
||||||
Who contacts the contact center? (Demographics, their situation)
|
|
||||||
|
|
||||||
Are certain callers or groups prioritized?
|
|
||||||
|
|
||||||
Languages?
|
|
||||||
English
|
|
||||||
Canadian French
|
|
||||||
Spanish
|
|
||||||
|
|
||||||
Video
|
|
||||||
Average number of active agents:
|
|
||||||
Voice
|
|
||||||
Average number of active agents:
|
|
||||||
Toll Free numbers & DIDs:
|
|
||||||
Approximate quantities or a list
|
|
||||||
Hours of operation? Any 24x7?
|
|
||||||
Average Wait Time:
|
|
||||||
Average Abandon rate:
|
|
||||||
Average Handle Time:
|
|
||||||
Average Call Hold Time:
|
|
||||||
Average After work call time:
|
|
||||||
Time to authenticate a caller:
|
|
||||||
% of calls transferred?
|
|
||||||
Internally?
|
|
||||||
Externally?
|
|
||||||
3rd parties handling certain types of calls, or being conferenced in?
|
|
||||||
Courtesy Callback/Virtual Hold
|
|
||||||
Post Call Survey
|
|
||||||
First Call Resolution Rate?
|
|
||||||
Average revenue per call (sales)?
|
|
||||||
Email, range of logged in users:
|
|
||||||
○ Response time:
|
|
||||||
○ Email server
|
|
||||||
Chat, range of logged in users:
|
|
||||||
Web Chat Internal
|
|
||||||
Web Chat External
|
|
||||||
App Integration / embedded
|
|
||||||
SMS
|
|
||||||
Facebook Messenger
|
|
||||||
WhatsApp
|
|
||||||
Telegram
|
|
||||||
iMessage
|
|
||||||
Web Site Forms
|
|
||||||
Mobile Apps
|
|
||||||
|
|
||||||
Top 3-5 Inbound contact reasons and approximate % of calls:
|
|
||||||
1.
|
|
||||||
|
|
||||||
Cost Per call?
|
|
||||||
Average Handle Time?
|
|
||||||
|
|
||||||
Busiest days:
|
|
||||||
|
|
||||||
Least busy days:
|
|
||||||
|
|
||||||
Seasonal variances?
|
|
||||||
|
|
||||||
What are your most difficult, commonly occurring calls?
|
|
||||||
|
|
||||||
What is the easiest commonly occurring call?
|
|
||||||
|
|
||||||
|
|
||||||
Outbound
|
|
||||||
|
|
||||||
Hours of operation
|
|
||||||
|
|
||||||
Voice, range of logged in users:
|
|
||||||
Preview, volume
|
|
||||||
Predictive dialer
|
|
||||||
Email, range of logged in users:
|
|
||||||
SMS, range of logged in users:
|
|
||||||
Recorded announcement, ports:
|
|
||||||
Self service applications
|
|
||||||
Transfer to agent
|
|
||||||
Live agent connect
|
|
||||||
Campaign management
|
|
||||||
DNC management
|
|
||||||
|
|
||||||
Top 3-5 outbound contact reasons and approximate % of calls:
|
|
||||||
1.
|
|
||||||
|
|
||||||
|
|
||||||
After call work?
|
|
||||||
|
|
||||||
|
|
||||||
## Agent & Supervisor Environment
|
|
||||||
Location types:
|
|
||||||
- WFH:
|
|
||||||
- Offices:
|
|
||||||
|
|
||||||
Hard phone
|
|
||||||
Soft phone
|
|
||||||
CODEC?
|
|
||||||
Headset, wireless?:
|
|
||||||
Agent greeting or pre-recorded messages
|
|
||||||
Whisper announcement
|
|
||||||
PC
|
|
||||||
Desktop
|
|
||||||
Laptop
|
|
||||||
VDI
|
|
||||||
Agent & Supervisor desktop (omnichannel?):
|
|
||||||
Custom gadgets
|
|
||||||
Screen pops
|
|
||||||
Workflows
|
|
||||||
Standard browser:
|
|
||||||
SSO:
|
|
||||||
Applications used to handle calls:
|
|
||||||
Knowledge Management:
|
|
||||||
|
|
||||||
## Routing & Automation
|
|
||||||
Self Service
|
|
||||||
IVR Persona?
|
|
||||||
Branding?
|
|
||||||
Style guides?
|
|
||||||
Voice actors?
|
|
||||||
DTMF
|
|
||||||
Speech
|
|
||||||
Speech recognition
|
|
||||||
Text To Speech
|
|
||||||
Natural Language Understanding
|
|
||||||
Voice Biometrics
|
|
||||||
Self service applications:
|
|
||||||
ID & Validate
|
|
||||||
Deflection
|
|
||||||
Situational Offer (Outage/ Time of Day / Scheduled)
|
|
||||||
API integration:
|
|
||||||
CRM
|
|
||||||
Intent Capture
|
|
||||||
Intent prediction
|
|
||||||
Offer push based on prediction/account attribute
|
|
||||||
Virtual Agents
|
|
||||||
Processes
|
|
||||||
Agent Assist
|
|
||||||
RPA
|
|
||||||
Current challenges or desired capabilities
|
|
||||||
Process management
|
|
||||||
Desire to automate
|
|
||||||
DevOps team?
|
|
||||||
|
|
||||||
Digital Assistant Apps
|
|
||||||
Alexa
|
|
||||||
Google Assistant
|
|
||||||
Siri
|
|
||||||
|
|
||||||
|
|
||||||
## Routing & Automation
|
|
||||||
Self Service
|
|
||||||
IVR Persona?
|
|
||||||
Branding?
|
|
||||||
Style guides?
|
|
||||||
Voice actors?
|
|
||||||
DTMF
|
|
||||||
Speech
|
|
||||||
Speech recognition
|
|
||||||
Text To Speech
|
|
||||||
Natural Language Understanding
|
|
||||||
Voice Biometrics
|
|
||||||
Self service applications:
|
|
||||||
ID & Validate
|
|
||||||
Deflection
|
|
||||||
Situational Offer (Outage/ Time of Day / Scheduled)
|
|
||||||
API integration:
|
|
||||||
CRM
|
|
||||||
Intent Capture
|
|
||||||
Intent prediction
|
|
||||||
Offer push based on prediction/account attribute
|
|
||||||
Agent Assist
|
|
||||||
RPA
|
|
||||||
Current challenges or desired capabilities
|
|
||||||
Process management
|
|
||||||
Desire to automate
|
|
||||||
DevOps team?
|
|
||||||
|
|
||||||
Digital Assistant Apps
|
|
||||||
Alexa
|
|
||||||
Google Assistant
|
|
||||||
Siri
|
|
||||||
|
|
||||||
|
|
||||||
Workforce Engagement
|
|
||||||
|
|
||||||
May 6, 2025
|
|
||||||
2:50 PM
|
|
||||||
|
|
||||||
Call Recording/QM
|
|
||||||
• How is call recording used?
|
|
||||||
Compliance
|
|
||||||
Screen capture
|
|
||||||
Voice transcription
|
|
||||||
Real time
|
|
||||||
Desktop analytics
|
|
||||||
Retention period
|
|
||||||
Quality Management
|
|
||||||
Do you have a quality team?
|
|
||||||
Who do they report to?
|
|
||||||
How many people work for the team?
|
|
||||||
Scorecards
|
|
||||||
Score method and metrics?
|
|
||||||
How many assessments are conducted?
|
|
||||||
Coaching
|
|
||||||
Live Monitor
|
|
||||||
• Locations with CR
|
|
||||||
• Number of Named Agents:
|
|
||||||
|
|
||||||
|
|
||||||
Workforce Engagement
|
|
||||||
• What is your recruitment process?
|
|
||||||
• What qualifications are required for the call center?
|
|
||||||
• What is the average staff tenure?
|
|
||||||
• What is the attrition rate?
|
|
||||||
• What proportion of the staff leave for other positions in the company per annum?
|
|
||||||
○ Are staff hired for the CC from other parts of the organization?
|
|
||||||
• Do you measure agent & supervisor experience?
|
|
||||||
○ How are they doing?
|
|
||||||
○ What type of recognition program do you have in place?
|
|
||||||
• Agent adherence measure?
|
|
||||||
○ KPIs
|
|
||||||
○ Gamification
|
|
||||||
○ How long is a shift?
|
|
||||||
○ What breaks are allocated
|
|
||||||
○ What annualized utilization % do you calculate?
|
|
||||||
• Forecasting & Scheduling
|
|
||||||
○ Peak call volume days, time of month, year?
|
|
||||||
○ Validate historical data
|
|
||||||
○ How are calculations performed (algorithms used)
|
|
||||||
○ WFM Interval (15/30 minutes)
|
|
||||||
• Intraday/ Realtime adherence
|
|
||||||
○ What happens when you are out of compliance, surprises happen?
|
|
||||||
• Payroll Integration
|
|
||||||
○ Payroll platform
|
|
||||||
• Satisfaction with current tool(s)?
|
|
||||||
○ Multiskilled agents
|
|
||||||
○ FTE Calculations
|
|
||||||
• Agent self-service?
|
|
||||||
○ How are absences reported?
|
|
||||||
○ Shift bids / swaps
|
|
||||||
○ Performance metrics
|
|
||||||
• Number of Named Agents:
|
|
||||||
• Integration to 3rd party
|
|
||||||
○ outsourcer / overflow:
|
|
||||||
○ Payroll: ADP / Workday
|
|
||||||
|
|
||||||
|
|
||||||
Training
|
|
||||||
• Onboarding process
|
|
||||||
○ How long is it?
|
|
||||||
○ Training format?
|
|
||||||
○ Assessment?
|
|
||||||
eLearning
|
|
||||||
QM Integration
|
|
||||||
|
|
||||||
## Reporting & Insights
|
|
||||||
Reporting
|
|
||||||
• Real time
|
|
||||||
• Historical
|
|
||||||
• What are the key metrics that you report on?
|
|
||||||
Data source integration
|
|
||||||
Dashboards
|
|
||||||
Wallboards
|
|
||||||
Agent & Supervisor status
|
|
||||||
|
|
||||||
Analytics & Insights
|
|
||||||
Do you have an integrated view of customer details and contact history?
|
|
||||||
CRM:
|
|
||||||
Does your contact centre collect and use customer insight?
|
|
||||||
Predictive engagement:
|
|
||||||
What sort of data analysis do you perform?
|
|
||||||
Do you have an analytics or business insights team?
|
|
||||||
How many people?
|
|
||||||
BI Platform:
|
|
||||||
What executive level reporting do you perform?
|
|
||||||
Marketing Team interlock
|
|
||||||
|
|
||||||
236
docs/Assessment_Pattern_V1-00.md
Normal file
236
docs/Assessment_Pattern_V1-00.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# 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.
|
||||||
@@ -690,3 +690,27 @@ All arithmetic uses Python `Decimal` to avoid floating-point drift. Values are s
|
|||||||
---
|
---
|
||||||
|
|
||||||
*TEI Tool — Athena*
|
*TEI Tool — Athena*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Palladium conventions (client-side)
|
||||||
|
|
||||||
|
Relocated from the root README (2026-07); these are Palladium's bridges where the
|
||||||
|
Forrester methodology and the Athena TEI API differ.
|
||||||
|
|
||||||
|
### Methodology bridges (Palladium ↔ Athena)
|
||||||
|
|
||||||
|
| Topic | Athena behaviour | Palladium convention |
|
||||||
|
|---|---|---|
|
||||||
|
| **Cost risk adjustment** | Costs are never risk-adjusted server-side | Cost values are pushed pre-multiplied by `(1 + risk_adj)`; field-level adjustment stays 0 |
|
||||||
|
| **Year-0 "Initial" costs** | No year-0 concept; non-annual values are folded into Year 1 | Each cost gets a companion non-annual `<key>_initial` field. `TEIClient` folds them back into an `initial` key on read. Athena discounts these as Year 1 (Forrester doesn't discount Year 0) — expect ≈0.15% drift on cost PV |
|
||||||
|
|
||||||
|
### Scenario analysis (`core.calculations.SCENARIOS`)
|
||||||
|
|
||||||
|
Three scenarios model uncertainty in adoption and realization:
|
||||||
|
|
||||||
|
| Scenario | Adoption | Risk delta | Effect |
|
||||||
|
|----------|----------|------------|--------|
|
||||||
|
| Conservative | 80% | +10pp on benefits | Lower benefits, higher modelled cost |
|
||||||
|
| Moderate | 100% | 0 | Base case (= published study) |
|
||||||
|
| Aggressive | 115% | –5pp on benefits | Higher benefits, lower padding on cost |
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
# Mercury Notebook Deliverable Pattern v1.0.0
|
# Mercury Notebook Deliverable Pattern v1.1.0
|
||||||
|
|
||||||
Standardizes how Palladium studies ship business-case deliverables: a Mercury-served
|
Standardizes how Palladium masters — **Studies and Assessments** — ship notebook
|
||||||
Jupyter notebook **is** the artifact — math in a self-contained study package,
|
deliverables: a Mercury-served Jupyter notebook **is** the artifact — logic in a
|
||||||
|
self-contained study package, content and client data in tagged notebook cells,
|
||||||
presentation in reactive notebooks, verification gates pinning every number, and
|
presentation in reactive notebooks, verification gates pinning every number, and
|
||||||
LLM-readable exports. All new studies follow this pattern; the Streamlit app path is
|
LLM-readable exports. All masters follow this pattern; the Streamlit app path is
|
||||||
retired by it.
|
retired by it.
|
||||||
|
|
||||||
## 🐾 Red Panda Approval™
|
## 🐾 Red Panda Approval™
|
||||||
|
|
||||||
This pattern follows Red Panda Approval standards.
|
This pattern follows Red Panda Approval standards (see `CLAUDE.md` for the rubric).
|
||||||
|
|
||||||
**Audience note:** this document is written to be loaded whole as context by an LLM
|
**Audience note:** this document is written to be loaded whole as context by an LLM
|
||||||
agent building or modifying a study. Rules are imperative (MUST/SHOULD/NEVER), each
|
agent building or modifying a master. Rules are imperative (MUST/SHOULD/NEVER), each
|
||||||
with a one-line *why*. Long code lives in the runnable template
|
with a one-line *why*. Long code lives in the runnable template
|
||||||
[`template/MercuryNotebook/`](../template/MercuryNotebook/) — copy it to start a study;
|
[`template/MercuryNotebook/`](../template/MercuryNotebook/) — copy it to start a study;
|
||||||
snippets here are excerpts from it. The full-scale reference implementation is the CTM
|
snippets here are excerpts from it (note: the template still encodes the py-engine
|
||||||
Genesys study, [`studies/202607_CTM_GenesysCX/`](../studies/202607_CTM_GenesysCX/).
|
model only). The reference implementation of the **notebook-first content model** is
|
||||||
|
[`assessments/CX_Discovery_Workshop/`](../assessments/CX_Discovery_Workshop/); the
|
||||||
|
largest worked multi-notebook example remains the CTM Genesys study,
|
||||||
|
[`studies/202607_CTM_GenesysCX/`](../studies/202607_CTM_GenesysCX/).
|
||||||
|
|
||||||
|
**Docs map:** this file holds the **shared mechanics** every master obeys.
|
||||||
|
[`Assessment_Pattern_V1-00.md`](Assessment_Pattern_V1-00.md) and
|
||||||
|
[`Study_Pattern_V1-00.md`](Study_Pattern_V1-00.md) specialize it per master type —
|
||||||
|
read the one for your master type first. `CLAUDE.md` at the repo root is the
|
||||||
|
always-on contract (rubric, three-layer contract, taxonomy, risk tiers,
|
||||||
|
confidentiality) and takes precedence where documents disagree.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,9 +64,11 @@ Instead, this pattern defines:
|
|||||||
|
|
||||||
```
|
```
|
||||||
palladium/
|
palladium/
|
||||||
├── docs/ # repo-wide docs (this pattern, brand.md)
|
├── docs/ # repo-wide docs (the patterns, brand.md)
|
||||||
├── template/
|
├── template/
|
||||||
│ └── MercuryNotebook/ # copy-me starting point (runnable)
|
│ └── MercuryNotebook/ # copy-me starting point (runnable)
|
||||||
|
├── assessments/
|
||||||
|
│ └── Instrument_Name/ # reusable workshop master (UNDATED), e.g. CX_Discovery_Workshop
|
||||||
└── studies/
|
└── studies/
|
||||||
├── YYYYMM_TEI_Vendor_Product/ # vendor TEI study, e.g. 202602_TEI_Amazon_Connect
|
├── YYYYMM_TEI_Vendor_Product/ # vendor TEI study, e.g. 202602_TEI_Amazon_Connect
|
||||||
└── YYYYMM_Client_EngagementName/ # client study, e.g. 202607_CTM_GenesysCX
|
└── YYYYMM_Client_EngagementName/ # client study, e.g. 202607_CTM_GenesysCX
|
||||||
@@ -63,6 +76,10 @@ palladium/
|
|||||||
|
|
||||||
- Names MUST use **underscores, never dashes** — dashed directories can't be Python
|
- Names MUST use **underscores, never dashes** — dashed directories can't be Python
|
||||||
packages, and everything in a study is importable code.
|
packages, and everything in a study is importable code.
|
||||||
|
- **Studies are dated** (`YYYYMM_` — they reproduce a dated publication or engagement);
|
||||||
|
**Assessments are undated** (living instruments). An **engagement copy** of a master
|
||||||
|
is stamped `YYYYMM_Client_Instrument` at copy-time and lives OUTSIDE this repo — see
|
||||||
|
the copy-out checklist in the Assessment Pattern and `CLAUDE.md` § Confidentiality.
|
||||||
- Every study is self-contained with this layout (from the template):
|
- Every study is self-contained with this layout (from the template):
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -93,11 +110,11 @@ JupyterLab by analysts, executed headless by nbconvert for exports. NEVER build
|
|||||||
parallel UI (Streamlit, Dash, a second "app" rendering the same model) — two surfaces
|
parallel UI (Streamlit, Dash, a second "app" rendering the same model) — two surfaces
|
||||||
over one model always diverge, and the notebook already *is* the interactive surface.
|
over one model always diverge, and the notebook already *is* the interactive surface.
|
||||||
|
|
||||||
### 2 · Engine/presentation split
|
### 2 · Engine/presentation split — and where content lives
|
||||||
|
|
||||||
Notebooks hold **no math**. Every number a stakeholder sees is computed in the study
|
Notebooks hold **no math**. Every number a stakeholder sees is computed in the study
|
||||||
package and imported. *Why:* the package is testable and diffable; notebook cells are
|
package and imported. *Why:* the package is testable and diffable for *logic*; the
|
||||||
neither. The test suite pins the engine; the notebook only arranges its outputs.
|
test suite pins the engine; the notebook only arranges its outputs.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# notebook cell — arrange and render, never compute
|
# notebook cell — arrange and render, never compute
|
||||||
@@ -106,6 +123,15 @@ inc, net_by = case_flows(total_by_year, ben_by_year)
|
|||||||
kpi = case_kpis(inc, net_by, DISCOUNT_RATE)
|
kpi = case_kpis(inc, net_by, DISCOUNT_RATE)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The split cuts the other way for **content and client data**: notebooks hold no math,
|
||||||
|
but they DO hold the human-authored content (topic banks, survey text, facilitation
|
||||||
|
prompts) and the client facts (spend, headcount, engagement identity) — in **tagged
|
||||||
|
cells** (`topic-bank`, `engagement-data`), edited in Jupyter. Content NEVER lives in a
|
||||||
|
`.py` file. *Why:* the notebook is the document the consultant reads and edits;
|
||||||
|
content hidden in importable modules defeats the point of a notebook. Tests pin
|
||||||
|
content by reading the tagged cells with nbformat and exec'ing them — no kernel
|
||||||
|
needed. Full contract: the [Assessment Pattern](Assessment_Pattern_V1-00.md).
|
||||||
|
|
||||||
### 3 · The Mercury reactivity contract
|
### 3 · The Mercury reactivity contract
|
||||||
|
|
||||||
Mercury re-executes only the cells **below** a changed widget's cell — never the
|
Mercury re-executes only the cells **below** a changed widget's cell — never the
|
||||||
@@ -416,8 +442,15 @@ Each of these cost a debugging session or a client-facing embarrassment. Don't.
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Layers, from inner to outer — every study ships all four:
|
Layers, from inner to outer — every master ships layers 1–4; layer 0 covers the whole
|
||||||
|
library from the repo root:
|
||||||
|
|
||||||
|
0. **Repo-level structural suite** (`tests/test_notebooks.py`, run from the ROOT venv:
|
||||||
|
`make check-notebooks`) — kernel-free nbformat checks on every committed notebook:
|
||||||
|
parses, python3 kernel, no error outputs, cleanly executed top-to-bottom, and (for
|
||||||
|
notebook-first masters) the tagged-cell taxonomy. Every notebook must be classified
|
||||||
|
in `tests/nbcheck.py`; grandfathered ones skip the notebook-first tier with a
|
||||||
|
recorded reason.
|
||||||
1. **Engine pins** (`tests/`) — hand-checked acceptance numbers for every model
|
1. **Engine pins** (`tests/`) — hand-checked acceptance numbers for every model
|
||||||
function; the contracted overlay AND the verbatim record both pinned, so neither can
|
function; the contracted overlay AND the verbatim record both pinned, so neither can
|
||||||
drift. Compute by hand first, then pin (see
|
drift. Compute by hand first, then pin (see
|
||||||
|
|||||||
94
docs/Notebook_Review_Prompt_V1-00.md
Normal file
94
docs/Notebook_Review_Prompt_V1-00.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Notebook Review Prompt v1.0.0
|
||||||
|
|
||||||
|
The canonical LLM review prompt for a Palladium master notebook. Model-agnostic:
|
||||||
|
paste everything below the line into any capable LLM together with the notebook's
|
||||||
|
raw `.ipynb` JSON, or run it in-repo with `/review-notebook <path>`. It hunts the
|
||||||
|
class of problems deterministic checks can't: content oddities, cross-cell rot,
|
||||||
|
stale numbers, and stage leakage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Role
|
||||||
|
|
||||||
|
You are reviewing one **Palladium master notebook** — a Mercury-served Jupyter
|
||||||
|
deliverable a client will see on a live call. Your input is the raw `.ipynb` JSON
|
||||||
|
(cell sources, tags, outputs, in document order). Before judging anything, read the
|
||||||
|
contracts it was built against:
|
||||||
|
|
||||||
|
- `docs/Mercury_Notebook_Pattern_V1-00.md` — reactivity, gate, stage/backstage,
|
||||||
|
appendix
|
||||||
|
- `docs/Assessment_Pattern_V1-00.md` or `docs/Study_Pattern_V1-00.md` — whichever
|
||||||
|
matches the master's type (assessments/… vs studies/…)
|
||||||
|
- The master's own `README.md` and, for cross-checks, its engine package
|
||||||
|
(`discoverylib/`, `teicalc/`, …) — you MAY open engine `.py` files to verify a
|
||||||
|
claim, and should when a number or name is in doubt.
|
||||||
|
|
||||||
|
## What NOT to re-check
|
||||||
|
|
||||||
|
`tests/test_notebooks.py` already enforces these deterministically — do not spend
|
||||||
|
review effort or findings on them: the file parses as nbformat 4; kernel is python3;
|
||||||
|
no error outputs; execution counts are monotonic top-to-bottom; the unique tags
|
||||||
|
(`topic-bank`, `engagement-data`, `gate`, `data-appendix`) appear at most once;
|
||||||
|
notebook-first masters have all four, the appendix last, engagement-data above the
|
||||||
|
widgets; presentation cells emit no stream output; content cells use no magics.
|
||||||
|
|
||||||
|
## Check catalogue
|
||||||
|
|
||||||
|
Work cell by cell in document order, then once more end-to-end per theme:
|
||||||
|
|
||||||
|
1. **Content oddities** — typos and grammar in client-visible text; duplicated or
|
||||||
|
near-duplicate prompts/items; terminology drifting from the source document the
|
||||||
|
content anchors (the README names it); `key` slugs that don't match their titles;
|
||||||
|
counts/minutes in comments or prose that don't match the actual content; tone that
|
||||||
|
doesn't belong in front of a client.
|
||||||
|
*Spot it by:* reading the content cells as an editor, then recounting anything a
|
||||||
|
comment or heading claims.
|
||||||
|
2. **Broken cross-cell references** — names used before their defining cell in
|
||||||
|
document order; a widget's `.value` read in the cell that defines it (frozen at
|
||||||
|
first render); variables shadowed or redefined with a different meaning; cells
|
||||||
|
that only work because of stale kernel state (would fail on a fresh top-to-bottom
|
||||||
|
run); imports used but not imported in any earlier cell.
|
||||||
|
*Spot it by:* tracing each name in a cell back to its defining cell index.
|
||||||
|
3. **Stale numbers** — figures hardcoded in markdown or annotations that the engine
|
||||||
|
or gate could contradict; outputs inconsistent with the current source (a changed
|
||||||
|
cell whose committed output still shows the old result); gate pins that disagree
|
||||||
|
with the test-suite pins; totals in prose that don't equal the content.
|
||||||
|
*Spot it by:* comparing markdown claims ↔ committed outputs ↔ gate asserts ↔
|
||||||
|
engine constants.
|
||||||
|
4. **Tag/metadata issues** — content sitting in an untagged cell (invisible to
|
||||||
|
tests and the export pipeline); a cell whose comment says one thing and whose tag
|
||||||
|
says another; presentation logic inside a content cell or vice versa.
|
||||||
|
*Spot it by:* asking, for each cell, "who consumes this — and would they find it?"
|
||||||
|
5. **Stage leakage** — anything client-inappropriate reachable on the Mercury stage:
|
||||||
|
markdown cells ALWAYS render on stage, so internal notes/instructions in markdown
|
||||||
|
are leaks; diagnostics via bare `print` instead of `backstage()`; placeholder or
|
||||||
|
internal wording in the board/stage HTML; a filled engagement value where the
|
||||||
|
master should have a placeholder.
|
||||||
|
*Spot it by:* simulating the stage — markdown cells + non-backstage outputs of
|
||||||
|
code cells are what the client sees.
|
||||||
|
6. **Export quality** — appendix JSON missing state a figure or table shows;
|
||||||
|
values that won't JSON-serialize; the `.md` handoff missing something an LLM
|
||||||
|
would need to draft the write-up; preamble claims that don't match the document.
|
||||||
|
*Spot it by:* reading the appendix cell's payload against everything rendered
|
||||||
|
above it.
|
||||||
|
|
||||||
|
## Output format
|
||||||
|
|
||||||
|
Report findings **ordered by severity**, one line each, exactly:
|
||||||
|
|
||||||
|
```
|
||||||
|
[BLOCKER] cell <index> (<tag or first source line>): <finding> — <evidence> — <suggested fix>
|
||||||
|
[SHOULD-FIX] cell <index> (<tag or first source line>): <finding> — <evidence> — <suggested fix>
|
||||||
|
[NIT] cell <index> (<tag or first source line>): <finding> — <evidence> — <suggested fix>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **BLOCKER** — a client would see something wrong, or the notebook lies (stale
|
||||||
|
number, stage leak, broken reference).
|
||||||
|
- **SHOULD-FIX** — correctness/maintainability debt that won't embarrass anyone
|
||||||
|
today.
|
||||||
|
- **NIT** — polish.
|
||||||
|
|
||||||
|
End with one verdict line: `VERDICT: <n> blocker(s), <n> should-fix, <n> nit(s) —
|
||||||
|
<one-sentence overall judgement>`. If a theme produced no findings, do not pad —
|
||||||
|
finding nothing is a valid result; say `No findings.` above the verdict if the whole
|
||||||
|
review is clean. Output nothing outside this format.
|
||||||
107
docs/Study_Pattern_V1-00.md
Normal file
107
docs/Study_Pattern_V1-00.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Study Pattern v1.0.0
|
||||||
|
|
||||||
|
How Palladium ships **Studies**: dated reproductions of a published base document —
|
||||||
|
a Forrester TEI study or similar — personalized to a client as an overlay. A Study is
|
||||||
|
a **master**: the anchor is public/published data, the master stays client-clean, and
|
||||||
|
client personalization happens in an engagement copy outside this repo. References:
|
||||||
|
[`studies/202512_TEI_Genesys_CX_Cloud/`](../studies/202512_TEI_Genesys_CX_Cloud/) and
|
||||||
|
[`studies/202602_TEI_Amazon_Connect/`](../studies/202602_TEI_Amazon_Connect/)
|
||||||
|
(both package `teicalc`).
|
||||||
|
|
||||||
|
## 🐾 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 a
|
||||||
|
study. This document holds what studies ADD to the
|
||||||
|
[Mercury Notebook Deliverable Pattern](Mercury_Notebook_Pattern_V1-00.md) — shared
|
||||||
|
mechanics live there and are not restated. `CLAUDE.md` takes precedence where
|
||||||
|
documents disagree.
|
||||||
|
|
||||||
|
**Status note — read this before "fixing" a study:** this pattern is grounded in the
|
||||||
|
TEI twins **as they are today**: math and anchors in the `.py` engine, hand-authored
|
||||||
|
notebooks, no tagged-cell taxonomy. The notebook-first evolution — client data in an
|
||||||
|
`engagement-data` cell, tagged content cells, the Assessment Pattern's cell taxonomy —
|
||||||
|
**lands with the first study redesign** (a recorded identified opportunity in
|
||||||
|
`CLAUDE.md`), not by piecemeal edits. Until then the repo-level structural suite
|
||||||
|
grandfathers the study notebooks by name, and their current shape is the correct
|
||||||
|
shape. `studies/202607_CTM_GenesysCX/` is a grandfathered real-client engagement
|
||||||
|
study — frozen; see `CLAUDE.md` § Known liabilities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What a Study is
|
||||||
|
|
||||||
|
| | **Study** | Assessment |
|
||||||
|
|---|---|---|
|
||||||
|
| Reproduces | a **dated publication** (TEI) or signed engagement | — |
|
||||||
|
| Naming | `YYYYMM_TEI_Vendor_Product` / `YYYYMM_Client_Engagement` | undated |
|
||||||
|
| Anchor | the publication's composite, **verbatim** | the instrument's content |
|
||||||
|
| "Numbers" | dollars — NPV / ROI / payback | qualitative state |
|
||||||
|
| Client data | overlay rescale on the anchor | `engagement-data` cell |
|
||||||
|
|
||||||
|
The `YYYYMM_` prefix is the **publication/engagement date** (e.g. `202512_` = the
|
||||||
|
December 2025 Forrester study), not the date you worked on it — a Study is a snapshot
|
||||||
|
of a dated document and stays dated.
|
||||||
|
|
||||||
|
## Verbatim anchor + contracted overlay
|
||||||
|
|
||||||
|
The heart of a Study (mechanics in the Mercury Pattern, *Recommended Practices*):
|
||||||
|
|
||||||
|
- `anchor.py` holds the publication's tables as `*_VERBATIM` constants — **NEVER
|
||||||
|
edited**. The reproduction is only worth anything if the anchor is the exact
|
||||||
|
published record.
|
||||||
|
- `overlay.py` layers client personalization over the anchor — in the current twins, a
|
||||||
|
🟡 first-order linear rescale by client drivers (agents / contacts / growth):
|
||||||
|
"the composite at your size", never presented as "your TEI".
|
||||||
|
- Notebooks read through the `anchor()` helper so the walk *published → personalized*
|
||||||
|
stays explicit and auditable on stage.
|
||||||
|
|
||||||
|
## The reproduction gate
|
||||||
|
|
||||||
|
A Study's gate (Mercury Pattern §4) MUST pin the **published headline numbers** —
|
||||||
|
the reproduction is the smoke test:
|
||||||
|
|
||||||
|
- Amazon Connect twin: NPV **$78,713,715**, ROI **342%**, payback **<6 months**.
|
||||||
|
- Genesys CX Cloud twin: NPV **$10,783,468**, ROI **266%**.
|
||||||
|
|
||||||
|
Plus the **composite-scale identity**: the overlay at the composite's own drivers must
|
||||||
|
reproduce the anchor exactly (rescale(1.0) == anchor) — the proof the personalization
|
||||||
|
layer adds nothing at scale 1.
|
||||||
|
|
||||||
|
## Confidentiality posture
|
||||||
|
|
||||||
|
- The **published source PDF** (the Forrester study) is public and MAY live in the
|
||||||
|
study's `docs/` — it is the one exception to the binary-documents block in
|
||||||
|
`.gitignore`: add it with `git add -f` and name the source in the commit message.
|
||||||
|
- Everything else follows `CLAUDE.md` § Confidentiality: masters client-clean, client
|
||||||
|
personalization only in an engagement copy outside the repo, and NEVER commit client
|
||||||
|
documents (SOWs, quotes, NDA'd vendor decks). The CTM study predates this rule and
|
||||||
|
is grandfathered — do not use it as precedent.
|
||||||
|
|
||||||
|
## Layout & testing
|
||||||
|
|
||||||
|
Layout and packaging are the Mercury Pattern's (self-contained `teicalc`-style engine,
|
||||||
|
per-study venv, whole toolchain as core deps). Testing is the standard four layers
|
||||||
|
plus repo-level layer 0; a Study's engine pins carry BOTH the verbatim record and the
|
||||||
|
overlay, so neither can drift:
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/test_anchor.py # the published tables, pinned line by line
|
||||||
|
tests/test_overlay.py # rescale identity at composite scale + spot rescales
|
||||||
|
tests/test_scenarios.py # scenario framing over the overlay
|
||||||
|
tests/test_model.py # finance primitives (NPV, payback) hand-checked
|
||||||
|
tests/test_staging.py # stage/backstage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variants
|
||||||
|
|
||||||
|
The Mercury Pattern's variants map onto Studies as:
|
||||||
|
|
||||||
|
- **Variant 4 — TEI composite reproduction** — the canonical Study (both twins).
|
||||||
|
- **Variant 1 — corrected/pressure-tested business case** — engagement-study form
|
||||||
|
(CTM's `ctm_business_case_corrected`): vendor claims verbatim, omitted costs added.
|
||||||
|
- **Variant 2 — scenario notebook on a thin module** — a second question over the same
|
||||||
|
engine (CTM's `migration_wfm`).
|
||||||
|
- **Variant 3 — exploratory calculator** — pre-anchor what-if surface that graduates
|
||||||
|
into Variant 1 as facts arrive.
|
||||||
@@ -16,6 +16,7 @@ dependencies = [
|
|||||||
"pandas>=2.0",
|
"pandas>=2.0",
|
||||||
"plotly>=5.18",
|
"plotly>=5.18",
|
||||||
"numpy>=1.26",
|
"numpy>=1.26",
|
||||||
|
"nbformat>=5.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -27,7 +28,7 @@ palladium = "core.cli.main:main"
|
|||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["core*", "palladium*"]
|
include = ["core*", "palladium*"]
|
||||||
exclude = ["tests*", "studies*", "docs*"]
|
exclude = ["tests*", "studies*", "assessments*", "docs*"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
@@ -44,4 +45,5 @@ ignore = ["E501"] # line length handled by formatter
|
|||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"studies/*/notebooks/*.ipynb" = ["E402"]
|
"studies/*/notebooks/*.ipynb" = ["E402"]
|
||||||
|
"assessments/*/notebooks/*.ipynb" = ["E402"]
|
||||||
"tests/*" = ["F401"]
|
"tests/*" = ["F401"]
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ jupyter>=1.0
|
|||||||
pandas>=2.0
|
pandas>=2.0
|
||||||
plotly>=5.18
|
plotly>=5.18
|
||||||
numpy>=1.26
|
numpy>=1.26
|
||||||
|
nbformat>=5.9
|
||||||
pytest>=7.4
|
pytest>=7.4
|
||||||
ruff>=0.1
|
ruff>=0.1
|
||||||
|
|||||||
6
studies/202607_CTM_GenesysCX/docs/README.md
Normal file
6
studies/202607_CTM_GenesysCX/docs/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Confidential source material — grandfathered
|
||||||
|
|
||||||
|
The client/vendor documents in this directory were committed before the
|
||||||
|
confidentiality rule existed (see `CLAUDE.md`, *Confidentiality* and *Known
|
||||||
|
liabilities*). They are grandfathered: **do not add more**. Extraction of this
|
||||||
|
study to an engagement archive is a recorded follow-up.
|
||||||
@@ -10135,16 +10135,16 @@
|
|||||||
"label": "NA — current platform cost ($/yr)",
|
"label": "NA — current platform cost ($/yr)",
|
||||||
"layout": "IPY_MODEL_6295cf92344b415598307483934485f3",
|
"layout": "IPY_MODEL_6295cf92344b415598307483934485f3",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 8000000.0,
|
"max": 8000000,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "inline",
|
"position": "inline",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
"step": 10000.0,
|
"step": 10000,
|
||||||
"tabbable": null,
|
"tabbable": null,
|
||||||
"tooltip": null,
|
"tooltip": null,
|
||||||
"url_key": "",
|
"url_key": "",
|
||||||
"value": 3348969.0
|
"value": 3348969
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"53869a7ea78b44f4a134983d354228f0": {
|
"53869a7ea78b44f4a134983d354228f0": {
|
||||||
@@ -10382,7 +10382,7 @@
|
|||||||
"layout": "IPY_MODEL_0e3be284c70e45b7ad9fe12c3b76163f",
|
"layout": "IPY_MODEL_0e3be284c70e45b7ad9fe12c3b76163f",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 0.6,
|
"max": 0.6,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "sidebar",
|
"position": "sidebar",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
@@ -10415,16 +10415,16 @@
|
|||||||
"label": "ASIA — current platform cost ($/yr)",
|
"label": "ASIA — current platform cost ($/yr)",
|
||||||
"layout": "IPY_MODEL_61085bdb080e48e186ec10b1abee291a",
|
"layout": "IPY_MODEL_61085bdb080e48e186ec10b1abee291a",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 8000000.0,
|
"max": 8000000,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "inline",
|
"position": "inline",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
"step": 10000.0,
|
"step": 10000,
|
||||||
"tabbable": null,
|
"tabbable": null,
|
||||||
"tooltip": null,
|
"tooltip": null,
|
||||||
"url_key": "",
|
"url_key": "",
|
||||||
"value": 2069588.0
|
"value": 2069588
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"61085bdb080e48e186ec10b1abee291a": {
|
"61085bdb080e48e186ec10b1abee291a": {
|
||||||
@@ -10555,16 +10555,16 @@
|
|||||||
"label": "CCaaS licence run-rate ($/yr) — contracted",
|
"label": "CCaaS licence run-rate ($/yr) — contracted",
|
||||||
"layout": "IPY_MODEL_3c767a6b472d42c89e6b7baf705fcbbc",
|
"layout": "IPY_MODEL_3c767a6b472d42c89e6b7baf705fcbbc",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 10000000.0,
|
"max": 10000000,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "inline",
|
"position": "inline",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
"step": 100000.0,
|
"step": 100000,
|
||||||
"tabbable": null,
|
"tabbable": null,
|
||||||
"tooltip": null,
|
"tooltip": null,
|
||||||
"url_key": "",
|
"url_key": "",
|
||||||
"value": 3200000.0
|
"value": 3200000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"6ce50fa7157042a4be99b19f24be3ce7": {
|
"6ce50fa7157042a4be99b19f24be3ce7": {
|
||||||
@@ -10642,16 +10642,16 @@
|
|||||||
"label": "ANZ — current platform cost ($/yr)",
|
"label": "ANZ — current platform cost ($/yr)",
|
||||||
"layout": "IPY_MODEL_54f1095af078476ba27715f8826aa967",
|
"layout": "IPY_MODEL_54f1095af078476ba27715f8826aa967",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 8000000.0,
|
"max": 8000000,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "inline",
|
"position": "inline",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
"step": 10000.0,
|
"step": 10000,
|
||||||
"tabbable": null,
|
"tabbable": null,
|
||||||
"tooltip": null,
|
"tooltip": null,
|
||||||
"url_key": "",
|
"url_key": "",
|
||||||
"value": 677320.0
|
"value": 677320
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"79ec042073404a6da4ec639b6b44a57c": {
|
"79ec042073404a6da4ec639b6b44a57c": {
|
||||||
@@ -11267,16 +11267,16 @@
|
|||||||
"label": "EMEA — current platform cost ($/yr)",
|
"label": "EMEA — current platform cost ($/yr)",
|
||||||
"layout": "IPY_MODEL_8309c39462b34cc5afad42785a0c53ff",
|
"layout": "IPY_MODEL_8309c39462b34cc5afad42785a0c53ff",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 8000000.0,
|
"max": 8000000,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "inline",
|
"position": "inline",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
"step": 10000.0,
|
"step": 10000,
|
||||||
"tabbable": null,
|
"tabbable": null,
|
||||||
"tooltip": null,
|
"tooltip": null,
|
||||||
"url_key": "",
|
"url_key": "",
|
||||||
"value": 1204124.0
|
"value": 1204124
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"d69203bdb8584fc9975e475199fcc235": {
|
"d69203bdb8584fc9975e475199fcc235": {
|
||||||
@@ -11301,16 +11301,16 @@
|
|||||||
"label": "Genesys ramp — licence-free months",
|
"label": "Genesys ramp — licence-free months",
|
||||||
"layout": "IPY_MODEL_7b92a3a1d4ec49049e22687c38236912",
|
"layout": "IPY_MODEL_7b92a3a1d4ec49049e22687c38236912",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 24.0,
|
"max": 24,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "inline",
|
"position": "inline",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
"step": 1.0,
|
"step": 1,
|
||||||
"tabbable": null,
|
"tabbable": null,
|
||||||
"tooltip": null,
|
"tooltip": null,
|
||||||
"url_key": "",
|
"url_key": "",
|
||||||
"value": 6.0
|
"value": 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"d7d4d74246ea490da7610c277c802b99": {
|
"d7d4d74246ea490da7610c277c802b99": {
|
||||||
@@ -11335,8 +11335,8 @@
|
|||||||
"label": "AI Translate eligibility 🟡",
|
"label": "AI Translate eligibility 🟡",
|
||||||
"layout": "IPY_MODEL_1bf396a231374a2097e03bf45fc874ff",
|
"layout": "IPY_MODEL_1bf396a231374a2097e03bf45fc874ff",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 1.0,
|
"max": 1,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "sidebar",
|
"position": "sidebar",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
@@ -11521,7 +11521,7 @@
|
|||||||
"layout": "IPY_MODEL_bf7b6f3bca5f48e6838dc54f84943731",
|
"layout": "IPY_MODEL_bf7b6f3bca5f48e6838dc54f84943731",
|
||||||
"layout_path": null,
|
"layout_path": null,
|
||||||
"max": 0.5,
|
"max": 0.5,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"position": "sidebar",
|
"position": "sidebar",
|
||||||
"render_slot_id": null,
|
"render_slot_id": null,
|
||||||
"source_cell_id": null,
|
"source_cell_id": null,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"source": [
|
"source": [
|
||||||
"# ── Setup ──────────────────────────────────────────────────────────\n",
|
"# ── Setup ──────────────────────────3rd with just the────────────────────────────────\n",
|
||||||
"import sys, pathlib\n",
|
"import sys, pathlib\n",
|
||||||
"_ROOT = pathlib.Path.cwd()\n",
|
"_ROOT = pathlib.Path.cwd()\n",
|
||||||
"if not (_ROOT / \"tokencalc\").exists(): # notebook lives in notebooks/\n",
|
"if not (_ROOT / \"tokencalc\").exists(): # notebook lives in notebooks/\n",
|
||||||
@@ -8847,7 +8847,7 @@
|
|||||||
"disabled": false,
|
"disabled": false,
|
||||||
"layout": "IPY_MODEL_d68795b0944341bf8354500e127e4e59",
|
"layout": "IPY_MODEL_d68795b0944341bf8354500e127e4e59",
|
||||||
"max": 0.5,
|
"max": 0.5,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"orientation": "horizontal",
|
"orientation": "horizontal",
|
||||||
"readout": true,
|
"readout": true,
|
||||||
"readout_format": ".0%",
|
"readout_format": ".0%",
|
||||||
@@ -8940,7 +8940,7 @@
|
|||||||
"description_allow_html": false,
|
"description_allow_html": false,
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"layout": "IPY_MODEL_3462a88bfec94f01b6aea7530a431884",
|
"layout": "IPY_MODEL_3462a88bfec94f01b6aea7530a431884",
|
||||||
"max": 5.0,
|
"max": 5,
|
||||||
"min": 0.5,
|
"min": 0.5,
|
||||||
"orientation": "horizontal",
|
"orientation": "horizontal",
|
||||||
"readout": true,
|
"readout": true,
|
||||||
@@ -9105,7 +9105,7 @@
|
|||||||
"disabled": false,
|
"disabled": false,
|
||||||
"layout": "IPY_MODEL_9fe19d91747e4865a104e65b658b7c8e",
|
"layout": "IPY_MODEL_9fe19d91747e4865a104e65b658b7c8e",
|
||||||
"max": 0.7,
|
"max": 0.7,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"orientation": "horizontal",
|
"orientation": "horizontal",
|
||||||
"readout": true,
|
"readout": true,
|
||||||
"readout_format": ".0%",
|
"readout_format": ".0%",
|
||||||
@@ -9334,7 +9334,7 @@
|
|||||||
"description_allow_html": false,
|
"description_allow_html": false,
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"layout": "IPY_MODEL_fd591354d1214f4b9b7d8850be81a511",
|
"layout": "IPY_MODEL_fd591354d1214f4b9b7d8850be81a511",
|
||||||
"max": 1.0,
|
"max": 1,
|
||||||
"min": 0.3,
|
"min": 0.3,
|
||||||
"orientation": "horizontal",
|
"orientation": "horizontal",
|
||||||
"readout": true,
|
"readout": true,
|
||||||
@@ -9434,7 +9434,7 @@
|
|||||||
"description_allow_html": false,
|
"description_allow_html": false,
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"layout": "IPY_MODEL_4c9514e9b96142e9a269035bdd0714c4",
|
"layout": "IPY_MODEL_4c9514e9b96142e9a269035bdd0714c4",
|
||||||
"max": 1.0,
|
"max": 1,
|
||||||
"min": 0.3,
|
"min": 0.3,
|
||||||
"orientation": "horizontal",
|
"orientation": "horizontal",
|
||||||
"readout": true,
|
"readout": true,
|
||||||
@@ -9483,7 +9483,7 @@
|
|||||||
"disabled": false,
|
"disabled": false,
|
||||||
"layout": "IPY_MODEL_995b326fefa942da94ce18e92254860e",
|
"layout": "IPY_MODEL_995b326fefa942da94ce18e92254860e",
|
||||||
"max": 0.3,
|
"max": 0.3,
|
||||||
"min": 0.0,
|
"min": 0,
|
||||||
"orientation": "horizontal",
|
"orientation": "horizontal",
|
||||||
"readout": true,
|
"readout": true,
|
||||||
"readout_format": ".0%",
|
"readout_format": ".0%",
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
"""Export the discovery notebook as an LLM-readable report source.
|
|
||||||
|
|
||||||
Executes the notebook fresh (widget defaults — or whatever you've captured and
|
|
||||||
saved in the notebook), then writes both formats to exports/:
|
|
||||||
|
|
||||||
exports/cx_discovery.html — human-reviewable
|
|
||||||
exports/cx_discovery.md — leanest LLM input
|
|
||||||
|
|
||||||
The client-facing board is HTML the export carries, but the machine payload —
|
|
||||||
every topic's status, sub-topic tally, and captured notes — lives in the
|
|
||||||
backstage data-appendix cell (markdown table + JSON), so the exported `.md` is
|
|
||||||
complete input for drafting the survey write-up or seeding a business case.
|
|
||||||
|
|
||||||
Run from the study root: python scripts/export_report.py
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
NOTEBOOKS = [
|
|
||||||
ROOT / "notebooks" / "cx_discovery.ipynb",
|
|
||||||
]
|
|
||||||
EXPORTS = ROOT / "exports"
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
picked = [nb for nb in NOTEBOOKS
|
|
||||||
if len(sys.argv) < 2 or sys.argv[1] in nb.name]
|
|
||||||
if not picked:
|
|
||||||
sys.exit(f"no notebook matches {sys.argv[1]!r}")
|
|
||||||
EXPORTS.mkdir(exist_ok=True)
|
|
||||||
for nb in picked:
|
|
||||||
for fmt in ("html", "markdown"):
|
|
||||||
subprocess.run(
|
|
||||||
[sys.executable, "-m", "nbconvert", "--execute",
|
|
||||||
"--to", fmt, "--output-dir", str(EXPORTS), str(nb)],
|
|
||||||
check=True, cwd=ROOT,
|
|
||||||
)
|
|
||||||
for p in sorted(EXPORTS.iterdir()):
|
|
||||||
if p.suffix in (".html", ".md"):
|
|
||||||
print(f"wrote {p.relative_to(ROOT)} ({p.stat().st_size / 1024:,.0f} KB)")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
"""Stage/backstage detection — Mercury kernels carry MERCURY_CONFIG_DIR."""
|
|
||||||
|
|
||||||
from discoverylib import staging
|
|
||||||
|
|
||||||
|
|
||||||
def test_backstage_prints_only_off_stage(monkeypatch, capsys):
|
|
||||||
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
|
|
||||||
assert not staging.on_stage()
|
|
||||||
staging.backstage("visible")
|
|
||||||
assert capsys.readouterr().out == "visible\n"
|
|
||||||
|
|
||||||
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
|
|
||||||
assert staging.on_stage()
|
|
||||||
staging.backstage("hidden")
|
|
||||||
assert capsys.readouterr().out == ""
|
|
||||||
117
tests/nbcheck.py
Normal file
117
tests/nbcheck.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""Structural checks for every master notebook in the library.
|
||||||
|
|
||||||
|
Kernel-free: everything here reads the committed ``.ipynb`` JSON with
|
||||||
|
nbformat — no study venv, no execution. Content and engine pins stay in
|
||||||
|
each master's own ``tests/`` (run in its venv); this layer guards only
|
||||||
|
STRUCTURE: the notebook parses, was executed cleanly top-to-bottom, and
|
||||||
|
(for notebook-first masters) carries the tagged-cell taxonomy the
|
||||||
|
Assessment Pattern requires.
|
||||||
|
|
||||||
|
Classification is explicit and non-silent: every notebook on disk must be
|
||||||
|
listed in exactly one of NOTEBOOK_FIRST or GRANDFATHERED (a completeness
|
||||||
|
test enforces it), so a new master cannot dodge the suite, and every
|
||||||
|
exemption carries its reason — grandfathered notebooks run the structural
|
||||||
|
tier and skip the notebook-first tier with that reason shown by
|
||||||
|
``pytest -rs``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import nbformat
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Masters built on the notebook-first content model (Assessment Pattern):
|
||||||
|
# full check set, including the tagged-cell taxonomy.
|
||||||
|
NOTEBOOK_FIRST = {
|
||||||
|
"assessments/CX_Discovery_Workshop/notebooks/cx_discovery.ipynb",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Structural tier only, each with its recorded reason (see CLAUDE.md,
|
||||||
|
# Known liabilities). Redesigning one of these to notebook-first means
|
||||||
|
# moving it up to NOTEBOOK_FIRST — never deleting it from here silently.
|
||||||
|
GRANDFATHERED = {
|
||||||
|
"assessments/CX_AI_Diagnostic/notebooks/diagnostic.ipynb":
|
||||||
|
"generated notebook; pre-dates the notebook-first model (redesign pending)",
|
||||||
|
"studies/202512_TEI_Genesys_CX_Cloud/notebooks/business_case.ipynb":
|
||||||
|
"pre-tag TEI master (redesign pending)",
|
||||||
|
"studies/202602_TEI_Amazon_Connect/notebooks/business_case.ipynb":
|
||||||
|
"pre-tag TEI master (redesign pending)",
|
||||||
|
"studies/202607_CTM_GenesysCX/notebooks/ctm_business_case_corrected.ipynb":
|
||||||
|
"real-client study, frozen (see CLAUDE.md known liabilities)",
|
||||||
|
"studies/202607_CTM_GenesysCX/notebooks/ctm_business_case_no_current_state.ipynb":
|
||||||
|
"real-client study, frozen (see CLAUDE.md known liabilities)",
|
||||||
|
"studies/202607_CTM_GenesysCX/notebooks/ctm_business_case_virtual_agents.ipynb":
|
||||||
|
"real-client study, frozen (see CLAUDE.md known liabilities)",
|
||||||
|
"studies/202607_CTM_GenesysCX/notebooks/ctm_migration_wfm.ipynb":
|
||||||
|
"real-client study, frozen (see CLAUDE.md known liabilities)",
|
||||||
|
"studies/202607_CTM_GenesysCX/notebooks/ctm_token_calculator.ipynb":
|
||||||
|
"real-client study, frozen (see CLAUDE.md known liabilities)",
|
||||||
|
"template/MercuryNotebook/notebooks/business_case.ipynb":
|
||||||
|
"template encodes the py-engine model (rework pending)",
|
||||||
|
}
|
||||||
|
|
||||||
|
ALL_CLASSIFIED = sorted(NOTEBOOK_FIRST | set(GRANDFATHERED))
|
||||||
|
|
||||||
|
# Tags that may appear at most once per notebook.
|
||||||
|
UNIQUE_TAGS = ("topic-bank", "engagement-data", "gate", "data-appendix")
|
||||||
|
|
||||||
|
|
||||||
|
def discover() -> list[str]:
|
||||||
|
"""Every notebook on disk under the master roots (repo-relative)."""
|
||||||
|
found: list[str] = []
|
||||||
|
for base in ("studies", "assessments", "template"):
|
||||||
|
root = REPO / base
|
||||||
|
if not root.is_dir():
|
||||||
|
continue
|
||||||
|
for p in root.rglob("*.ipynb"):
|
||||||
|
if ".ipynb_checkpoints" in p.parts or ".venv" in p.parts:
|
||||||
|
continue
|
||||||
|
if p.parent.name != "notebooks":
|
||||||
|
continue
|
||||||
|
found.append(p.relative_to(REPO).as_posix())
|
||||||
|
return sorted(found)
|
||||||
|
|
||||||
|
|
||||||
|
def load(rel: str) -> Any:
|
||||||
|
return nbformat.read(REPO / rel, as_version=4)
|
||||||
|
|
||||||
|
|
||||||
|
def cell_tags(cell: Any) -> list[str]:
|
||||||
|
return list(cell.metadata.get("tags", []))
|
||||||
|
|
||||||
|
|
||||||
|
def cells_tagged(nb: Any, tag: str) -> list[int]:
|
||||||
|
return [i for i, c in enumerate(nb.cells) if tag in cell_tags(c)]
|
||||||
|
|
||||||
|
|
||||||
|
def execution_problem(nb: Any) -> str | None:
|
||||||
|
"""None if executed cleanly top-to-bottom, else what's wrong.
|
||||||
|
|
||||||
|
Non-empty code cells must carry integer execution counts, strictly
|
||||||
|
increasing 1..N in document order (proof of one clean linear run);
|
||||||
|
empty cells may be unexecuted (``None``).
|
||||||
|
"""
|
||||||
|
prev = 0
|
||||||
|
for i, c in enumerate(nb.cells):
|
||||||
|
if c.cell_type != "code" or not c.source.strip():
|
||||||
|
continue
|
||||||
|
ec = c.get("execution_count")
|
||||||
|
if not isinstance(ec, int):
|
||||||
|
return f"cell {i} has no execution count (notebook not executed?)"
|
||||||
|
if ec != prev + 1:
|
||||||
|
return f"cell {i} has execution count {ec}, expected {prev + 1}"
|
||||||
|
prev = ec
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def error_outputs(nb: Any) -> list[int]:
|
||||||
|
return [
|
||||||
|
i
|
||||||
|
for i, c in enumerate(nb.cells)
|
||||||
|
if c.cell_type == "code"
|
||||||
|
and any(o.get("output_type") == "error" for o in c.get("outputs", []))
|
||||||
|
]
|
||||||
163
tests/test_notebooks.py
Normal file
163
tests/test_notebooks.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"""Repo-level notebook validation — structure only, kernel-free.
|
||||||
|
|
||||||
|
Layer 0 of the testing story (see CLAUDE.md and the pattern docs): runs
|
||||||
|
from the ROOT venv against every master's committed ``.ipynb``. Content
|
||||||
|
and engine pins live in each master's own ``tests/``.
|
||||||
|
|
||||||
|
Tiers (defined in ``nbcheck.py``):
|
||||||
|
structural — every notebook: parses, python3 kernel, no error outputs,
|
||||||
|
cleanly executed top-to-bottom, no duplicate claimed-unique
|
||||||
|
tags.
|
||||||
|
notebook-first — NOTEBOOK_FIRST masters only: the tagged-cell taxonomy
|
||||||
|
(topic-bank / engagement-data / gate / data-appendix) with
|
||||||
|
its uniqueness and position rules. GRANDFATHERED notebooks
|
||||||
|
skip this tier with their recorded reason (``pytest -rs``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .nbcheck import (
|
||||||
|
ALL_CLASSIFIED,
|
||||||
|
GRANDFATHERED,
|
||||||
|
NOTEBOOK_FIRST,
|
||||||
|
UNIQUE_TAGS,
|
||||||
|
cell_tags,
|
||||||
|
cells_tagged,
|
||||||
|
discover,
|
||||||
|
error_outputs,
|
||||||
|
execution_problem,
|
||||||
|
load,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def notebook_first_only(rel: str) -> None:
|
||||||
|
if rel in GRANDFATHERED:
|
||||||
|
pytest.skip(GRANDFATHERED[rel])
|
||||||
|
|
||||||
|
|
||||||
|
# ── Classification is complete — a new master cannot dodge the suite ──
|
||||||
|
def test_every_notebook_on_disk_is_classified():
|
||||||
|
on_disk = set(discover())
|
||||||
|
classified = set(ALL_CLASSIFIED)
|
||||||
|
assert on_disk == classified, (
|
||||||
|
f"unclassified notebooks: {sorted(on_disk - classified)}; "
|
||||||
|
f"classified but missing from disk: {sorted(classified - on_disk)} — "
|
||||||
|
"add each notebook to NOTEBOOK_FIRST or GRANDFATHERED (with a reason) "
|
||||||
|
"in tests/nbcheck.py"
|
||||||
|
)
|
||||||
|
assert not (NOTEBOOK_FIRST & set(GRANDFATHERED)), "a notebook is in both tiers"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Structural tier — every notebook ─────────────────────────────────
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_parses_as_nbformat_4(rel):
|
||||||
|
nb = load(rel)
|
||||||
|
assert nb.nbformat == 4
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_kernel_is_python3(rel):
|
||||||
|
nb = load(rel)
|
||||||
|
assert nb.metadata.get("kernelspec", {}).get("name") == "python3"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_no_error_outputs(rel):
|
||||||
|
assert error_outputs(load(rel)) == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_executed_cleanly_top_to_bottom(rel):
|
||||||
|
problem = execution_problem(load(rel))
|
||||||
|
assert problem is None, problem
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_no_duplicate_unique_tags(rel):
|
||||||
|
nb = load(rel)
|
||||||
|
for tag in UNIQUE_TAGS:
|
||||||
|
hits = cells_tagged(nb, tag)
|
||||||
|
assert len(hits) <= 1, f"tag {tag!r} appears in cells {hits}"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Notebook-first tier — the Assessment Pattern tagged-cell taxonomy ─
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_has_exactly_one_topic_bank(rel):
|
||||||
|
notebook_first_only(rel)
|
||||||
|
assert len(cells_tagged(load(rel), "topic-bank")) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_engagement_data_present_and_above_widgets(rel):
|
||||||
|
notebook_first_only(rel)
|
||||||
|
nb = load(rel)
|
||||||
|
hits = cells_tagged(nb, "engagement-data")
|
||||||
|
assert len(hits) == 1
|
||||||
|
# Position: client data must never re-run on a sidebar change, so the
|
||||||
|
# cell precedes the widget-defining presentation cell (identified by a
|
||||||
|
# widget constructor in its source; if a master builds widgets another
|
||||||
|
# way, its own tests carry the precise rule).
|
||||||
|
widget_cells = [
|
||||||
|
i
|
||||||
|
for i, c in enumerate(nb.cells)
|
||||||
|
if c.cell_type == "code"
|
||||||
|
and "presentation" in cell_tags(c)
|
||||||
|
and "mr.Select(" in c.source
|
||||||
|
]
|
||||||
|
if widget_cells:
|
||||||
|
assert hits[0] < min(widget_cells), (
|
||||||
|
"engagement-data cell must sit above the widget cell"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_has_one_gate_with_asserts(rel):
|
||||||
|
notebook_first_only(rel)
|
||||||
|
nb = load(rel)
|
||||||
|
hits = cells_tagged(nb, "gate")
|
||||||
|
assert len(hits) == 1
|
||||||
|
assert "assert" in nb.cells[hits[0]].source, "gate cell carries no assert"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_data_appendix_is_last_code_cell(rel):
|
||||||
|
notebook_first_only(rel)
|
||||||
|
nb = load(rel)
|
||||||
|
hits = cells_tagged(nb, "data-appendix")
|
||||||
|
assert len(hits) == 1
|
||||||
|
last_code = max(
|
||||||
|
i for i, c in enumerate(nb.cells) if c.cell_type == "code"
|
||||||
|
)
|
||||||
|
assert hits[0] == last_code, "data-appendix must be the last code cell"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_presentation_cells_do_not_print(rel):
|
||||||
|
notebook_first_only(rel)
|
||||||
|
nb = load(rel)
|
||||||
|
for i in cells_tagged(nb, "presentation"):
|
||||||
|
streams = [
|
||||||
|
o for o in nb.cells[i].get("outputs", [])
|
||||||
|
if o.get("output_type") == "stream"
|
||||||
|
]
|
||||||
|
assert not streams, (
|
||||||
|
f"presentation cell {i} emits stream output — route diagnostics "
|
||||||
|
"through backstage()"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("rel", ALL_CLASSIFIED)
|
||||||
|
def test_content_cells_are_exec_safe(rel):
|
||||||
|
# topic-bank / engagement-data are exec'd by tag in per-master tests
|
||||||
|
# and by scripts — magics or shell escapes would break that contract.
|
||||||
|
notebook_first_only(rel)
|
||||||
|
nb = load(rel)
|
||||||
|
for tag in ("topic-bank", "engagement-data"):
|
||||||
|
for i in cells_tagged(nb, tag):
|
||||||
|
for line in nb.cells[i].source.splitlines():
|
||||||
|
stripped = line.lstrip()
|
||||||
|
assert not stripped.startswith(("%", "!")), (
|
||||||
|
f"{tag} cell {i} uses a magic/shell escape: {line!r}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user