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:
123
assessments/CX_Discovery_Workshop/scripts/export_report.py
Normal file
123
assessments/CX_Discovery_Workshop/scripts/export_report.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Export the discovery notebook as an LLM-readable report source.
|
||||
|
||||
Executes the notebook ONCE (widget defaults — or whatever you've captured
|
||||
and saved in the notebook), then converts the executed copy twice:
|
||||
|
||||
exports/cx_discovery.html — human-reviewable, full presentation
|
||||
exports/cx_discovery.md — leanest LLM input: presentation-tagged cells
|
||||
(setup, widgets, board) are stripped, a
|
||||
framing preamble is prepended, and the data
|
||||
appendix ends the file with one fenced JSON
|
||||
block — the machine source of truth.
|
||||
|
||||
Engagement identity (client, date, facilitator) is read from the notebook's
|
||||
``engagement-data`` cell and stamped into the preamble.
|
||||
|
||||
Run from the study root: python scripts/export_report.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
NOTEBOOK = ROOT / "notebooks" / "cx_discovery.ipynb"
|
||||
EXPORTS = ROOT / "exports"
|
||||
|
||||
# Cells tagged with any of these never reach the .md export — they are
|
||||
# stage presentation (source and widget-repr noise), not session record.
|
||||
STRIP_TAGS_FROM_MD = '{"presentation"}'
|
||||
|
||||
|
||||
def engagement_data() -> dict[str, Any]:
|
||||
"""Exec the engagement-data cell (same trick as tests/conftest.py)."""
|
||||
import nbformat
|
||||
|
||||
nb = nbformat.read(NOTEBOOK, as_version=4)
|
||||
cells = [c for c in nb.cells
|
||||
if "engagement-data" in c.metadata.get("tags", [])]
|
||||
assert len(cells) == 1, "expected exactly one engagement-data cell"
|
||||
ns: dict[str, Any] = {}
|
||||
exec(compile(cells[0].source, f"{NOTEBOOK.name} [engagement-data]", "exec"), ns)
|
||||
return ns["ENGAGEMENT"] # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def preamble() -> str:
|
||||
eng = engagement_data()
|
||||
who = " · ".join(str(eng[k]).strip()
|
||||
for k in ("client", "workshop_date", "facilitator")
|
||||
if str(eng.get(k, "")).strip())
|
||||
attendees = ", ".join(str(a) for a in eng.get("attendees", ()) or ())
|
||||
if who:
|
||||
line = who + (f" — attendees: {attendees}" if attendees else "")
|
||||
else:
|
||||
line = "master copy — placeholders; no engagement captured."
|
||||
return "\n".join([
|
||||
"<!-- Export preamble — generated by scripts/export_report.py -->",
|
||||
"**What this is** — the exported record of a CX Exploration & Discovery",
|
||||
"workshop session, produced from the Mercury-served notebook that ran the",
|
||||
"session. It is LLM input for drafting the survey write-up or seeding a",
|
||||
"business case.",
|
||||
"",
|
||||
f"**Engagement** — {line}",
|
||||
"",
|
||||
"**How to read it** — first the workshop content as annotated source (the",
|
||||
"topic bank, then the engagement data), then the facilitation script",
|
||||
"(every prompt, grouped by topic and sub-topic), the verification gate,",
|
||||
"and finally the captured-session record: the per-topic table (status,",
|
||||
"covered sub-topics, notes) and — last — **Session state (JSON)**, one",
|
||||
"fenced `json` block. Where prose and JSON disagree, the JSON block is",
|
||||
"the source of truth.",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"",
|
||||
])
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) > 1 and sys.argv[1] not in NOTEBOOK.name:
|
||||
sys.exit(f"no notebook matches {sys.argv[1]!r}")
|
||||
EXPORTS.mkdir(exist_ok=True)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
executed = Path(tmp) / NOTEBOOK.name
|
||||
# 1. Execute once — both formats convert the same session state.
|
||||
# (Never combine --execute with TagRemovePreprocessor in one call:
|
||||
# the cell could be stripped before it runs.)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "nbconvert", "--execute",
|
||||
"--to", "notebook", "--output", str(executed), str(NOTEBOOK)],
|
||||
check=True, cwd=ROOT,
|
||||
)
|
||||
# 2. HTML — full presentation, human review artifact.
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "nbconvert", "--to", "html",
|
||||
"--output-dir", str(EXPORTS), "--output", NOTEBOOK.stem,
|
||||
str(executed)],
|
||||
check=True, cwd=ROOT,
|
||||
)
|
||||
# 3. Markdown — LLM artifact: strip presentation cells.
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "nbconvert", "--to", "markdown",
|
||||
"--output-dir", str(EXPORTS), "--output", NOTEBOOK.stem,
|
||||
"--TagRemovePreprocessor.enabled=True",
|
||||
f"--TagRemovePreprocessor.remove_cell_tags={STRIP_TAGS_FROM_MD}",
|
||||
str(executed)],
|
||||
check=True, cwd=ROOT,
|
||||
)
|
||||
|
||||
# 4. Prepend the framing preamble to the markdown export.
|
||||
md = EXPORTS / f"{NOTEBOOK.stem}.md"
|
||||
md.write_text(preamble() + md.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
|
||||
for p in sorted(EXPORTS.iterdir()):
|
||||
if p.suffix in (".html", ".md"):
|
||||
print(f"wrote {p.relative_to(ROOT)} ({p.stat().st_size / 1024:,.0f} KB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user