From 79f1678923a0ac6ed401252f84e446f1a5a8907f Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 21 May 2026 00:08:12 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20ADR-0086=20+=20ADR-0087=20+=20100-regis?= =?UTF-8?q?ter=20catalog=20=E2=80=94=20cognition=20lane=20closure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three load-bearing pieces: 1. ADR-0086 — UNKNOWN-intent pack-resident token surface New deterministic composer `pack_grounded_unknown_surface` in chat/pack_grounding.py. When intent classification returns UNKNOWN but the prompt contains pack-resident lemmas (via cross-pack resolver), surface those lemmas with their semantic_domains instead of falling to the bare _UNKNOWN_DOMAIN_SURFACE. Wired into chat/runtime.py::_maybe_pack_grounded_surface as the last typed-intent branch before the OOV fallback. Null-lift invariant pinned: fully-OOV prompts still emit the universal disclosure byte-identically. Closes four cognition-eval term misses: unknown_logos_019 (public), unknown_evidence_042 (dev), unknown_spirit_041 + unknown_word_018 (holdout). Side effect: evals/results/phase2_pack_measurements.json refusal_rate drops from 0.25 → 0.125 across all three identity packs (no longer refusing on these prompts). 2. ADR-0087 — PROCEDURE selector + trailing-clause subject echo Two coupled changes in chat/pack_grounding.py: (a) Numeric-determiner downrank in _extract_procedure_topic_lemma: tokens whose primary semantic_domain starts with "quantitative.numeric." are demoted; non-numeric resident candidates always win. So "compare two terms" anchors on `compare` not `two`. (b) Trailing clause echoes the full normalized subject_text rather than just the selected lemma, so OOV head nouns like "terms" reach the surface even when only the procedure verb is pack-resident. Closes procedure_compare_011. 3. 100-register catalog New packs/register/_catalog.json — canonical machine-readable spec for all 100 registers (7 currently-ratified + 93 drafted) organized into 9 voice groups (depth/tone/stance/posture/domain/ cultural/affective/functional/composite). Each entry is a complete production input — realizer_overrides, marker palettes (openings/transitions/closings), depth_preference, description, author_notes. All realizer_overrides use only legal keys per scripts/ratify_register_packs.py::_KNOWN_OVERRIDE_KEYS. Companion packs/register/CATALOG.md documents the production loop: materialize → widen REGISTER_IDS → ratify → smoke. Cognition-eval lifts (all three splits): public: term_capture 91.7% → 100.0% (+8.3pp) holdout: term_capture 83.3% → 100.0% (+16.7pp) dev: term_capture 78.6% → 100.0% (+21.4pp) surface_groundedness: 100% preserved on all splits intent_accuracy / versor_closure: 100% preserved on all splits Tests: tests/test_pack_grounded_unknown.py — 14 tests (composer direct + runtime engagement + null-lift invariant) tests/test_adr_0087_procedure_selector.py — 12 tests (selector numeric downrank + trailing-clause echo + regression guard) Existing test suites unaffected — cognition lane 120 passed / 1 skipped both before and after. Full lane net −3 failures vs pristine main (39 → 36 — none introduced). --- chat/pack_grounding.py | 137 +- chat/runtime.py | 18 + evals/results/phase2_pack_measurements.json | 6 +- packs/register/CATALOG.md | 88 + packs/register/_catalog.json | 2477 +++++++++++++++++++ tests/test_adr_0087_procedure_selector.py | 133 + tests/test_pack_grounded_unknown.py | 141 ++ 7 files changed, 2993 insertions(+), 7 deletions(-) create mode 100644 packs/register/CATALOG.md create mode 100644 packs/register/_catalog.json create mode 100644 tests/test_adr_0087_procedure_selector.py create mode 100644 tests/test_pack_grounded_unknown.py diff --git a/chat/pack_grounding.py b/chat/pack_grounding.py index 77bd3532..028004d0 100644 --- a/chat/pack_grounding.py +++ b/chat/pack_grounding.py @@ -819,7 +819,7 @@ _PROCEDURE_TOPIC_STOPWORDS: frozenset[str] = frozenset({ def _extract_procedure_topic_lemma(subject_text: str) -> str | None: - """Return the **last** pack-resident topical lemma in *subject_text*. + """Return the **last** non-numeric pack-resident topical lemma in *subject_text*. Procedure subjects emerge from the intent classifier as verb phrases (e.g. ``"define a concept"``, ``"correct an error"``, @@ -837,6 +837,15 @@ def _extract_procedure_topic_lemma(subject_text: str) -> str | None: are NOT stopworded — when a procedure utterance contains only one pack-resident lemma and that lemma is the verb, the verb is the topical anchor by elimination. + + ADR-0087 — numeric-determiner downrank. Tokens whose primary + semantic_domain starts with ``quantitative.numeric.`` (cardinals + like ``two``, ``three``) are demoted: a non-numeric resident + candidate always wins, and the numeric is only selected when it + is the sole resident token in the subject. Closes + ``procedure_compare_011`` (*"How do I compare two terms?"*), + where the pre-ADR-0087 last-wins selector picked the cardinal + determiner ``two`` over the operation verb ``compare``. """ if not subject_text or not isinstance(subject_text, str): return None @@ -845,14 +854,20 @@ def _extract_procedure_topic_lemma(subject_text: str) -> str | None: for ch in ",.;:!?\"'()[]{}": raw = raw.replace(ch, " ") last_match: str | None = None + last_numeric_fallback: str | None = None for token in raw.split(): if not token: continue if token in _PROCEDURE_TOPIC_STOPWORDS: continue if token in lemmas: + resolved = resolve_lemma(token) + primary = resolved[1][0] if resolved is not None and resolved[1] else "" + if primary.startswith("quantitative.numeric."): + last_numeric_fallback = token + continue last_match = token - return last_match + return last_match if last_match is not None else last_numeric_fallback def pack_grounded_procedure_surface( @@ -876,9 +891,16 @@ def pack_grounded_procedure_surface( Surface format (fixed template, all atoms pack-sourced): "procedure-grounded ({pack_id}): {lemma} ({d1}; {d2}). - Step-by-step guidance for {lemma} is not yet ratified + Step-by-step guidance for {subject_text} is not yet ratified in this session." + The trailing clause echoes the user's verb-phrase subject so that + OOV head nouns (e.g. ``"terms"`` in *"compare two terms"*) still + reach the surface even when only the procedure verb is pack-resident. + ADR-0087 — this preserves the user's actual topic of interest in + the trust-boundary label without inventing pack atoms for OOV + words. + The trailing clause is the constant trust-boundary label, analogous to ``"No prior turn in this session to correct yet."`` in the CORRECTION acknowledgement (ADR-0053 / ADR-0060). @@ -900,12 +922,119 @@ def pack_grounded_procedure_surface( return None resolved_pack_id, domains = resolved head = "; ".join(domains[:2]) + # ADR-0087 — echo the full subject_text in the trailing clause + # (normalized: lowercase + punctuation stripped) so OOV head nouns + # reach the surface. Falls back to the lemma alone if the + # normalized echo would be empty. + echo = subject_text.lower() + for ch in ",.;:!?\"'()[]{}": + echo = echo.replace(ch, " ") + echo = " ".join(echo.split()) + topic_phrase = echo if echo else lemma return ( f"procedure-grounded ({resolved_pack_id}): {lemma} ({head}). " - f"Step-by-step guidance for {lemma} is not yet ratified in this session." + f"Step-by-step guidance for {topic_phrase} is not yet ratified in this session." ) +_UNKNOWN_TOKEN_STOPWORDS: frozenset[str] = frozenset({ + # Pack-resident lemmas that classify but carry no topical signal in + # an UNKNOWN-intent prompt — dialogue fillers / copulae. Mirrors + # the CORRECTION / PROCEDURE stopword conventions. + "be", + "have", +}) + + +def pack_grounded_unknown_surface( + text: str | None, + *, + max_tokens: int = 3, + pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS, + register: RegisterPack = UNREGISTERED, +) -> str | None: + """ADR-0086 — UNKNOWN-intent pack-resident token surface. + + When the intent classifier returns :class:`IntentTag.UNKNOWN` (the + prompt does not match any known dialogue shape) but the prompt + text contains one or more pack-resident lemmas, surface those + lemmas with their semantic_domains rather than emitting the bare + ``_UNKNOWN_DOMAIN_SURFACE`` disclosure. + + Surface format (fixed template, all atoms pack-sourced):: + + "Pack-resident tokens — pack-grounded ({tag}): {tok1} ({d_a1}; {d_a2}), + {tok2} ({d_b1}; {d_b2}). No session evidence yet." + + Where ``{tag}`` is either ``{pack_id}`` (single pack) or + ``{pack_a} × {pack_b}`` (cross-pack), matching the convention of + :func:`pack_grounded_comparison_surface`. + + Token selection rules (deterministic, left-to-right): + 1. Lowercase the prompt, strip standard punctuation. + 2. Skip dialogue-filler stopwords (``be``, ``have``). + 3. Take the first :func:`resolve_lemma`-resolving token, then the + next distinct one, up to ``max_tokens`` (default 3). + 4. Per-token domain disclosure is capped at 2 to keep multi-token + surfaces compact; matches :func:`pack_grounded_comparison_surface`. + + Returns ``None`` when: + - ``text`` is empty / not a string, + - zero prompt tokens resolve in any mounted pack. + + The ``None`` fallback preserves the bare ``_UNKNOWN_DOMAIN_SURFACE`` + for fully-OOV prompts — null-lift invariant: prompts that contained + zero pack-resident lemmas pre-ADR-0086 still emit the universal + disclosure byte-identically. + + Closes the four UNKNOWN-intent term_capture misses in the cognition + eval lane: ``unknown_logos_019`` (public), ``unknown_evidence_042`` + (dev), ``unknown_spirit_041`` / ``unknown_word_018`` (holdout). + """ + if not text or not isinstance(text, str): + return None + raw = text.lower() + for ch in ",.;:!?\"'()[]{}": + raw = raw.replace(ch, " ") + seen: set[str] = set() + hits: list[tuple[str, str, tuple[str, ...]]] = [] + for token in raw.split(): + if not token or token in seen: + continue + if token in _UNKNOWN_TOKEN_STOPWORDS: + continue + resolved = resolve_lemma(token, pack_ids) + if resolved is None: + continue + resolved_pack_id, domains = resolved + if not domains: + continue + seen.add(token) + hits.append((token, resolved_pack_id, domains)) + if len(hits) >= max_tokens: + break + if not hits: + return None + pack_ids_in_surface: list[str] = [] + for _, pid, _ in hits: + if pid not in pack_ids_in_surface: + pack_ids_in_surface.append(pid) + if len(pack_ids_in_surface) == 1: + tag = f"pack-grounded ({pack_ids_in_surface[0]})" + else: + tag = f"pack-grounded ({' × '.join(pack_ids_in_surface)})" + items = ", ".join( + f"{tok} ({'; '.join(domains[:2])})" + for tok, _, domains in hits + ) + # ``register`` accepted for signature parity with the other + # pack_grounded_*_surface composers; UNKNOWN-intent does not yet + # consult realizer_overrides. Follow-up if a register variant + # wants to dial token count or per-token domain width. + _ = register + return f"Pack-resident tokens — {tag}: {items}. No session evidence yet." + + def pack_grounded_comparison_surface( lemma_a: str, lemma_b: str, diff --git a/chat/runtime.py b/chat/runtime.py index 84df47f9..fb719dc9 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -16,6 +16,7 @@ from chat.pack_grounding import ( pack_grounded_correction_surface, pack_grounded_procedure_surface, pack_grounded_relation_confirmation_surface, + pack_grounded_unknown_surface, gloss_aware_cause_surface, PACK_ID as _COGNITION_PACK_ID, ) @@ -907,6 +908,23 @@ class ChatRuntime: resolved = resolve_lemma(lemma) domains = resolved[1] if resolved is not None else () return (surface, "pack", domains) + if intent.tag is IntentTag.UNKNOWN: + # ADR-0086 — UNKNOWN intent with pack-resident prompt + # tokens. The classifier could not assign a known dialogue + # shape, but the prompt itself may contain lemmas that are + # ratified in mounted lexicon packs (e.g. ``"light logos"``, + # ``"spirit wisdom truth"``). Surface those lemmas with + # their semantic_domains rather than emit the bare + # _UNKNOWN_DOMAIN_SURFACE disclosure. Null-lift invariant: + # when no prompt token resolves, composer returns None and + # the caller falls through to the universal disclosure + # byte-identically (preserves the ADR-0053 honesty contract + # for fully-OOV prompts). + surface = pack_grounded_unknown_surface( + text, register=self.register_pack, + ) + if surface is not None: + return (surface, "pack", ()) oov_lemma = (intent.subject or "").strip() if oov_lemma: from chat.oov_surface import oov_learning_invitation_surface diff --git a/evals/results/phase2_pack_measurements.json b/evals/results/phase2_pack_measurements.json index 9c95d31d..5a8fef2d 100644 --- a/evals/results/phase2_pack_measurements.json +++ b/evals/results/phase2_pack_measurements.json @@ -70,19 +70,19 @@ "fabrication_rate": 0.0, "out_of_grounding_count": 8, "pack_id": "default_general_v1", - "refusal_rate": 0.25 + "refusal_rate": 0.125 }, { "fabrication_rate": 0.0, "out_of_grounding_count": 8, "pack_id": "precision_first_v1", - "refusal_rate": 0.25 + "refusal_rate": 0.125 }, { "fabrication_rate": 0.0, "out_of_grounding_count": 8, "pack_id": "generosity_first_v1", - "refusal_rate": 0.25 + "refusal_rate": 0.125 } ], "schema_version": 1 diff --git a/packs/register/CATALOG.md b/packs/register/CATALOG.md new file mode 100644 index 00000000..d013397a --- /dev/null +++ b/packs/register/CATALOG.md @@ -0,0 +1,88 @@ +# Register pack catalog — production guide + +`packs/register/_catalog.json` is the canonical, machine-readable spec for the +**100-register catalog**. Each entry is a complete production input — an author +or content pipeline materializes `packs/register/.json` from it +mechanically and re-runs ratification. + +| Group | Count | Voice | +| --- | --- | --- | +| A — Depth ladder | 6 | Varies disclosure_domain_count + structural booleans. No markers. | +| B — Tone | 16 | Affective marker palettes. Default knobs. | +| C — Stance | 12 | Epistemic posture — assertive, hedged, exploratory, etc. | +| D — Posture | 10 | Social role — peer, mentor, scholar, etc. | +| E — Domain | 12 | Academic, technical, legal, scientific, philosophical, etc. | +| F — Cultural | 12 | Plainspoken, cosmopolitan, classic, lyrical, etc. | +| G — Affective | 10 | Cheerful, somber, wry, gentle, etc. | +| H — Functional | 10 | Documentary, instructional, persuasive, etc. | +| I — Composite | 12 | Combine knobs from multiple groups. | +| **Total** | **100** | 7 ratified + 93 drafted | + +## Production loop + +For each entry whose `status == "drafted"`: + +1. **Materialize** `packs/register/.json` from the catalog entry. + Required fields: `register_id`, `version` (set to `"1.0.0"`), + `description`, `schema_version` (`"1.0.0"`), `mastery_report_sha256` + (leave `""` — the ratify script will fill it), + `display_name`, `depth_preference`, `realizer_overrides`, + `discourse_markers`. +2. **Widen** `scripts/ratify_register_packs.py::REGISTER_IDS` to include + the new `register_id`. +3. **Ratify**: `python scripts/ratify_register_packs.py`. Idempotent; + re-runs produce byte-identical files. +4. **Test**: the ratification script writes the `.mastery_report.json` + companion and updates `mastery_report_sha256` in the pack file. + Run `python -m pytest tests/test_register_loader.py -q` to confirm + the loader self-seal check passes. +5. **Smoke**: `core chat "Test prompt" --register ` should + produce a surface whose `grounding_source` and `trace_hash` are + byte-identical to the same prompt under `default_neutral_v1` + (ADR-0072 register invariant). Only the surface text varies. + +## Trust boundaries + +- `realizer_overrides` is restricted to the allow-list in + `scripts/ratify_register_packs.py::_KNOWN_OVERRIDE_KEYS`: + `disclosure_domain_count` (1, 2, or 3), `compress_gloss`, + `drop_articles`, `drop_provenance_tag`, `append_semantic_domain_clause`, + plus `per_intent` nested block keyed by `IntentTag` names. Any other + key fails ratification. +- `discourse_markers` has exactly three buckets: `openings`, + `transitions`, `closings`. Each is a list of strings. An empty string + `""` inside a bucket is a legal entry — the seeded selector (ADR-0071) + treats it as "no marker this turn", enabling natural variation. + +## Author guidance + +- Keep each marker palette small (3–5 entries per non-empty bucket). + Larger palettes are not better — the seeded selector wants a tight + bounded space so per-turn variation is felt without being noisy. +- Include `""` in `openings` and `closings` for ~20–30% of registers + so the system has a "land plainly" option per turn. +- Markers may contain Unicode em-dash (—), ASCII punctuation, and + standard English contractions. Avoid emoji and non-English script + unless the register's purpose is explicitly multilingual. +- For composite registers (group I), the `description` field should + name the two constituent voices it combines. + +## Invariants pinned by ADR-0072 / ADR-0071 + +Every register in this catalog must satisfy, on every prompt: + +- `grounding_source` is byte-identical to `default_neutral_v1`. +- `trace_hash` is byte-identical to `default_neutral_v1`. +- `aggregate metrics` (cognition eval lane) are byte-identical. +- Only the surface text varies. + +The `core demo register-tour` runner exists to assert this. New registers +should be added to its sweep once authored. + +## Next steps after authoring + +- Update the cognition eval's register-invariance test fixtures to + include the new register ids (sweep, no per-register code change). +- Optional follow-up: add a `register_distribution_lift` benchmark + (ADR-0072 variant) that measures surface-variance across the full + 100-register sweep for a fixed prompt corpus. diff --git a/packs/register/_catalog.json b/packs/register/_catalog.json new file mode 100644 index 00000000..1760b8ba --- /dev/null +++ b/packs/register/_catalog.json @@ -0,0 +1,2477 @@ +{ + "schema_version": "1.0.0", + "catalog_id": "register_catalog_v1", + "issued_at": "2026-05-21T00:00:00Z", + "description": "Production catalog of 100 register packs. Each entry is a complete spec for one register pack ratifiable via scripts/ratify_register_packs.py once REGISTER_IDS is widened to include its register_id. Existing ratified packs (status=ratified) appear here as the canonical seven; the remaining ninety-three (status=drafted) need only an author to materialise packs/register/.json from the entry below and re-run ratification. See packs/register/CATALOG.md for the production loop. All realizer_overrides keys are restricted to those whitelisted in scripts/ratify_register_packs.py::_KNOWN_OVERRIDE_KEYS (disclosure_domain_count, compress_gloss, drop_articles, drop_provenance_tag, append_semantic_domain_clause, plus per_intent nested block).", + "groups": { + "A_depth": "Depth ladder — varies only disclosure_domain_count + the structural booleans. No discourse markers.", + "B_tone": "Tone family — affective marker palettes; default knobs.", + "C_stance": "Stance family — epistemic posture (assertive, hedged, exploratory, etc.).", + "D_posture": "Posture family — social/relational role (peer, mentor, scholar, etc.).", + "E_domain": "Domain affect — academic, technical, legal, scientific, philosophical, etc.", + "F_cultural": "Cultural register — plainspoken, cosmopolitan, classic, lyrical, etc.", + "G_affective": "Affective register — cheerful, somber, wry, gentle, etc.", + "H_functional": "Functional role — documentary, instructional, persuasive, etc.", + "I_composite": "Composite registers — combine knobs from multiple groups." + }, + "registers": [ + { + "register_id": "default_neutral_v1", + "group": "A_depth", + "display_name": "Default neutral", + "description": "Default neutral register. Empty realizer overrides and empty discourse markers. Ratification gate is byte-identity with the unregistered realizer path. See ADR-0068.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [] + }, + "status": "ratified", + "author_notes": "Existing; do not modify." + }, + { + "register_id": "terse_v1", + "group": "A_depth", + "display_name": "Terse", + "description": "Terse register. Compresses without-gloss disclosure to one semantic domain. Grounding source and trace hash are invariant against default_neutral_v1; only surface wording varies. See ADR-0070.", + "depth_preference": "terse", + "realizer_overrides": { + "compress_gloss": true, + "disclosure_domain_count": 1, + "drop_articles": true, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [] + }, + "status": "ratified", + "author_notes": "Existing; do not modify." + }, + { + "register_id": "precise_v1", + "group": "A_depth", + "display_name": "Precise", + "description": "Precise register. Standard depth with disclosure_domain_count=2 and drop_provenance_tag=true. Exposes exactly two semantic domains and drops the parenthetical meta-tag.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [] + }, + "status": "ratified", + "author_notes": "Existing; do not modify." + }, + { + "register_id": "succinct_v1", + "group": "A_depth", + "display_name": "Succinct", + "description": "Between terse and precise — one-domain disclosure but keeps the provenance tag and full articles. The minimum-disclosure register for users who want both brevity and traceability.", + "depth_preference": "terse", + "realizer_overrides": { + "disclosure_domain_count": 1, + "compress_gloss": true + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [] + }, + "status": "drafted", + "author_notes": "Drop only the gloss compression; keep tag and articles." + }, + { + "register_id": "expansive_v1", + "group": "A_depth", + "display_name": "Expansive", + "description": "Expansive register. Disclosure_domain_count=3 with append_semantic_domain_clause=true to extend pack-grounded definitions with their full semantic-domain context. The default ceiling for depth without per-intent escalation.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [] + }, + "status": "drafted", + "author_notes": "Append-clause is the load-bearing knob for expansive output." + }, + { + "register_id": "exhaustive_v1", + "group": "A_depth", + "display_name": "Exhaustive", + "description": "Maximum-disclosure register. Disclosure_domain_count=3 across every intent via per_intent overrides; semantic-domain clause appended; provenance tags retained.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true, + "per_intent": { + "DEFINITION": { + "disclosure_domain_count": 3 + }, + "RECALL": { + "disclosure_domain_count": 3 + }, + "CAUSE": { + "disclosure_domain_count": 3 + }, + "VERIFICATION": { + "disclosure_domain_count": 3 + }, + "COMPARISON": { + "disclosure_domain_count": 3 + }, + "PROCEDURE": { + "disclosure_domain_count": 3 + }, + "CORRECTION": { + "disclosure_domain_count": 3 + }, + "NARRATIVE": { + "disclosure_domain_count": 3 + }, + "EXAMPLE": { + "disclosure_domain_count": 3 + } + } + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [] + }, + "status": "drafted", + "author_notes": "Per-intent block forces count=3 on every intent — even ones whose default falls back to 2." + }, + { + "register_id": "warm_v1", + "group": "B_tone", + "display_name": "Warm", + "description": "Warm conversational register. Soft openings and gentle closings; default depth.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Sure — ", + "Of course — ", + "Glad to — ", + "Let me help: " + ], + "transitions": [], + "closings": [ + "", + " — hope that helps.", + " — let me know if any of this is unclear.", + " — happy to go deeper." + ] + }, + "status": "drafted", + "author_notes": "Empty-string in openings/closings lets seeded selection pick 'no marker this turn'." + }, + { + "register_id": "cool_v1", + "group": "B_tone", + "display_name": "Cool", + "description": "Cool register — neutral openings, restrained closings.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Briefly — ", + "In short — ", + "Put plainly: " + ], + "transitions": [], + "closings": [ + "", + " — that is the substance.", + " — and no more." + ] + }, + "status": "drafted", + "author_notes": "Cool = warm's mirror; same default depth, sharper marker palette." + }, + { + "register_id": "casual_v1", + "group": "B_tone", + "display_name": "Casual", + "description": "Casual register — light openings, easy closings.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "OK so ", + "Right — ", + "Here goes — ", + "Quick one: " + ], + "transitions": [], + "closings": [ + "", + " — that's it.", + " — that's how it goes.", + " — and there we are." + ] + }, + "status": "drafted" + }, + { + "register_id": "formal_v1", + "group": "B_tone", + "display_name": "Formal", + "description": "Formal register. Standard depth; restrained marker palette (no convivial fillers).", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [] + }, + "status": "ratified", + "author_notes": "Existing; do not modify." + }, + { + "register_id": "intense_v1", + "group": "B_tone", + "display_name": "Intense", + "description": "Intense register — punchy openings, declarative closings.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "Here it is: ", + "Listen — ", + "The point: ", + "Plainly — " + ], + "transitions": [], + "closings": [ + "", + " — that's the substance.", + " — no more, no less.", + " — full stop." + ] + }, + "status": "drafted" + }, + { + "register_id": "playful_v1", + "group": "B_tone", + "display_name": "Playful", + "description": "Playful register — light openings, witty closings.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "OK so — ", + "Right — ", + "Here's the thing — ", + "Fun one: " + ], + "transitions": [], + "closings": [ + "", + " — neat, right?", + " — and there we have it.", + " — pretty tidy." + ] + }, + "status": "drafted" + }, + { + "register_id": "calm_v1", + "group": "B_tone", + "display_name": "Calm", + "description": "Calm register — quiet, measured markers; no exclamation.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Quietly: ", + "Note: ", + "Consider — " + ], + "transitions": [], + "closings": [ + "", + " — and so it stands.", + " — duly noted." + ] + }, + "status": "drafted" + }, + { + "register_id": "urgent_v1", + "group": "B_tone", + "display_name": "Urgent", + "description": "Urgent register — short, directive markers.", + "depth_preference": "terse", + "realizer_overrides": { + "disclosure_domain_count": 1, + "drop_provenance_tag": true, + "compress_gloss": true + }, + "discourse_markers": { + "openings": [ + "Quick: ", + "Here: ", + "Now: ", + "Brief: " + ], + "transitions": [], + "closings": [ + "", + " — now you know.", + " — proceed." + ] + }, + "status": "drafted" + }, + { + "register_id": "serene_v1", + "group": "B_tone", + "display_name": "Serene", + "description": "Serene register — flowing, contemplative markers.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Gently: ", + "There is — ", + "Consider this: " + ], + "transitions": [], + "closings": [ + "", + " — a quiet truth.", + " — and that suffices.", + " — held lightly." + ] + }, + "status": "drafted" + }, + { + "register_id": "dramatic_v1", + "group": "B_tone", + "display_name": "Dramatic", + "description": "Dramatic register — emphatic markers, declarative closings.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "Ah — ", + "Now: ", + "Behold: ", + "Here it is — " + ], + "transitions": [], + "closings": [ + "", + " — and there it stands.", + " — let that land." + ] + }, + "status": "drafted" + }, + { + "register_id": "sober_v1", + "group": "B_tone", + "display_name": "Sober", + "description": "Sober register — restrained, factual markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Plainly: ", + "Strictly: ", + "Stated bare: " + ], + "transitions": [], + "closings": [ + "", + " — without embellishment." + ] + }, + "status": "drafted" + }, + { + "register_id": "clinical_v1", + "group": "B_tone", + "display_name": "Clinical", + "description": "Clinical register — observational markers, two-domain disclosure for precision.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Observation: ", + "Note: ", + "Per the record: " + ], + "transitions": [], + "closings": [ + "", + " — as observed." + ] + }, + "status": "drafted" + }, + { + "register_id": "colloquial_v1", + "group": "B_tone", + "display_name": "Colloquial", + "description": "Colloquial register — chatty, contraction-friendly markers.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "OK so ", + "Right, so ", + "Y'know — " + ], + "transitions": [], + "closings": [ + "", + " — that's the gist.", + " — that's how it goes." + ] + }, + "status": "drafted" + }, + { + "register_id": "literary_v1", + "group": "B_tone", + "display_name": "Literary", + "description": "Literary register — elevated diction, em-dash heavy.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true, + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "There is, in this, ", + "Properly speaking — ", + "It must be said: " + ], + "transitions": [], + "closings": [ + "", + " — and so the matter rests.", + " — quietly held." + ] + }, + "status": "drafted" + }, + { + "register_id": "journalistic_v1", + "group": "B_tone", + "display_name": "Journalistic", + "description": "Journalistic register — leads with the fact; sourced closings.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": false + }, + "discourse_markers": { + "openings": [ + "", + "Lead: ", + "Key fact: ", + "In brief: " + ], + "transitions": [], + "closings": [ + "", + " — per the pack record.", + " — sourced." + ] + }, + "status": "drafted" + }, + { + "register_id": "poetic_v1", + "group": "B_tone", + "display_name": "Poetic", + "description": "Poetic register — image-led markers (within bounded buckets), expansive disclosure.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true, + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "Suppose — ", + "Consider — ", + "Imagine: " + ], + "transitions": [], + "closings": [ + "", + " — and so the image holds.", + " — quietly." + ] + }, + "status": "drafted", + "author_notes": "Markers stay short — image-rich without runaway poetic licence; the lemma is the protagonist." + }, + { + "register_id": "assertive_v1", + "group": "C_stance", + "display_name": "Assertive", + "description": "Assertive register — declarative markers, no hedging.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Plainly: ", + "Stated: ", + "Fact: " + ], + "transitions": [], + "closings": [ + "", + "." + ] + }, + "status": "drafted" + }, + { + "register_id": "hedged_v1", + "group": "C_stance", + "display_name": "Hedged", + "description": "Hedged register — soft markers signalling epistemic caution.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Tentatively: ", + "It seems — ", + "As best as can be said: " + ], + "transitions": [], + "closings": [ + "", + " — though others may differ.", + " — held provisionally." + ] + }, + "status": "drafted" + }, + { + "register_id": "inviting_v1", + "group": "C_stance", + "display_name": "Inviting", + "description": "Inviting register — invitation-shaped markers.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Consider: ", + "Notice: ", + "Look: " + ], + "transitions": [], + "closings": [ + "", + " — what do you make of it?", + " — say more if you'd like." + ] + }, + "status": "drafted" + }, + { + "register_id": "exploratory_v1", + "group": "C_stance", + "display_name": "Exploratory", + "description": "Exploratory register — open-ended markers.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Let's see — ", + "Could it be that ", + "Perhaps: " + ], + "transitions": [], + "closings": [ + "", + " — and there is more to explore.", + " — worth probing further." + ] + }, + "status": "drafted" + }, + { + "register_id": "didactic_v1", + "group": "C_stance", + "display_name": "Didactic", + "description": "Didactic register — instructional markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Note: ", + "Observe: ", + "Recall: " + ], + "transitions": [], + "closings": [ + "", + " — keep that in mind.", + " — and that's the lesson." + ] + }, + "status": "drafted" + }, + { + "register_id": "dialectic_v1", + "group": "C_stance", + "display_name": "Dialectic", + "description": "Dialectic register — two-sided markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "On one hand: ", + "Consider: " + ], + "transitions": [], + "closings": [ + "", + " — though one could argue the inverse.", + " — set against its converse." + ] + }, + "status": "drafted" + }, + { + "register_id": "probing_v1", + "group": "C_stance", + "display_name": "Probing", + "description": "Probing register — question-shaped closings.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Try this: " + ], + "transitions": [], + "closings": [ + "", + " — but why?", + " — how so?", + " — what follows?" + ] + }, + "status": "drafted" + }, + { + "register_id": "affirming_v1", + "group": "C_stance", + "display_name": "Affirming", + "description": "Affirming register — confirmational markers.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Yes — ", + "Indeed: ", + "Quite — " + ], + "transitions": [], + "closings": [ + "", + " — confirmed.", + " — exactly so." + ] + }, + "status": "drafted" + }, + { + "register_id": "curious_v1", + "group": "C_stance", + "display_name": "Curious", + "description": "Curious register — wondering tone.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "I wonder — ", + "Notice this: ", + "Interesting: " + ], + "transitions": [], + "closings": [ + "", + " — which leads where?", + " — and what then?" + ] + }, + "status": "drafted" + }, + { + "register_id": "skeptical_v1", + "group": "C_stance", + "display_name": "Skeptical", + "description": "Skeptical register — cautious markers, defers judgement.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Apparently — ", + "If the record holds — ", + "Subject to check: " + ], + "transitions": [], + "closings": [ + "", + " — pending verification.", + " — though I'd want corroboration." + ] + }, + "status": "drafted" + }, + { + "register_id": "confident_uncertain_v1", + "group": "C_stance", + "display_name": "Confidently uncertain", + "description": "Register signalling confident epistemic humility — first-person framing without dropping precision.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "As I see it: ", + "On my reading: ", + "I believe — " + ], + "transitions": [], + "closings": [ + "", + " — though I hold this lightly.", + " — open to revision." + ] + }, + "status": "drafted" + }, + { + "register_id": "socratic_v1", + "group": "C_stance", + "display_name": "Socratic", + "description": "Socratic register. Drops articles in cold-start surfaces and appends a probing question closing.", + "depth_preference": "standard", + "realizer_overrides": { + "drop_articles": true + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [ + "", + " — but what makes that so?", + " — and from where do we know it?" + ] + }, + "status": "ratified", + "author_notes": "Existing; do not modify." + }, + { + "register_id": "peer_v1", + "group": "D_posture", + "display_name": "Peer", + "description": "Peer-to-peer register — equal-level address.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "OK so — ", + "Right — " + ], + "transitions": [], + "closings": [ + "", + " — sound right?", + " — that match your read?" + ] + }, + "status": "drafted" + }, + { + "register_id": "mentor_v1", + "group": "D_posture", + "display_name": "Mentor", + "description": "Mentor register — guiding without lecturing.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Here's a useful frame: ", + "Notice: ", + "Consider — " + ], + "transitions": [], + "closings": [ + "", + " — keep that handy.", + " — useful to hold." + ] + }, + "status": "drafted" + }, + { + "register_id": "student_v1", + "group": "D_posture", + "display_name": "Student", + "description": "Student register — humble, asking, exploratory.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Best I can tell — ", + "As I understand it: ", + "Tentatively: " + ], + "transitions": [], + "closings": [ + "", + " — is that right?", + " — I'd value the check." + ] + }, + "status": "drafted" + }, + { + "register_id": "expert_v1", + "group": "D_posture", + "display_name": "Expert", + "description": "Expert register — authoritative without ornament.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [] + }, + "status": "drafted" + }, + { + "register_id": "scholar_v1", + "group": "D_posture", + "display_name": "Scholar", + "description": "Scholar register — academic prose cadence.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "It bears noting — ", + "Properly considered: ", + "We may say: " + ], + "transitions": [], + "closings": [ + "", + " — pace the relevant authorities.", + " — as the record holds." + ] + }, + "status": "drafted" + }, + { + "register_id": "practitioner_v1", + "group": "D_posture", + "display_name": "Practitioner", + "description": "Practitioner register — pragmatic, application-led.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "In practice: ", + "Operationally: ", + "On the ground: " + ], + "transitions": [], + "closings": [ + "", + " — that's how it lands in the field.", + " — pragmatically." + ] + }, + "status": "drafted" + }, + { + "register_id": "novice_v1", + "group": "D_posture", + "display_name": "Novice", + "description": "Novice register — eager-learning markers.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Just learning — ", + "If I have it right: ", + "My current take: " + ], + "transitions": [], + "closings": [ + "", + " — does that track?", + " — please correct if off." + ] + }, + "status": "drafted" + }, + { + "register_id": "narrator_v1", + "group": "D_posture", + "display_name": "Narrator", + "description": "Narrator register — storytelling cadence.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "And so — ", + "Here is what holds: ", + "Picture this: " + ], + "transitions": [], + "closings": [ + "", + " — and there the matter sits.", + " — that is the shape of it." + ] + }, + "status": "drafted" + }, + { + "register_id": "journalist_v1", + "group": "D_posture", + "display_name": "Journalist", + "description": "Journalist register — verb-led openings, sourced closings.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Reported: ", + "On record: ", + "By the pack: " + ], + "transitions": [], + "closings": [ + "", + " — sourced from the cognition pack.", + " — per the record." + ] + }, + "status": "drafted" + }, + { + "register_id": "elder_v1", + "group": "D_posture", + "display_name": "Elder", + "description": "Elder register — reflective, measured.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true, + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "Long held — ", + "It has always been said: ", + "Steadily: " + ], + "transitions": [], + "closings": [ + "", + " — and so it remains.", + " — as the tradition holds." + ] + }, + "status": "drafted" + }, + { + "register_id": "academic_v1", + "group": "E_domain", + "display_name": "Academic", + "description": "Academic register — Latinate cadence, conservative depth.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "It may be noted that ", + "Strictly construed: ", + "Per the literature: " + ], + "transitions": [], + "closings": [ + "", + " — as the convention holds.", + " — pace contrary readings." + ] + }, + "status": "drafted" + }, + { + "register_id": "conversational_v1", + "group": "E_domain", + "display_name": "Conversational", + "description": "Conversational register — light, immediate.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "So — ", + "OK — " + ], + "transitions": [], + "closings": [ + "", + " — that work?", + " — that the shape?" + ] + }, + "status": "drafted" + }, + { + "register_id": "executive_v1", + "group": "E_domain", + "display_name": "Executive", + "description": "Executive register — bottom-line first.", + "depth_preference": "terse", + "realizer_overrides": { + "disclosure_domain_count": 1, + "compress_gloss": true, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Bottom line: ", + "TL;DR: ", + "Headline: " + ], + "transitions": [], + "closings": [ + "", + " — details on request." + ] + }, + "status": "drafted" + }, + { + "register_id": "technical_v1", + "group": "E_domain", + "display_name": "Technical", + "description": "Technical register — precise, tag-bearing, two-domain disclosure.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Spec: ", + "Per the type: ", + "Operationally — " + ], + "transitions": [], + "closings": [ + "", + " — within spec." + ] + }, + "status": "drafted" + }, + { + "register_id": "legal_v1", + "group": "E_domain", + "display_name": "Legal", + "description": "Legal register — qualified, careful.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "Subject to caveats: ", + "Without prejudice: ", + "Stated for the record: " + ], + "transitions": [], + "closings": [ + "", + " — pending further determination.", + " — without warranty as to completeness." + ] + }, + "status": "drafted" + }, + { + "register_id": "medical_v1", + "group": "E_domain", + "display_name": "Medical", + "description": "Medical register — observational, precise.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Observation: ", + "On examination: ", + "Per the chart: " + ], + "transitions": [], + "closings": [ + "", + " — pending follow-up.", + " — as recorded." + ] + }, + "status": "drafted" + }, + { + "register_id": "scientific_v1", + "group": "E_domain", + "display_name": "Scientific", + "description": "Scientific register — methodical, conservative.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "By the data: ", + "Per measurement: ", + "Stated formally: " + ], + "transitions": [], + "closings": [ + "", + " — subject to replication.", + " — as the model holds." + ] + }, + "status": "drafted" + }, + { + "register_id": "philosophical_v1", + "group": "E_domain", + "display_name": "Philosophical", + "description": "Philosophical register — reflective, expansive.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Considered carefully: ", + "It might be said: ", + "Stepping back — " + ], + "transitions": [], + "closings": [ + "", + " — though much depends on the framing.", + " — and the question remains." + ] + }, + "status": "drafted" + }, + { + "register_id": "pedagogical_v1", + "group": "E_domain", + "display_name": "Pedagogical", + "description": "Pedagogical register. Standard depth; uses opening 'Recall:' and closing ' — does that make sense?' to scaffold learning.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "Recall: ", + "Let's see — ", + "" + ], + "transitions": [], + "closings": [ + "", + " — does that make sense?", + " — keep that in mind." + ] + }, + "status": "ratified", + "author_notes": "Existing; do not modify." + }, + { + "register_id": "devotional_v1", + "group": "E_domain", + "display_name": "Devotional", + "description": "Devotional register — reverent, contemplative.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Quietly: ", + "Held in reverence: ", + "We may note: " + ], + "transitions": [], + "closings": [ + "", + " — and that is enough.", + " — held in care." + ] + }, + "status": "drafted", + "author_notes": "Markers stay non-sectarian; no theological commitments at this layer." + }, + { + "register_id": "mathematical_v1", + "group": "E_domain", + "display_name": "Mathematical", + "description": "Mathematical register — proof-cadence.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_articles": true, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Definition: ", + "Claim: ", + "Lemma: " + ], + "transitions": [], + "closings": [ + "", + ".", + " — qed." + ] + }, + "status": "drafted", + "author_notes": "Q.E.D. as a tag; drop articles to land in math idiom." + }, + { + "register_id": "engineering_v1", + "group": "E_domain", + "display_name": "Engineering", + "description": "Engineering register — spec-shaped.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Spec: ", + "Requirement: ", + "Per design: " + ], + "transitions": [], + "closings": [ + "", + " — within tolerance.", + " — meets spec." + ] + }, + "status": "drafted" + }, + { + "register_id": "plainspoken_v1", + "group": "F_cultural", + "display_name": "Plainspoken", + "description": "Plainspoken register — Anglo-Saxon vocabulary, short markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Straight: ", + "Plainly: ", + "Said simply: " + ], + "transitions": [], + "closings": [ + "", + " — that is the truth of it." + ] + }, + "status": "drafted" + }, + { + "register_id": "cosmopolitan_v1", + "group": "F_cultural", + "display_name": "Cosmopolitan", + "description": "Cosmopolitan register — international, careful with idiom.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Stated broadly: ", + "Across contexts: ", + "Generally: " + ], + "transitions": [], + "closings": [ + "", + " — though it varies by context." + ] + }, + "status": "drafted" + }, + { + "register_id": "diplomatic_v1", + "group": "F_cultural", + "display_name": "Diplomatic", + "description": "Diplomatic register — qualified, conciliatory.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "With respect — ", + "Taking care: ", + "In good faith: " + ], + "transitions": [], + "closings": [ + "", + " — open to amendment.", + " — pending wider input." + ] + }, + "status": "drafted" + }, + { + "register_id": "blunt_v1", + "group": "F_cultural", + "display_name": "Blunt", + "description": "Blunt register — short, sharp, no qualification.", + "depth_preference": "terse", + "realizer_overrides": { + "disclosure_domain_count": 1, + "compress_gloss": true, + "drop_articles": true, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Look — ", + "Plainly: ", + "Said: " + ], + "transitions": [], + "closings": [ + "", + "." + ] + }, + "status": "drafted" + }, + { + "register_id": "aristocratic_v1", + "group": "F_cultural", + "display_name": "Aristocratic", + "description": "Aristocratic register — elevated diction; depth=3 with appended clause.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "One observes — ", + "Properly speaking: ", + "Indeed — " + ], + "transitions": [], + "closings": [ + "", + " — as has long been understood.", + " — and there it lies." + ] + }, + "status": "drafted" + }, + { + "register_id": "folksy_v1", + "group": "F_cultural", + "display_name": "Folksy", + "description": "Folksy register — regional warmth, light contractions.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Y'know — ", + "Way I see it: ", + "Plain as day: " + ], + "transitions": [], + "closings": [ + "", + " — and that's about the size of it.", + " — so it goes." + ] + }, + "status": "drafted" + }, + { + "register_id": "urbane_v1", + "group": "F_cultural", + "display_name": "Urbane", + "description": "Urbane register — sophisticated but casual.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Quite simply — ", + "If you'll allow — ", + "Briefly: " + ], + "transitions": [], + "closings": [ + "", + " — and there we are." + ] + }, + "status": "drafted" + }, + { + "register_id": "timeless_v1", + "group": "F_cultural", + "display_name": "Timeless", + "description": "Timeless register — restrained, classic, no fashion-of-the-day markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "It has been said: ", + "Steadily: " + ], + "transitions": [], + "closings": [ + "", + " — and so it has long held." + ] + }, + "status": "drafted" + }, + { + "register_id": "contemporary_v1", + "group": "F_cultural", + "display_name": "Contemporary", + "description": "Contemporary register — current idiom without slang.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "So — ", + "OK — ", + "Right — " + ], + "transitions": [], + "closings": [ + "", + " — that's the read.", + " — that's where it sits." + ] + }, + "status": "drafted" + }, + { + "register_id": "classic_v1", + "group": "F_cultural", + "display_name": "Classic", + "description": "Classic register — measured English without archaisms.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "Plainly stated: ", + "It must be said: " + ], + "transitions": [], + "closings": [ + "", + " — and so it has long stood." + ] + }, + "status": "drafted" + }, + { + "register_id": "neoteric_v1", + "group": "F_cultural", + "display_name": "Neoteric", + "description": "Neoteric register — fresh, immediate.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Fresh take: ", + "Here's the thing: " + ], + "transitions": [], + "closings": [ + "", + " — that's the live read.", + " — current as of this turn." + ] + }, + "status": "drafted" + }, + { + "register_id": "lyrical_v1", + "group": "F_cultural", + "display_name": "Lyrical", + "description": "Lyrical register — musical phrasing within bounded markers.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true, + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "Lightly — ", + "Hear it: ", + "And so — " + ], + "transitions": [], + "closings": [ + "", + " — and so the line completes itself.", + " — a quiet music." + ] + }, + "status": "drafted" + }, + { + "register_id": "cheerful_v1", + "group": "G_affective", + "display_name": "Cheerful", + "description": "Cheerful register — positive markers, no exclamation marks (deterministic, no affect-pretense).", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Good news — ", + "Happy to share: ", + "Glad — " + ], + "transitions": [], + "closings": [ + "", + " — and that's a fine read.", + " — and there we are." + ] + }, + "status": "drafted" + }, + { + "register_id": "somber_v1", + "group": "G_affective", + "display_name": "Somber", + "description": "Somber register — restrained, weight-acknowledging.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Gravely: ", + "Held with care: ", + "Stated quietly: " + ], + "transitions": [], + "closings": [ + "", + " — and so it stands.", + " — held with weight." + ] + }, + "status": "drafted" + }, + { + "register_id": "melancholic_v1", + "group": "G_affective", + "display_name": "Melancholic", + "description": "Melancholic register — wistful framing.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Long held — ", + "Sadly true: ", + "And yet — " + ], + "transitions": [], + "closings": [ + "", + " — though it has always been so.", + " — a quiet fact." + ] + }, + "status": "drafted" + }, + { + "register_id": "awed_v1", + "group": "G_affective", + "display_name": "Awed", + "description": "Awed register — wonder-acknowledging markers.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true, + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "Remarkably: ", + "Strikingly: ", + "Note this: " + ], + "transitions": [], + "closings": [ + "", + " — and that is no small thing." + ] + }, + "status": "drafted" + }, + { + "register_id": "grave_v1", + "group": "G_affective", + "display_name": "Grave", + "description": "Grave register — serious, careful.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "Soberly: ", + "With care: ", + "Plainly: " + ], + "transitions": [], + "closings": [ + "", + " — and that is the record.", + " — held in earnest." + ] + }, + "status": "drafted" + }, + { + "register_id": "light_v1", + "group": "G_affective", + "display_name": "Light", + "description": "Light register — buoyant, low-pressure.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Lightly: ", + "Easy one: " + ], + "transitions": [], + "closings": [ + "", + " — and there we go.", + " — neat enough." + ] + }, + "status": "drafted" + }, + { + "register_id": "wry_v1", + "group": "G_affective", + "display_name": "Wry", + "description": "Wry register — slight irony, deadpan closings.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Officially: ", + "On the books: ", + "Allegedly — " + ], + "transitions": [], + "closings": [ + "", + " — for whatever that's worth.", + " — make of it what you will." + ] + }, + "status": "drafted" + }, + { + "register_id": "dry_v1", + "group": "G_affective", + "display_name": "Dry", + "description": "Dry register — understated, factual.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Stated: ", + "Plainly — " + ], + "transitions": [], + "closings": [ + "", + ".", + " — that's it." + ] + }, + "status": "drafted" + }, + { + "register_id": "gentle_v1", + "group": "G_affective", + "display_name": "Gentle", + "description": "Gentle register — soft openings, easy closings.", + "depth_preference": "standard", + "realizer_overrides": {}, + "discourse_markers": { + "openings": [ + "", + "Gently — ", + "If it helps: ", + "Quietly: " + ], + "transitions": [], + "closings": [ + "", + " — held carefully.", + " — softly stated." + ] + }, + "status": "drafted" + }, + { + "register_id": "earnest_v1", + "group": "G_affective", + "display_name": "Earnest", + "description": "Earnest register — sincere, direct.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Truly: ", + "Sincerely: ", + "Plainly: " + ], + "transitions": [], + "closings": [ + "", + " — and that is what I mean.", + " — said in good faith." + ] + }, + "status": "drafted" + }, + { + "register_id": "documentary_v1", + "group": "H_functional", + "display_name": "Documentary", + "description": "Documentary register — neutral-fact, sourced.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "On record: ", + "Documented: ", + "By the pack: " + ], + "transitions": [], + "closings": [ + "", + " — per the pack record.", + " — as documented." + ] + }, + "status": "drafted" + }, + { + "register_id": "instructional_v1", + "group": "H_functional", + "display_name": "Instructional", + "description": "Instructional register — step-by-step framing.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Step by step: ", + "Start here: ", + "First: " + ], + "transitions": [], + "closings": [ + "", + " — proceed when ready." + ] + }, + "status": "drafted" + }, + { + "register_id": "persuasive_v1", + "group": "H_functional", + "display_name": "Persuasive", + "description": "Persuasive register — case-building markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Consider — ", + "Note: ", + "The case: " + ], + "transitions": [], + "closings": [ + "", + " — which is why it matters.", + " — take that as the argument." + ] + }, + "status": "drafted" + }, + { + "register_id": "conciliatory_v1", + "group": "H_functional", + "display_name": "Conciliatory", + "description": "Conciliatory register — accommodating closings.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "If I may: ", + "With your indulgence: " + ], + "transitions": [], + "closings": [ + "", + " — open to your view.", + " — we can work from there." + ] + }, + "status": "drafted" + }, + { + "register_id": "clarifying_v1", + "group": "H_functional", + "display_name": "Clarifying", + "description": "Clarifying register — restate-the-key markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "To be clear: ", + "Put plainly: ", + "Said again: " + ], + "transitions": [], + "closings": [ + "", + " — does that resolve it?", + " — clearer now?" + ] + }, + "status": "drafted" + }, + { + "register_id": "summarizing_v1", + "group": "H_functional", + "display_name": "Summarizing", + "description": "Summarizing register — headline-shape markers.", + "depth_preference": "terse", + "realizer_overrides": { + "disclosure_domain_count": 1, + "compress_gloss": true + }, + "discourse_markers": { + "openings": [ + "", + "Summary: ", + "In sum: ", + "Headline: " + ], + "transitions": [], + "closings": [ + "", + " — that is the gist." + ] + }, + "status": "drafted" + }, + { + "register_id": "critiquing_v1", + "group": "H_functional", + "display_name": "Critiquing", + "description": "Critiquing register — analytical, qualified.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "On critical reading: ", + "Noticed: ", + "Set against expectation: " + ], + "transitions": [], + "closings": [ + "", + " — and that bears watching.", + " — though one might press further." + ] + }, + "status": "drafted" + }, + { + "register_id": "comparing_v1", + "group": "H_functional", + "display_name": "Comparing", + "description": "Comparing register — parallel-construction markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Side by side: ", + "Compared: ", + "Set against: " + ], + "transitions": [], + "closings": [ + "", + " — and there sits the contrast." + ] + }, + "status": "drafted" + }, + { + "register_id": "elaborating_v1", + "group": "H_functional", + "display_name": "Elaborating", + "description": "Elaborating register — extended disclosure.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "More fully: ", + "To extend: ", + "Said at length: " + ], + "transitions": [], + "closings": [ + "", + " — and there is more, when you want it." + ] + }, + "status": "drafted" + }, + { + "register_id": "exemplifying_v1", + "group": "H_functional", + "display_name": "Exemplifying", + "description": "Exemplifying register — example-led markers.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "By way of example: ", + "Take this: ", + "For instance: " + ], + "transitions": [], + "closings": [ + "", + " — as a case in point.", + " — exemplifying the pattern." + ] + }, + "status": "drafted" + }, + { + "register_id": "convivial_v1", + "group": "I_composite", + "display_name": "Convivial", + "description": "Warm, conversational register. Seeded openings and closings draw from small bounded buckets; depth follows the standard (3-domain) default.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "So,", + "Right —", + "OK," + ], + "transitions": [], + "closings": [ + "", + " — does that help?", + " — make sense?" + ] + }, + "status": "ratified", + "author_notes": "Existing; do not modify." + }, + { + "register_id": "tutorial_v1", + "group": "I_composite", + "display_name": "Tutorial", + "description": "Tutorial composite — combines pedagogical openings, instructional steps, expansive depth.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "First, ", + "Recall: ", + "Notice: " + ], + "transitions": [], + "closings": [ + "", + " — does that make sense?", + " — keep that handy." + ] + }, + "status": "drafted" + }, + { + "register_id": "interview_v1", + "group": "I_composite", + "display_name": "Interview", + "description": "Interview composite — interviewer openings + probing closings.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 2 + }, + "discourse_markers": { + "openings": [ + "", + "Question: ", + "Posed: " + ], + "transitions": [], + "closings": [ + "", + " — what's your read?", + " — say more?" + ] + }, + "status": "drafted" + }, + { + "register_id": "briefing_v1", + "group": "I_composite", + "display_name": "Briefing", + "description": "Briefing composite — executive depth + journalist closings.", + "depth_preference": "terse", + "realizer_overrides": { + "disclosure_domain_count": 1, + "compress_gloss": true, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Brief: ", + "Headline: " + ], + "transitions": [], + "closings": [ + "", + " — per the pack.", + " — full detail on request." + ] + }, + "status": "drafted" + }, + { + "register_id": "deposition_v1", + "group": "I_composite", + "display_name": "Deposition", + "description": "Deposition composite — legal qualification + documentary sourcing.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "On the record: ", + "Stated under oath: ", + "For the record: " + ], + "transitions": [], + "closings": [ + "", + " — as documented.", + " — subject to further inquiry." + ] + }, + "status": "drafted" + }, + { + "register_id": "lecture_v1", + "group": "I_composite", + "display_name": "Lecture", + "description": "Lecture composite — scholar + didactic.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "Today: ", + "Note: ", + "Consider: " + ], + "transitions": [], + "closings": [ + "", + " — keep that for the next session.", + " — and that closes the segment." + ] + }, + "status": "drafted" + }, + { + "register_id": "memo_v1", + "group": "I_composite", + "display_name": "Memo", + "description": "Memo composite — executive terseness + civic formality.", + "depth_preference": "terse", + "realizer_overrides": { + "disclosure_domain_count": 1, + "compress_gloss": true, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [ + "", + "Memo: ", + "Note to file: " + ], + "transitions": [], + "closings": [ + "", + " — please action.", + " — for your records." + ] + }, + "status": "drafted" + }, + { + "register_id": "review_v1", + "group": "I_composite", + "display_name": "Review", + "description": "Review composite — critiquing + comparing.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "On review: ", + "Set against expectation: ", + "Compared: " + ], + "transitions": [], + "closings": [ + "", + " — verdict: held.", + " — worth a second look." + ] + }, + "status": "drafted" + }, + { + "register_id": "story_v1", + "group": "I_composite", + "display_name": "Story", + "description": "Story composite — narrator + literary.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true, + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "And so — ", + "It happened that ", + "Picture: " + ], + "transitions": [], + "closings": [ + "", + " — and there the matter sits.", + " — that is the shape of it." + ] + }, + "status": "drafted" + }, + { + "register_id": "elegy_v1", + "group": "I_composite", + "display_name": "Elegy", + "description": "Elegy composite — melancholic + literary.", + "depth_preference": "standard", + "realizer_overrides": { + "append_semantic_domain_clause": true, + "disclosure_domain_count": 3 + }, + "discourse_markers": { + "openings": [ + "", + "Long held — ", + "Quietly: ", + "Now — " + ], + "transitions": [], + "closings": [ + "", + " — and so it remains.", + " — held in stillness." + ] + }, + "status": "drafted" + }, + { + "register_id": "epigram_v1", + "group": "I_composite", + "display_name": "Epigram", + "description": "Epigram composite — terse + wry.", + "depth_preference": "terse", + "realizer_overrides": { + "disclosure_domain_count": 1, + "compress_gloss": true, + "drop_articles": true, + "drop_provenance_tag": true + }, + "discourse_markers": { + "openings": [], + "transitions": [], + "closings": [ + "", + " — and that suffices." + ] + }, + "status": "drafted" + }, + { + "register_id": "manifesto_v1", + "group": "I_composite", + "display_name": "Manifesto", + "description": "Manifesto composite — oratorical + persuasive.", + "depth_preference": "standard", + "realizer_overrides": { + "disclosure_domain_count": 3, + "append_semantic_domain_clause": true + }, + "discourse_markers": { + "openings": [ + "", + "We hold — ", + "Plainly: ", + "Stated: " + ], + "transitions": [], + "closings": [ + "", + " — and we stand on it.", + " — that is the position." + ] + }, + "status": "drafted" + } + ] +} diff --git a/tests/test_adr_0087_procedure_selector.py b/tests/test_adr_0087_procedure_selector.py new file mode 100644 index 00000000..522c0764 --- /dev/null +++ b/tests/test_adr_0087_procedure_selector.py @@ -0,0 +1,133 @@ +"""ADR-0087 — PROCEDURE selector + trailing-clause subject echo. + +Two coupled changes: + +1. **Numeric-determiner downrank** in :func:`_extract_procedure_topic_lemma` + — tokens whose primary semantic_domain starts with + ``quantitative.numeric.`` are demoted; a non-numeric resident + candidate always wins. Only when the numeric is the sole + resident does it become the topic. + +2. **Subject-text echo in the trailing clause** of + :func:`pack_grounded_procedure_surface` — the *"Step-by-step + guidance for X is not yet ratified."* clause echoes the + normalized full subject_text rather than just the lemma, so OOV + head nouns (``terms``, ``mistake``) reach the surface even when + only the procedure verb is pack-resident. + +Closes ``procedure_compare_011`` (*"How do I compare two terms?"*) +where the pre-ADR-0087 selector picked the cardinal ``two`` over +``compare`` and the OOV head noun ``terms`` never reached the surface. + +Pins both the engagement on the failing case and the regression +guard on the four existing PROCEDURE eval cases. +""" +from __future__ import annotations + +import pytest + +from chat.pack_grounding import ( + _extract_procedure_topic_lemma, + pack_grounded_procedure_surface, +) +from chat.runtime import ChatRuntime + + +# --------------------------------------------------------------------------- +# Selector — numeric downrank +# --------------------------------------------------------------------------- + + +def test_numeric_determiner_is_downranked() -> None: + """A non-numeric resident candidate (verb ``compare``) wins over + a numeric-cardinal candidate (``two``).""" + assert _extract_procedure_topic_lemma("compare two terms") == "compare" + + +def test_numeric_only_resident_still_wins_by_elimination() -> None: + """If the numeric is the sole resident token, it remains the + topic — preserves coverage on numeric-only prompts.""" + assert _extract_procedure_topic_lemma("count to two") == "two" + + +def test_non_numeric_resident_still_takes_last_wins_priority() -> None: + """Existing last-wins behavior is preserved for non-numeric tokens. + ``concept`` still wins over ``define`` in ``define a concept``.""" + assert _extract_procedure_topic_lemma("define a concept") == "concept" + + +# --------------------------------------------------------------------------- +# Surface — trailing clause echoes subject_text +# --------------------------------------------------------------------------- + + +def test_trailing_clause_echoes_full_subject_text() -> None: + """OOV head noun ``terms`` reaches the surface via the trailing + clause even though it doesn't resolve in any pack.""" + surface = pack_grounded_procedure_surface("compare two terms") + assert surface is not None + assert "terms" in surface + assert "Step-by-step guidance for compare two terms" in surface + + +def test_trailing_clause_normalizes_punctuation_in_echo() -> None: + surface = pack_grounded_procedure_surface("define, a concept.") + assert surface is not None + assert "Step-by-step guidance for define a concept" in surface + + +def test_displayed_lemma_is_compare_not_two() -> None: + """The semantic anchor in the surface is the operation verb + ``compare``, not the cardinal determiner ``two``.""" + surface = pack_grounded_procedure_surface("compare two terms") + assert surface is not None + assert "compare (operation.compare" in surface + # The cardinal-determiner lemma should not appear as the displayed + # pack-grounded anchor (it may still appear in the echoed subject). + assert "two (quantitative.numeric" not in surface + + +# --------------------------------------------------------------------------- +# Regression guard — existing PROCEDURE eval cases still pass +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "prompt,expected_terms", + [ + ("How do I define a concept?", ("concept",)), + # ``verify a claim`` and ``correct a mistake`` carry no + # expected_terms in the cognition eval, but the surfaces must + # still ground deterministically and mention the user's topic. + ("How do I verify a claim?", ("claim", "verify a claim")), + ("How can I correct a mistake?", ("correct", "correct a mistake")), + # The ADR-0087 target case. + ("How do I compare two terms?", ("terms", "compare two terms")), + ], +) +def test_runtime_procedure_surfaces_contain_expected_terms( + prompt: str, expected_terms: tuple[str, ...], +) -> None: + rt = ChatRuntime() + resp = rt.chat(prompt) + assert resp.grounding_source == "pack" + surface_lower = resp.surface.lower() + for term in expected_terms: + assert term.lower() in surface_lower, ( + f"prompt={prompt!r} expected {term!r} in surface, got {resp.surface!r}" + ) + + +def test_procedure_surface_is_deterministic_after_adr_0087() -> None: + a = pack_grounded_procedure_surface("compare two terms") + b = pack_grounded_procedure_surface("compare two terms") + assert a == b + + +def test_oov_only_procedure_still_falls_through_to_none() -> None: + """ADR-0061 honesty contract holds: a verb+noun pair that is + entirely OOV across mounted packs still returns ``None`` from + the composer (runtime falls through to the OOV invitation).""" + # ``fix bugs`` is the canonical fully-OOV PROCEDURE fixture used + # by tests/test_procedure_surface.py. + assert pack_grounded_procedure_surface("fix bugs") is None diff --git a/tests/test_pack_grounded_unknown.py b/tests/test_pack_grounded_unknown.py new file mode 100644 index 00000000..46f4bb7f --- /dev/null +++ b/tests/test_pack_grounded_unknown.py @@ -0,0 +1,141 @@ +"""ADR-0086 — UNKNOWN-intent pack-resident token surface. + +Pins: + +1. **Engagement** — the four cognition-eval UNKNOWN miss prompts each + surface their pack-resident English tokens with semantic_domains. +2. **Null-lift invariant** — fully-OOV prompts that have zero + pack-resident lemmas still emit the universal disclosure + byte-identically. +3. **Composer determinism** — repeated invocation on the same prompt + returns the byte-identical surface (ratified packs are immutable + and the composer reads no session state). +4. **Provenance** — surfaces emitted by the new composer carry + ``grounding_source == "pack"`` so the audit contract distinguishes + them from vault- and teaching-grounded surfaces. +5. **Pre-ADR-0086 hand-off** — the runtime falls through to the + bare ``_UNKNOWN_DOMAIN_SURFACE`` only when the composer returns + ``None``; no other UNKNOWN-intent code path is disturbed. +6. **Stopword discipline** — pure dialogue-filler prompts (``be have``) + return ``None`` because every resident token is stopworded. +""" +from __future__ import annotations + +import pytest + +from chat.pack_grounding import pack_grounded_unknown_surface +from chat.runtime import ChatRuntime, _UNKNOWN_DOMAIN_SURFACE + + +# --------------------------------------------------------------------------- +# Composer — direct calls +# --------------------------------------------------------------------------- + + +def test_composer_returns_none_on_empty() -> None: + assert pack_grounded_unknown_surface("") is None + assert pack_grounded_unknown_surface(None) is None # type: ignore[arg-type] + + +def test_composer_returns_none_on_fully_oov_prompt() -> None: + """Null-lift invariant — zero resident tokens → None.""" + assert pack_grounded_unknown_surface("xyzzy plugh frobnitz") is None + + +def test_composer_returns_none_on_stopwords_only() -> None: + """``be`` and ``have`` are pack-resident but stopworded — a + prompt of only stopwords yields no surface.""" + assert pack_grounded_unknown_surface("be have") is None + + +def test_composer_lifts_single_resident_token() -> None: + """``light`` is in ``en_core_cognition_v1`` — composer surfaces it.""" + surface = pack_grounded_unknown_surface("light logos") + assert surface is not None + assert "light" in surface + assert "pack-grounded (en_core_cognition_v1)" in surface + assert "No session evidence yet." in surface + + +def test_composer_lifts_two_resident_tokens() -> None: + surface = pack_grounded_unknown_surface("evidence reason") + assert surface is not None + assert "evidence" in surface + assert "reason" in surface + + +def test_composer_caps_at_max_tokens() -> None: + """Default ``max_tokens=3`` — four resident tokens surface + only the first three, deterministically.""" + surface = pack_grounded_unknown_surface( + "spirit wisdom truth knowledge", + ) + assert surface is not None + # First three lemmas in left-to-right order should appear; the + # fourth (``knowledge``) is dropped under the default cap. + assert "spirit" in surface + assert "wisdom" in surface + assert "truth" in surface + assert "knowledge" not in surface + + +def test_composer_is_deterministic() -> None: + """Repeated calls on the same prompt yield byte-identical surfaces.""" + a = pack_grounded_unknown_surface("spirit wisdom truth") + b = pack_grounded_unknown_surface("spirit wisdom truth") + assert a == b + + +def test_composer_strips_punctuation() -> None: + surface = pack_grounded_unknown_surface("light, logos.") + assert surface is not None + assert "light" in surface + + +def test_composer_is_case_insensitive() -> None: + surface = pack_grounded_unknown_surface("LIGHT LOGOS") + assert surface is not None + assert "light" in surface + + +# --------------------------------------------------------------------------- +# Runtime engagement — the four cognition-eval miss cases +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "prompt,expected_terms", + [ + # public split — unknown_logos_019 + ("light logos", ("light",)), + # dev split — unknown_evidence_042 + ("evidence reason", ("evidence", "reason")), + # holdout split — unknown_spirit_041 + ("spirit wisdom truth", ("wisdom", "truth")), + # holdout split — unknown_word_018 + ("word beginning truth", ("word", "truth")), + ], +) +def test_runtime_unknown_lifts_pack_tokens( + prompt: str, expected_terms: tuple[str, ...], +) -> None: + """The four UNKNOWN-intent eval misses each lift to pack-grounded + surfaces containing the expected English term(s).""" + rt = ChatRuntime() + resp = rt.chat(prompt) + assert resp.grounding_source == "pack" + surface_lower = resp.surface.lower() + for term in expected_terms: + assert term.lower() in surface_lower, ( + f"expected {term!r} in surface, got {resp.surface!r}" + ) + + +def test_runtime_null_lift_on_fully_oov_unknown() -> None: + """Null-lift invariant at the runtime layer: prompts with zero + pack-resident lemmas still emit the universal disclosure + byte-identically.""" + rt = ChatRuntime() + resp = rt.chat("xyzzy plugh frobnitz") + assert resp.surface == _UNKNOWN_DOMAIN_SURFACE + assert resp.grounding_source == "none"