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:
2026-07-31 16:16:07 +00:00
parent 53c069fddb
commit a967f73d09
61 changed files with 4881 additions and 4257 deletions

View 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
View File

@@ -7,8 +7,26 @@
# Palladium-specific
.env
.DS_Store
# Generated exports — never committed (masters and assessments)
studies/*/exports/*
!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
profile_default/

261
CLAUDE.md Normal file
View 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 24 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

View File

@@ -4,7 +4,7 @@ VENV := .venv
PY := $(VENV)/bin/python
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)
setup:
@@ -19,10 +19,16 @@ setup:
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:
$(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:
$(VENV)/bin/ruff check .

508
README.md
View File

@@ -1,425 +1,139 @@
# 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.
```
┌──────────────────────────────────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────────────────────────────────┘
```
## Taxonomy
### 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 |
|-----------|---------|
| **`studies/`** | One self-contained folder per engagement — own venv, engine package, Mercury notebook, tests, exports |
| **`template/`** | Copy-me study scaffold — start here for new studies |
| **`core/tei_client`** | Python API client for Athena's TEI endpoints |
| **`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 |
Masters in this repo stay **client-clean**: placeholder engagement data, published or
synthetic numbers, nothing a client said. Patterns:
[Assessment](docs/Assessment_Pattern_V1-00.md) ·
[Study](docs/Study_Pattern_V1-00.md) ·
[shared Mercury mechanics](docs/Mercury_Notebook_Pattern_V1-00.md).
> **All studies follow the [Mercury Notebook Deliverable Pattern](docs/Mercury_Notebook_Pattern_V1-00.md)**:
> 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 (050%) 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
## Repository layout
```
palladium/
├── 00_setup.ipynb # ← START HERE: credentials + connection
├── Makefile # make setup / lab / test
├── core/ # Shared, study-agnostic Python package
── bootstrap.py # one-import notebook setup (init, save_credentials)
│ ├── tei_client/ # Athena API client
│ ├── client.py # TEIClient with all /api/v1/tei/ methods
│ └── models.py # Optional dataclasses for typed access
── calculations/ # Pure-python financial math
├── npv.py
├── roi.py
├── payback.py
│ │ └── scenarios.py
│ ├── export/
└── report_data.py # JSON envelope for the report pipeline
│ └── 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
├── CLAUDE.md # the always-on contract (rubric, layers, risk tiers)
├── assessments/
│ ├── CX_Discovery_Workshop/ # ★ reference implementation (notebook-first model)
── CX_AI_Diagnostic/ # capability diagnostic (pre-redesign: generated notebook)
├── studies/
│ ├── 202512_TEI_Genesys_CX_Cloud/ # Forrester TEI reproduction — NPV $10.8M · ROI 266%
├── 202602_TEI_Amazon_Connect/ # Forrester TEI reproduction — NPV $78.7M · ROI 342%
── 202607_CTM_GenesysCX/ # client engagement study (grandfathered — see CLAUDE.md)
├── template/MercuryNotebook/ # copy-me scaffold (py-engine model; see "Adding a master")
├── docs/ # the pattern docs, brand.md, review prompt, Athena reference
├── core/ # shared Athena toolkit (masters do NOT import it)
├── tests/ # core tests + the notebook structural suite (all masters)
├── 00_setup.ipynb # Athena credentials + connection (core layer)
└── Makefile # make setup / lab / test / check-notebooks
```
---
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
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
## Quick start — run a master
```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
and the calculation suite uses the Amazon Connect seed data to verify the
Forrester numbers reproduce within rounding.
Every master's README carries its own specifics; the commands are the same shape in
all of them.
### 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
ruff check .
ruff format .
python -m palladium test | list | reports | summary <id> | calculate <id> | export <id> -o export.json
```
### 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)
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
## Related projects
| Project | Relationship |
|---------|-------------|

View File

@@ -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).
Unlike the TEI business-case studies, this deliverable computes no dollars —
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
The topic bank — every topic, sub-topic, and facilitator prompt — lives in
the **`topic-bank` cell** of
[`notebooks/cx_discovery.ipynb`](notebooks/cx_discovery.ipynb) (the code cell
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
tracks the source survey
[`docs/cx_discovery_survey.md`](docs/cx_discovery_survey.md) (the original
`cxxm.md`), and the `key` slugs are stable identities the sidebar widgets,
captured notes, and JSON export all key off — never renumber or rename them
casually. The cell's own comment block carries the full editing rules.
Two tagged cells of
[`notebooks/cx_discovery.ipynb`](notebooks/cx_discovery.ipynb) carry
everything a consultant edits — **in Jupyter, never in a `.py` file**:
- **`topic-bank`** — the workshop content: every topic, sub-topic, and
facilitator prompt. It is the study's verbatim anchor: wording tracks the
source survey [`docs/cx_discovery_survey.md`](docs/cx_discovery_survey.md)
(the original `cxxm.md`), and the `key` slugs are stable identities the
sidebar widgets, captured notes, and JSON export all key off — never
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
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
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
out of the notebook (no kernel) and pins the content, so `pytest` guards the
bank exactly as shipped.
a content edit automatically. The test suite reads the tagged cells straight
out of the notebook (no kernel) and pins content shape, so `pytest` guards
the bank exactly as shipped.
## 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
session.py # Topic/SubTopic schema, status vocabulary, progress, export payload
staging.py # stage/backstage detection (copied verbatim)
scripts/export_report.py # nbconvert → exports/*.html + *.md
tests/ # content pins (read from the notebook) + engine pins + staging test
staging.py # stage/backstage detection (+ backstage_md for the appendix)
scripts/export_report.py # execute once → exports/*.html + LLM-ready *.md
tests/ # content + engagement pins (read from the notebook), engine pins, staging
docs/cx_discovery_survey.md # source survey (the original cxxm.md)
exports/ # generated report sources
exports/ # generated report sources (never committed)
```
## Run
@@ -72,11 +82,17 @@ pip install -e ".[dev]"
mercury --working-dir . # serve the stage (share this screen)
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
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
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
*logic* (not content) goes in `discoverylib/session.py` with pins in
`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.

View File

@@ -5,18 +5,18 @@
[main]
title = "CX Discovery Workshop"
favicon_emoji = "🧭"
footer = "CX Exploration & Discovery Workshop"
footer = "CX Exploration & Discovery Workshop · NTT DATA"
notebooks_button_label = "Workshops"
[welcome]
header = "CX Discovery Workshop"
message = """
The live visual for a CX exploration & discovery session. Topics and
progress render on screen for the client; the facilitator drives the
question script and captures notes backstage. Mark each topic's status in
the sidebar as the conversation moves the board and the progress bar
update live. Afterward, export the captured notes for drafting with
`python scripts/export_report.py`.
The live visual for a CX exploration & discovery session open
**cx_discovery.ipynb**. Topics and progress render on screen for the
client; the facilitator drives the question script and captures notes
backstage. Mark each topic's status in the sidebar as the conversation
moves the board and the progress bar update live. Afterward, export the
captured notes for drafting with `python scripts/export_report.py`.
"""
[theme]
@@ -46,6 +46,12 @@ focus_border_color = "#0072bc"
hover_background_color = "#eef5fb"
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_background_color = "#ffffff"
sidebar_text_color = "#2e404d"

View File

@@ -40,9 +40,9 @@ from .session import (
subtopic_checklist,
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__ = [
# schema
@@ -55,5 +55,5 @@ __all__ = [
"ChecklistItem", "subtopic_checklist", "subtopic_id",
"TopicState", "build_session", "session_json",
# staging
"on_stage", "backstage",
"on_stage", "backstage", "backstage_md",
]

View File

@@ -196,6 +196,9 @@ class TopicState:
notes: str
subtopics_total: 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(
@@ -216,8 +219,9 @@ def build_session(
topic_states: list[TopicState] = []
for t in topics:
status = normalize_status(status_by_topic.get(t.key))
done = sum(
1 for st in t.subtopics if subtopic_id(t.key, st.key) in done_subtopics
covered = tuple(
st.key for st in t.subtopics
if subtopic_id(t.key, st.key) in done_subtopics
)
topic_states.append(
TopicState(
@@ -231,7 +235,8 @@ def build_session(
color=STATUS_COLOR[status],
notes=(notes_by_topic.get(t.key) or "").strip(),
subtopics_total=len(t.subtopics),
subtopics_done=done,
subtopics_done=len(covered),
subtopics_covered=covered,
)
)
@@ -255,7 +260,7 @@ def session_json(
appendix does."""
prog: Progress = session["progress"]
return {
"study": "202607_CX_Discovery_Workshop",
"assessment": "CX_Discovery_Workshop",
"instrument": "CX Exploration & Discovery Workshop",
"meta": meta or {},
"progress": {
@@ -278,6 +283,7 @@ def session_json(
"minutes": ts.minutes,
"subtopics_done": ts.subtopics_done,
"subtopics_total": ts.subtopics_total,
"subtopics_covered": list(ts.subtopics_covered),
"notes": ts.notes,
}
for ts in session["topics"]

View File

@@ -28,3 +28,21 @@ def backstage(*args: object, **kwargs: Any) -> None:
"""``print`` that renders only backstage (JupyterLab, nbconvert)."""
if not on_stage():
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]

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "discoverylib"
version = "0.1.0"
version = "0.3.0"
description = "CX Exploration & Discovery Workshop — live facilitation aid (Mercury Notebook Pattern)"
requires-python = ">=3.10"
# The notebook is the deliverable (served with Mercury, exported via

View 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()

View File

@@ -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
``topic-bank`` in ``notebooks/cx_discovery.ipynb``; ``.py`` files hold code
only. The fixtures below read that cell with nbformat and exec it, so
pytest pins the exact content the deliverable ships (no kernel needed
the cell is self-contained by contract).
Content and client data live in the notebook, never in ``.py`` the cells
tagged ``topic-bank`` and ``engagement-data`` in
``notebooks/cx_discovery.ipynb``. The fixtures below read those cells with
nbformat and exec them, so pytest pins the exact content the deliverable
ships (no kernel needed tagged cells are self-contained by contract).
The sys.path insert makes discoverylib importable even without the study
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"
@pytest.fixture(scope="session")
def topic_bank() -> dict[str, Any]:
"""The executed namespace of the notebook's topic-bank cell."""
def tagged_cell_ns(tag: str) -> dict[str, Any]:
"""Exec the single cell carrying ``tag`` and return its namespace."""
import nbformat
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, (
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)}"
)
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
@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")
def topics(topic_bank: dict[str, Any]) -> tuple[Any, ...]:
"""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")
def topic_by_key(topics: tuple[Any, ...]) -> dict[str, Any]:
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]

View 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"

View File

@@ -87,10 +87,14 @@ def test_build_session_shape_and_subtopic_done_count(topics, topic_by_key):
assert ch.status == IN_PROGRESS
assert ch.subtopics_done == 2
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
bg = next(ts for ts in s["topics"] if ts.key == "background")
assert bg.status == COMPLETE and bg.subtopics_done == 0
assert bg.subtopics_covered == ()
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),
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["progress"]["completed"] == 1
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"
ch = next(t for t in payload["topics"] if t["key"] == "channels")
assert ch["subtopics_done"] == 1
assert ch["subtopics_covered"] == ["voice_metrics"]
# JSON-serializable (no dataclasses / sets leaked through)
import json

View 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
View File

@@ -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.
Ive 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

View 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.

View File

@@ -690,3 +690,27 @@ All arithmetic uses Python `Decimal` to avoid floating-point drift. Values are s
---
*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 |

View File

@@ -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
Jupyter notebook **is** the artifact — math in a self-contained study package,
Standardizes how Palladium masters — **Studies and Assessments** — ship notebook
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
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.
## 🐾 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
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
[`template/MercuryNotebook/`](../template/MercuryNotebook/) — copy it to start a study;
snippets here are excerpts from it. The full-scale reference implementation is the CTM
Genesys study, [`studies/202607_CTM_GenesysCX/`](../studies/202607_CTM_GenesysCX/).
snippets here are excerpts from it (note: the template still encodes the py-engine
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/
├── docs/ # repo-wide docs (this pattern, brand.md)
├── docs/ # repo-wide docs (the patterns, brand.md)
├── template/
│ └── MercuryNotebook/ # copy-me starting point (runnable)
├── assessments/
│ └── Instrument_Name/ # reusable workshop master (UNDATED), e.g. CX_Discovery_Workshop
└── studies/
├── YYYYMM_TEI_Vendor_Product/ # vendor TEI study, e.g. 202602_TEI_Amazon_Connect
└── 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
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):
```
@@ -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
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
package and imported. *Why:* the package is testable and diffable; notebook cells are
neither. The test suite pins the engine; the notebook only arranges its outputs.
package and imported. *Why:* the package is testable and diffable for *logic*; the
test suite pins the engine; the notebook only arranges its outputs.
```python
# 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)
```
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
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
Layers, from inner to outer — every study ships all four:
Layers, from inner to outer — every master ships layers 14; 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
function; the contracted overlay AND the verbatim record both pinned, so neither can
drift. Compute by hand first, then pin (see

View 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
View 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.

View File

@@ -16,6 +16,7 @@ dependencies = [
"pandas>=2.0",
"plotly>=5.18",
"numpy>=1.26",
"nbformat>=5.9",
]
[project.optional-dependencies]
@@ -27,7 +28,7 @@ palladium = "core.cli.main:main"
[tool.setuptools.packages.find]
include = ["core*", "palladium*"]
exclude = ["tests*", "studies*", "docs*"]
exclude = ["tests*", "studies*", "assessments*", "docs*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
@@ -44,4 +45,5 @@ ignore = ["E501"] # line length handled by formatter
[tool.ruff.lint.per-file-ignores]
"studies/*/notebooks/*.ipynb" = ["E402"]
"assessments/*/notebooks/*.ipynb" = ["E402"]
"tests/*" = ["F401"]

View File

@@ -4,5 +4,6 @@ jupyter>=1.0
pandas>=2.0
plotly>=5.18
numpy>=1.26
nbformat>=5.9
pytest>=7.4
ruff>=0.1

View 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.

View File

@@ -10135,16 +10135,16 @@
"label": "NA — current platform cost ($/yr)",
"layout": "IPY_MODEL_6295cf92344b415598307483934485f3",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
"max": 8000000,
"min": 0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 10000.0,
"step": 10000,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 3348969.0
"value": 3348969
}
},
"53869a7ea78b44f4a134983d354228f0": {
@@ -10382,7 +10382,7 @@
"layout": "IPY_MODEL_0e3be284c70e45b7ad9fe12c3b76163f",
"layout_path": null,
"max": 0.6,
"min": 0.0,
"min": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
@@ -10415,16 +10415,16 @@
"label": "ASIA — current platform cost ($/yr)",
"layout": "IPY_MODEL_61085bdb080e48e186ec10b1abee291a",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
"max": 8000000,
"min": 0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 10000.0,
"step": 10000,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 2069588.0
"value": 2069588
}
},
"61085bdb080e48e186ec10b1abee291a": {
@@ -10555,16 +10555,16 @@
"label": "CCaaS licence run-rate ($/yr) — contracted",
"layout": "IPY_MODEL_3c767a6b472d42c89e6b7baf705fcbbc",
"layout_path": null,
"max": 10000000.0,
"min": 0.0,
"max": 10000000,
"min": 0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 100000.0,
"step": 100000,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 3200000.0
"value": 3200000
}
},
"6ce50fa7157042a4be99b19f24be3ce7": {
@@ -10642,16 +10642,16 @@
"label": "ANZ — current platform cost ($/yr)",
"layout": "IPY_MODEL_54f1095af078476ba27715f8826aa967",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
"max": 8000000,
"min": 0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 10000.0,
"step": 10000,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 677320.0
"value": 677320
}
},
"79ec042073404a6da4ec639b6b44a57c": {
@@ -11267,16 +11267,16 @@
"label": "EMEA — current platform cost ($/yr)",
"layout": "IPY_MODEL_8309c39462b34cc5afad42785a0c53ff",
"layout_path": null,
"max": 8000000.0,
"min": 0.0,
"max": 8000000,
"min": 0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 10000.0,
"step": 10000,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 1204124.0
"value": 1204124
}
},
"d69203bdb8584fc9975e475199fcc235": {
@@ -11301,16 +11301,16 @@
"label": "Genesys ramp — licence-free months",
"layout": "IPY_MODEL_7b92a3a1d4ec49049e22687c38236912",
"layout_path": null,
"max": 24.0,
"min": 0.0,
"max": 24,
"min": 0,
"position": "inline",
"render_slot_id": null,
"source_cell_id": null,
"step": 1.0,
"step": 1,
"tabbable": null,
"tooltip": null,
"url_key": "",
"value": 6.0
"value": 6
}
},
"d7d4d74246ea490da7610c277c802b99": {
@@ -11335,8 +11335,8 @@
"label": "AI Translate eligibility 🟡",
"layout": "IPY_MODEL_1bf396a231374a2097e03bf45fc874ff",
"layout_path": null,
"max": 1.0,
"min": 0.0,
"max": 1,
"min": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,
@@ -11521,7 +11521,7 @@
"layout": "IPY_MODEL_bf7b6f3bca5f48e6838dc54f84943731",
"layout_path": null,
"max": 0.5,
"min": 0.0,
"min": 0,
"position": "sidebar",
"render_slot_id": null,
"source_cell_id": null,

View File

@@ -42,7 +42,7 @@
}
],
"source": [
"# ── Setup ──────────────────────────────────────────────────────────\n",
"# ── Setup ──────────────────────────3rd with just the────────────────────────────────\n",
"import sys, pathlib\n",
"_ROOT = pathlib.Path.cwd()\n",
"if not (_ROOT / \"tokencalc\").exists(): # notebook lives in notebooks/\n",
@@ -8847,7 +8847,7 @@
"disabled": false,
"layout": "IPY_MODEL_d68795b0944341bf8354500e127e4e59",
"max": 0.5,
"min": 0.0,
"min": 0,
"orientation": "horizontal",
"readout": true,
"readout_format": ".0%",
@@ -8940,7 +8940,7 @@
"description_allow_html": false,
"disabled": false,
"layout": "IPY_MODEL_3462a88bfec94f01b6aea7530a431884",
"max": 5.0,
"max": 5,
"min": 0.5,
"orientation": "horizontal",
"readout": true,
@@ -9105,7 +9105,7 @@
"disabled": false,
"layout": "IPY_MODEL_9fe19d91747e4865a104e65b658b7c8e",
"max": 0.7,
"min": 0.0,
"min": 0,
"orientation": "horizontal",
"readout": true,
"readout_format": ".0%",
@@ -9334,7 +9334,7 @@
"description_allow_html": false,
"disabled": false,
"layout": "IPY_MODEL_fd591354d1214f4b9b7d8850be81a511",
"max": 1.0,
"max": 1,
"min": 0.3,
"orientation": "horizontal",
"readout": true,
@@ -9434,7 +9434,7 @@
"description_allow_html": false,
"disabled": false,
"layout": "IPY_MODEL_4c9514e9b96142e9a269035bdd0714c4",
"max": 1.0,
"max": 1,
"min": 0.3,
"orientation": "horizontal",
"readout": true,
@@ -9483,7 +9483,7 @@
"disabled": false,
"layout": "IPY_MODEL_995b326fefa942da94ce18e92254860e",
"max": 0.3,
"min": 0.0,
"min": 0,
"orientation": "horizontal",
"readout": true,
"readout_format": ".0%",

View File

@@ -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()

View File

@@ -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
View 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
View 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}"
)