"""Handbook substitution vocabulary, built per-render from an L2 instance.
BXa.1 (2026-05-30) collapsed the prior Sasquatch-specific intercept
into a single ``vocabulary_for_l2`` builder. The institution name +
acronym + curated investigation personas now live on the L2 YAML as
top-level ``institution_name`` / ``institution_acronym`` /
``investigation_personas`` fields; the deleted persona block's other
slots (stakeholders, merchants, gl_accounts, flavor) were doubly
dead — bypassed by the intercept AND not substituted in any docs
page — so they went with the intercept.
The neutral fallback (regex-extracted institution name + "Your
Institution" placeholder) survives for L2 YAMLs that omit
``institution_name``. Empty `investigation_personas` is the same
"no curated walkthrough content" signal the old empty-persona path
carried — the handbook's ``{% if vocab.demo.investigation.layering_chain %}``
gates already hide the walkthrough sections when no curated
narrative is available.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from recon_gen.common.l2.primitives import (
InvestigationPersona,
L2Instance,
)
# -- Sub-shapes -------------------------------------------------------------
[docs]
@dataclass(frozen=True)
class InstitutionVocabulary:
"""How the handbook refers to the institution."""
name: str
"""Full name — ``"Sasquatch National Bank"`` / ``"Your Institution"``."""
acronym: str
"""Short name — ``"SNB"`` / ``"the bank"``."""
description: str
"""One-paragraph intro for handbook landing pages."""
[docs]
@dataclass(frozen=True)
class DemoAccount:
"""An account demonstrated by a planted scenario.
``id`` is the runtime account id (joins to the seed). ``name`` is a
human-readable label for prose — falls back to the id when no
description is available on the L2 account.
"""
id: str
name: str
[docs]
@dataclass(frozen=True)
class InvestigationScenarioVocabulary:
"""The Investigation app's planted scenario, normalized for prose.
Only populated when the active L2 has at least one
``inv_fanout_plants`` entry; otherwise the parent
``DemoScenarioVocabulary.investigation`` is ``None`` and the
Investigation worked-example admonitions don't render.
"""
anchor: DemoAccount
"""The recipient account the fanout converges on."""
fanout_sender_count: int
"""Number of distinct senders in the recipient fanout."""
layering_chain: tuple[DemoAccount, ...] = ()
"""Optional shell-DDA layering chain (Sasquatch's Shell A-B-C); empty for L2s without a hand-curated layering scenario."""
anomaly_pair_sender: DemoAccount | None = None
"""Optional sender of the volume-anomaly spike (Sasquatch's Cascadia Trust Bank — Operations); None for L2s without one."""
[docs]
@dataclass(frozen=True)
class DemoScenarioVocabulary:
"""Plant-derived demo content for walkthrough worked examples.
Each field is ``None`` when the active L2 has no matching plants.
Walkthrough templates use ``{% if vocab.demo.X %}...{% endif %}``
to hide their worked-example sections when the field is None.
"""
drift_account: DemoAccount | None = None
overdraft_account: DemoAccount | None = None
limit_breach_account: DemoAccount | None = None
investigation: InvestigationScenarioVocabulary | None = None
@property
def has_investigation_plants(self) -> bool:
return self.investigation is not None
# -- Top-level ---------------------------------------------------------------
[docs]
@dataclass(frozen=True)
class HandbookVocabulary:
"""Substitution vocabulary handed to mkdocs-macros at render time.
BXa.1 (2026-05-30) trimmed ``stakeholders`` / ``merchants`` /
``gl_accounts`` / ``flavor`` — never substituted in any rendered
page; the hardcoded production-code values that backed them
(``StakeholderVocabulary`` + ``MerchantVocabulary`` tables) went
with them. If a future custom-prose template needs per-institution
counterparty / merchant narrative, add a new top-level
``L2Instance`` field + extend this dataclass; don't resurrect
the hardcoded vocab path.
"""
institution: InstitutionVocabulary
investigation_personas: tuple[InvestigationPersona, ...]
fixture_name: str | None = None
"""Bundled fixture name (e.g. ``"sasquatch_pr"``) when the active L2 IS a bundled fixture; ``None`` for integrator-supplied YAMLs. Used by handbook prose that references "the bundled <X> fixture"."""
demo: DemoScenarioVocabulary = field(default_factory=DemoScenarioVocabulary)
"""Plant-derived demo accounts + Investigation scenario for walkthrough worked examples."""
# -- Public entry point ------------------------------------------------------
# BXa.1: kept as the one remaining hardcoded discriminator in
# production code. The Sasquatch fixture's L2 yaml declares
# ``institution_acronym: SNB``; this maps it to the bundled fixture
# name docs/L1_Invariants.md gates on. If a customer forks the
# fixture + changes the acronym, fixture_name becomes None and the
# docs render the neutral prose branch.
_BUNDLED_FIXTURE_ACRONYM_MAP: dict[str, str] = {
"SNB": "sasquatch_pr",
}
[docs]
def vocabulary_for(l2_instance: L2Instance) -> HandbookVocabulary:
"""Return the handbook vocabulary for ``l2_instance``.
Single dispatch path post-BXa.1. Institution name + acronym +
investigation-persona curated narrative all come from the L2 YAML
directly; no special-case branches for bundled fixtures vs operator
L2s.
"""
return vocabulary_for_l2(l2_instance)
def _bundled_fixture_name(l2_instance: L2Instance) -> str | None:
"""Return the bundled-fixture name when the L2's acronym maps to one;
else None.
The handbook's ``L1_Invariants.md`` gates on
``{% if vocab.fixture_name == "sasquatch_pr" %}`` to render
Sasquatch-specific concrete examples. The map is the one
remaining production-code literal tying the docs surface to a
fixture name; a future "drop bundled-fixture-specific docs"
pass eliminates it entirely.
"""
if l2_instance.institution_acronym is None:
return None
return _BUNDLED_FIXTURE_ACRONYM_MAP.get(l2_instance.institution_acronym)
def _build_demo_scenario(
l2_instance: L2Instance,
) -> DemoScenarioVocabulary:
"""Derive ``DemoScenarioVocabulary`` from the L2's planted scenarios.
Reads ``default_scenario_for(l2_instance)`` and pulls the FIRST
plant of each kind (drift/overdraft/limit_breach/inv_fanout). Each
plant's ``account_id`` is resolved to a ``DemoAccount`` via the
L2's own ``investigation_personas`` (when set) for curated display
names, falling back to the account roster's description, then to
the raw id.
"""
# Lazy import — auto_scenario imports a lot, and we don't want
# vocabulary_for() to drag the seed pipeline in for callers that
# only need the institution name.
from recon_gen.common.l2.auto_scenario import default_scenario_for
persona_lookup = {
p.account_id: p.name for p in l2_instance.investigation_personas
}
def _account_name(account_id: str) -> str:
"""Resolve a runtime account id to a human-readable name."""
if account_id in persona_lookup:
return persona_lookup[account_id]
for acc in l2_instance.accounts:
if str(acc.id) == account_id:
return (
acc.description.split(".")[0].strip()
if acc.description
else account_id
)
return account_id
def _to_demo_account(account_id: str) -> DemoAccount:
return DemoAccount(id=account_id, name=_account_name(account_id))
report = default_scenario_for(l2_instance)
plant = report.scenario
drift = (
_to_demo_account(str(plant.drift_plants[0].account_id))
if plant.drift_plants else None
)
overdraft = (
_to_demo_account(str(plant.overdraft_plants[0].account_id))
if plant.overdraft_plants else None
)
limit_breach = (
_to_demo_account(str(plant.limit_breach_plants[0].account_id))
if plant.limit_breach_plants else None
)
investigation: InvestigationScenarioVocabulary | None = None
if plant.inv_fanout_plants:
# Curated personas (when present in the L2) override the raw
# plant data — the bundled fixture's seed plants a smaller raw
# fanout than the demo CLI eventually deploys, but the
# hand-curated narrative (Juniper at 12 senders + Cascadia
# spike + Shell A-B-C layering chain) is what the walkthroughs
# are written around. For integrator L2s with no curated
# personas, fall back to the first plant.
curated_anchor = next(
(
_to_demo_account(p.account_id)
for p in l2_instance.investigation_personas
if p.role == "convergence_anchor"
),
None,
)
first = plant.inv_fanout_plants[0]
if curated_anchor is not None:
anchor = curated_anchor
fanout_sender_count = 12
else:
anchor = _to_demo_account(str(first.recipient_account_id))
fanout_sender_count = len(first.sender_account_ids)
chain = tuple(
_to_demo_account(p.account_id)
for p in l2_instance.investigation_personas
if p.role == "shell_entity"
)
anomaly_sender = next(
(
_to_demo_account(p.account_id)
for p in l2_instance.investigation_personas
if p.role == "operations_account"
),
None,
)
investigation = InvestigationScenarioVocabulary(
anchor=anchor,
fanout_sender_count=fanout_sender_count,
layering_chain=chain,
anomaly_pair_sender=anomaly_sender,
)
return DemoScenarioVocabulary(
drift_account=drift,
overdraft_account=overdraft,
limit_breach_account=limit_breach,
investigation=investigation,
)
# -- Single builder (post-BXa.1) --------------------------------------------
_INSTITUTION_NAME_RE = re.compile(
# First strict-title-case run (≥2 words) in the description's first
# sentence — e.g. "Sasquatch National Bank's combined treasury…" →
# "Sasquatch National Bank". Strict title case (uppercase initial +
# lowercase remainder) so all-caps tokens don't false-match: in
# ``spec_example`` the description opens with "Generic SPEC-shaped
# instance…", and we want that to fall through to the placeholder
# rather than emit "Generic SPEC". Real bank names with all-caps
# tokens ("BMO Harris", "PNC Financial") need to set
# ``institution_name`` on the L2 explicitly — the regex isn't
# trying to cover the long tail.
r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,4})\b"
)
[docs]
def vocabulary_for_l2(l2_instance: L2Instance) -> HandbookVocabulary:
"""Build the handbook vocabulary from the L2 instance directly.
Precedence for the institution name:
1. ``l2_instance.institution_name`` if set (the BXa.1 promoted field
that operator L2s declare directly).
2. Regex-extracted from the L2's description (the neutral fallback
for L2s that omit ``institution_name``).
3. ``"Your Institution"`` placeholder (when no description either).
Same precedence for acronym (explicit field → derived-from-name →
``"the institution"`` placeholder).
"""
description = (
l2_instance.description.strip()
if l2_instance.description
else "An L2-fed institution — handbook generated from the L2 YAML."
)
if l2_instance.institution_name is not None:
name = l2_instance.institution_name
else:
name = _extract_institution_name(description)
if l2_instance.institution_acronym is not None:
acronym = l2_instance.institution_acronym
else:
acronym = _institution_acronym(name)
return HandbookVocabulary(
institution=InstitutionVocabulary(
name=name,
acronym=acronym,
description=description,
),
investigation_personas=l2_instance.investigation_personas,
fixture_name=_bundled_fixture_name(l2_instance),
demo=_build_demo_scenario(l2_instance),
)
def _extract_institution_name(description: str) -> str:
"""Pull a proper-noun-shaped institution name out of a description.
Returns ``"Your Institution"`` when no candidate is found, so the
handbook reads sensibly for L2 instances whose descriptions are
test-shaped or otherwise lack a proper-noun run.
"""
first_sentence = description.split(".", 1)[0]
match = _INSTITUTION_NAME_RE.search(first_sentence)
if match is None:
return "Your Institution"
return match.group(1)
def _institution_acronym(name: str) -> str:
"""Make a 2-4 letter acronym from a multi-word institution name.
Single-word names (or the ``"Your Institution"`` fallback) collapse
to ``"the institution"`` for readable inline prose.
"""
words = [w for w in name.split() if w[0].isupper()]
if len(words) < 2 or name == "Your Institution":
return "the institution"
return "".join(w[0] for w in words)