The Mercury app (3.2.x) is a hybrid server whose kernel pool serves both the client-facing app view and JupyterLab, so the server-level MERCURY_CONFIG_DIR signal cannot tell who is looking and leaks backstage content in the app. Switch the primary stage signal to the shadow-copy session name (__mercury__ in JPY_SESSION_NAME), keeping the env var only as a --working-dir fallback. - Update CX Discovery staging.py to the per-view detection model - Ignore Mercury runtime artifacts (.mercury_sessions/, *__mercury__*) - Document the leak in remaining masters (TEI, CTM, AI Diagnostic, template) and flag the staging.py retrofit as urgent
123 lines
4.9 KiB
Python
123 lines
4.9 KiB
Python
"""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
|
|
# Mercury app shadow copies are runtime artifacts, not masters
|
|
# (layout varies by mercury version: .mercury_sessions/ or a
|
|
# __mercury__ suffix beside the notebook).
|
|
if ".mercury_sessions" in p.parts or "__mercury__" in p.name:
|
|
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", []))
|
|
]
|