fix: detect Mercury stage per-view via session name
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
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,6 +9,11 @@
|
|||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Mercury app runtime artifacts — per-session shadow notebook copies
|
||||||
|
# (in an engagement copy these hold live session state: never commit)
|
||||||
|
.mercury_sessions/
|
||||||
|
*__mercury__*
|
||||||
|
|
||||||
# Generated exports — never committed (masters and assessments)
|
# Generated exports — never committed (masters and assessments)
|
||||||
studies/*/exports/*
|
studies/*/exports/*
|
||||||
!studies/*/exports/.gitkeep
|
!studies/*/exports/.gitkeep
|
||||||
|
|||||||
18
CLAUDE.md
18
CLAUDE.md
@@ -78,9 +78,11 @@ there; the repo-root `.venv` (from `make setup`) serves only `core/` and the not
|
|||||||
structural suite. Running a master's pytest from the wrong venv is the classic
|
structural suite. Running a master's pytest from the wrong venv is the classic
|
||||||
"missing module" ghost.
|
"missing module" ghost.
|
||||||
|
|
||||||
- `MERCURY_CONFIG_DIR` present in the environment = **the stage is live** (a client
|
- Stage detection is **per view, not per server**: the primary signal is
|
||||||
may be looking). Its presence is the stage signal `staging.py` keys off — never set
|
`__mercury__` in `JPY_SESSION_NAME` (the app's shadow-copy session);
|
||||||
it manually except to simulate the stage in a test.
|
`MERCURY_CONFIG_DIR` is only a fallback and is set **only** by
|
||||||
|
`mercury --working-dir`. Never set either manually except to simulate the stage
|
||||||
|
in a test (Mercury pattern §6 has the mechanism).
|
||||||
- Only masters live here. **If a notebook in this repo contains a real client's name,
|
- 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).
|
something is wrong** — stop and flag it (see Confidentiality).
|
||||||
|
|
||||||
@@ -219,6 +221,12 @@ quietly leaving it. Live ones worth knowing:
|
|||||||
fix the typing with the redesign, not piecemeal.
|
fix the typing with the redesign, not piecemeal.
|
||||||
- **TEI twins + CTM pre-date the tagged-cell taxonomy** — the structural suite
|
- **TEI twins + CTM pre-date the tagged-cell taxonomy** — the structural suite
|
||||||
grandfathers them by name in `tests/nbcheck.py`, each with its reason.
|
grandfathers them by name in `tests/nbcheck.py`, each with its reason.
|
||||||
|
- **Every master except CX Discovery still carries the env-var-only `staging.py`**
|
||||||
|
(TEI twins, CTM, AI Diagnostic, template) — their `on_stage()` misses the Mercury
|
||||||
|
app unless the server was launched with `--working-dir`, so **their stages leak
|
||||||
|
backstage content** on a plain `mercury` launch (found live 2026-07-31; fixed in
|
||||||
|
the Discovery reference). The retrofit is a recorded opportunity — until it
|
||||||
|
lands, serve those masters only via `mercury --working-dir .`.
|
||||||
- **`template/MercuryNotebook/` encodes only the py-engine model** (and its
|
- **`template/MercuryNotebook/` encodes only the py-engine model** (and its
|
||||||
`staging.py` lacks the mypy-strict `backstage_md` variant); rework is a recorded
|
`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.
|
follow-up. Its `exports/*.{html,md}` are tracked — predates the exports rule.
|
||||||
@@ -237,7 +245,9 @@ quietly leaving it. Live ones worth knowing:
|
|||||||
- TEI + AI Diagnostic redesigns to the notebook-first model (each moves its notebook
|
- TEI + AI Diagnostic redesigns to the notebook-first model (each moves its notebook
|
||||||
from GRANDFATHERED to NOTEBOOK_FIRST in `tests/nbcheck.py`).
|
from GRANDFATHERED to NOTEBOOK_FIRST in `tests/nbcheck.py`).
|
||||||
- Template rework: an Assessment template derived from the Discovery reference;
|
- Template rework: an Assessment template derived from the Discovery reference;
|
||||||
staging.py mypy-strict retrofit across masters.
|
staging.py retrofit across masters (now urgent-ish: brings the corrected
|
||||||
|
shadow-session stage detection, plus mypy-strict + `backstage_md`, to the TEI
|
||||||
|
twins, CTM, AI Diagnostic, and the template).
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
"""
|
"""
|
||||||
Stage vs backstage — is this notebook render stakeholder-facing?
|
Stage vs backstage — is this notebook render stakeholder-facing?
|
||||||
|
|
||||||
The Mercury CLI (``mercury --working-dir …``) exports ``MERCURY_CONFIG_DIR``
|
The Mercury app (3.2.x) is a hybrid Jupyter server: the SAME server (and
|
||||||
into the server process so the widget library can locate ``config.toml``
|
kernel pool) can serve both the client-facing app view and JupyterLab, so a
|
||||||
(see ``mercury/config.py``); every kernel that server spawns inherits it.
|
server-level signal cannot tell who is looking. The reliable, per-view
|
||||||
JupyterLab and nbconvert kernels don't have it. That makes the variable a
|
signal is the session name: the app runs every session against a shadow
|
||||||
reliable signal for "the audience is looking" (the stage) versus an
|
copy named ``<notebook>__mercury__<id>.ipynb``
|
||||||
analyst session or a headless export run (backstage).
|
(``mercury_app/handlers.py``), and the kernel sees that path in
|
||||||
|
``JPY_SESSION_NAME``. JupyterLab and nbconvert sessions carry the plain
|
||||||
|
notebook path (or no session name at all).
|
||||||
|
|
||||||
Diagnostics routed through :func:`backstage` stay visible in JupyterLab
|
``MERCURY_CONFIG_DIR`` is kept as a fallback: ``mercury --working-dir …``
|
||||||
and land in the nbconvert exports (where the machine-readable appendix
|
exports it into the server process and every kernel inherits it. It is a
|
||||||
must appear for LLM consumption) but never render in the Mercury app.
|
server-level signal — on such a server even JupyterLab kernels carry it, so
|
||||||
|
diagnostics are then hidden in that Lab view too (hidden, never leaked; use
|
||||||
|
a separate ``jupyter lab`` for analysis, which is the normal workflow). It
|
||||||
|
is NOT set when mercury is launched without ``--working-dir``, which is why
|
||||||
|
it cannot be the primary signal.
|
||||||
|
|
||||||
|
Diagnostics routed through :func:`backstage` / :func:`backstage_md` stay
|
||||||
|
visible in JupyterLab and land in the nbconvert exports (where the
|
||||||
|
machine-readable appendix must appear for LLM consumption) but never render
|
||||||
|
in the Mercury app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -20,7 +31,9 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
def on_stage() -> bool:
|
def on_stage() -> bool:
|
||||||
"""True when running under the Mercury app (stakeholder-facing)."""
|
"""True when this kernel renders the client-facing Mercury app view."""
|
||||||
|
if "__mercury__" in os.getenv("JPY_SESSION_NAME", ""):
|
||||||
|
return True # the app's shadow-copy session
|
||||||
return os.getenv("MERCURY_CONFIG_DIR") is not None
|
return os.getenv("MERCURY_CONFIG_DIR") is not None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,14 +1,47 @@
|
|||||||
"""Stage/backstage detection — Mercury kernels carry MERCURY_CONFIG_DIR."""
|
"""Stage/backstage detection.
|
||||||
|
|
||||||
|
Two signals mark the Mercury app view (see staging.py): the app's shadow
|
||||||
|
session name (``__mercury__`` in ``JPY_SESSION_NAME`` — per-view, primary)
|
||||||
|
and the ``MERCURY_CONFIG_DIR`` env var (server-level fallback, only set by
|
||||||
|
``mercury --working-dir``). Either one means "a client may be looking".
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from discoverylib import staging
|
from discoverylib import staging
|
||||||
|
|
||||||
|
|
||||||
def test_backstage_prints_only_off_stage(monkeypatch, capsys):
|
@pytest.fixture(autouse=True)
|
||||||
|
def clean_stage_env(monkeypatch):
|
||||||
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
|
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
|
||||||
|
monkeypatch.delenv("JPY_SESSION_NAME", raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backstage_by_default(capsys):
|
||||||
assert not staging.on_stage()
|
assert not staging.on_stage()
|
||||||
staging.backstage("visible")
|
staging.backstage("visible")
|
||||||
assert capsys.readouterr().out == "visible\n"
|
assert capsys.readouterr().out == "visible\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plain_session_name_is_backstage(monkeypatch, capsys):
|
||||||
|
# A JupyterLab or nbconvert session: the real notebook path, no marker.
|
||||||
|
monkeypatch.setenv("JPY_SESSION_NAME", "notebooks/cx_discovery.ipynb")
|
||||||
|
assert not staging.on_stage()
|
||||||
|
staging.backstage("visible")
|
||||||
|
assert capsys.readouterr().out == "visible\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mercury_shadow_session_is_stage(monkeypatch, capsys):
|
||||||
|
# The Mercury app runs against a shadow copy: <stem>__mercury__<id>.ipynb
|
||||||
|
monkeypatch.setenv(
|
||||||
|
"JPY_SESSION_NAME", "notebooks/cx_discovery__mercury__545e5520.ipynb"
|
||||||
|
)
|
||||||
|
assert staging.on_stage()
|
||||||
|
staging.backstage("hidden")
|
||||||
|
assert capsys.readouterr().out == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_dir_fallback_is_stage(monkeypatch, capsys):
|
||||||
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
|
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
|
||||||
assert staging.on_stage()
|
assert staging.on_stage()
|
||||||
staging.backstage("hidden")
|
staging.backstage("hidden")
|
||||||
@@ -18,11 +51,12 @@ def test_backstage_prints_only_off_stage(monkeypatch, capsys):
|
|||||||
def test_backstage_md_renders_only_off_stage(monkeypatch, capsys):
|
def test_backstage_md_renders_only_off_stage(monkeypatch, capsys):
|
||||||
# Off stage it must emit SOMETHING (rich markdown under a kernel;
|
# Off stage it must emit SOMETHING (rich markdown under a kernel;
|
||||||
# IPython's display degrades to print under plain pytest) …
|
# IPython's display degrades to print under plain pytest) …
|
||||||
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
|
|
||||||
staging.backstage_md("**visible**")
|
staging.backstage_md("**visible**")
|
||||||
assert capsys.readouterr().out != ""
|
assert capsys.readouterr().out != ""
|
||||||
|
|
||||||
# … and on stage, nothing at all.
|
# … and on stage — either signal — nothing at all.
|
||||||
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
|
monkeypatch.setenv(
|
||||||
|
"JPY_SESSION_NAME", "notebooks/cx_discovery__mercury__ab12cd34.ipynb"
|
||||||
|
)
|
||||||
staging.backstage_md("**hidden**")
|
staging.backstage_md("**hidden**")
|
||||||
assert capsys.readouterr().out == ""
|
assert capsys.readouterr().out == ""
|
||||||
|
|||||||
@@ -200,14 +200,22 @@ in [`template/MercuryNotebook/notebooks/business_case.ipynb`](../template/Mercur
|
|||||||
### 6 · Stage / backstage
|
### 6 · Stage / backstage
|
||||||
|
|
||||||
The Mercury app is **the stage** — stakeholder-facing. JupyterLab and nbconvert are
|
The Mercury app is **the stage** — stakeholder-facing. JupyterLab and nbconvert are
|
||||||
**backstage** — analyst diagnostics and the data appendix belong there. Detection:
|
**backstage** — analyst diagnostics and the data appendix belong there. Detection
|
||||||
the `mercury` CLI exports `MERCURY_CONFIG_DIR` into its server and every kernel
|
(corrected in v1.1.0 — the old env-var-only check leaked backstage content on real
|
||||||
inherits it; JupyterLab/nbconvert kernels don't have it.
|
serves): the app runs every session against a **shadow copy** named
|
||||||
|
`<notebook>__mercury__<id>.ipynb` (`mercury_app/handlers.py`), and the kernel sees
|
||||||
|
that path in `JPY_SESSION_NAME` — a per-view signal that works however mercury was
|
||||||
|
launched, even on one hybrid server. `MERCURY_CONFIG_DIR` is only a fallback: the
|
||||||
|
CLI exports it **only when `--working-dir` is passed**, it is server-level (a
|
||||||
|
JupyterLab view on that same server inherits it too — diagnostics then hidden,
|
||||||
|
never leaked), and it is absent entirely on a plain `mercury` launch.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# studylib/staging.py — copy verbatim
|
# staging.py — copy verbatim (canonical: the CX Discovery reference)
|
||||||
def on_stage() -> bool:
|
def on_stage() -> bool:
|
||||||
"""True when running under the Mercury app (stakeholder-facing)."""
|
"""True when this kernel renders the client-facing Mercury app view."""
|
||||||
|
if "__mercury__" in os.getenv("JPY_SESSION_NAME", ""):
|
||||||
|
return True # the app's shadow-copy session
|
||||||
return os.getenv("MERCURY_CONFIG_DIR") is not None
|
return os.getenv("MERCURY_CONFIG_DIR") is not None
|
||||||
|
|
||||||
def backstage(*args, **kwargs) -> None:
|
def backstage(*args, **kwargs) -> None:
|
||||||
@@ -432,11 +440,13 @@ Each of these cost a debugging session or a client-facing embarrassment. Don't.
|
|||||||
[theme] # NTT DATA brand palette — see docs/brand.md
|
[theme] # NTT DATA brand palette — see docs/brand.md
|
||||||
```
|
```
|
||||||
|
|
||||||
- `MERCURY_CONFIG_DIR` — set by the `mercury` CLI for its server; kernels inherit it.
|
- `MERCURY_CONFIG_DIR` — set by the `mercury` CLI **only when `--working-dir` is
|
||||||
The pattern uses its **presence** as the stage signal (`studylib/staging.py`). Do not
|
passed**; kernels inherit it. It is the *fallback* stage signal; the primary is
|
||||||
set it manually except to simulate the stage in tests.
|
the `__mercury__` shadow-session name (Required §6). Do not set either manually
|
||||||
- Serve from the project root: `mercury --working-dir notebooks/` (so `config.toml`
|
except to simulate the stage in tests.
|
||||||
loads). Analyst view: `jupyter lab`. Exports: `python scripts/export_report.py`.
|
- Serve from the study root: `mercury --working-dir .` (so `config.toml` loads and
|
||||||
|
the env fallback is armed). Analyst view: a **separate** `jupyter lab`. Exports:
|
||||||
|
`python scripts/export_report.py`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ def discover() -> list[str]:
|
|||||||
for p in root.rglob("*.ipynb"):
|
for p in root.rglob("*.ipynb"):
|
||||||
if ".ipynb_checkpoints" in p.parts or ".venv" in p.parts:
|
if ".ipynb_checkpoints" in p.parts or ".venv" in p.parts:
|
||||||
continue
|
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":
|
if p.parent.name != "notebooks":
|
||||||
continue
|
continue
|
||||||
found.append(p.relative_to(REPO).as_posix())
|
found.append(p.relative_to(REPO).as_posix())
|
||||||
|
|||||||
Reference in New Issue
Block a user