diff --git a/chat/pack_grounding.py b/chat/pack_grounding.py index ad2e515a..66d0727b 100644 --- a/chat/pack_grounding.py +++ b/chat/pack_grounding.py @@ -476,6 +476,57 @@ def pack_grounded_surface( return candidate.surface if candidate is not None else None +_RELATION_CONFIRMATION_DISPLAY: dict[str, str] = { + "reveals": "reveals", + "grounds": "grounds", + "supports": "supports", + "requires": "requires", + "causes": "causes", + "precedes": "precedes", + "follows": "follows", +} + + +def pack_grounded_relation_confirmation_surface( + subject_lemma: str, + relation: str | None, + object_lemma: str | None, + *, + negated: bool = False, +) -> str | None: + """Return a deterministic surface for a confirmed relation claim. + + C2 handles prompts like ``"Light reveals truth, right?"`` by + stripping the terminal confirmation tag before intent classification. + This composer preserves the resulting proposition without requiring + a reviewed teaching chain for every relation variant. It only + emits when both endpoint lemmas resolve in mounted packs and the + relation is in the closed display table above. + """ + if not subject_lemma or not object_lemma or not relation: + return None + rel = _RELATION_CONFIRMATION_DISPLAY.get(relation.strip().lower()) + if rel is None: + return None + subject_key = subject_lemma.strip().lower() + object_key = object_lemma.strip().lower() + subject_resolved = resolve_lemma(subject_key) + object_resolved = resolve_lemma(object_key) + if subject_resolved is None or object_resolved is None: + return None + subject_pack_id, subject_domains = subject_resolved + object_pack_id, object_domains = object_resolved + if negated: + predicate = f"does not {rel[:-1] if rel.endswith('s') else rel}" + else: + predicate = rel + return ( + f"{subject_key} {predicate} {object_key}. " + f"pack-grounded ({subject_pack_id}; {object_pack_id}): " + f"{subject_domains[0]}; {object_domains[0]}." + ) + + def is_pack_lemma(lemma: str) -> bool: """Return True iff *lemma* has an entry with ``semantic_domains`` in the ratified cognition pack (``en_core_cognition_v1``). diff --git a/chat/runtime.py b/chat/runtime.py index 77f4185f..d94b2e4a 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -15,6 +15,7 @@ from chat.pack_grounding import ( pack_grounded_comparison_surface, pack_grounded_correction_surface, pack_grounded_procedure_surface, + pack_grounded_relation_confirmation_surface, PACK_ID as _COGNITION_PACK_ID, ) from chat.teaching_grounding import ( @@ -752,6 +753,19 @@ class ChatRuntime: if intent.tag in (IntentTag.CAUSE, IntentTag.VERIFICATION): lemma = (intent.subject or "").strip() if lemma: + if ( + intent.tag is IntentTag.VERIFICATION + and intent.relation + and intent.secondary_subject + ): + surface = pack_grounded_relation_confirmation_surface( + lemma, + intent.relation, + intent.object or intent.secondary_subject, + negated=intent.negated, + ) + if surface is not None: + return (surface, "pack") if self.config.composed_surface: surface = teaching_grounded_surface_composed( lemma, intent.tag, register=self.register_pack, diff --git a/docs/decisions/ADR-0076-confirmation-tag-normalization.md b/docs/decisions/ADR-0076-confirmation-tag-normalization.md new file mode 100644 index 00000000..0325e7dc --- /dev/null +++ b/docs/decisions/ADR-0076-confirmation-tag-normalization.md @@ -0,0 +1,78 @@ +# ADR-0076 — Confirmation-Tag Normalization (C2) + +**Status:** Accepted +**Date:** 2026-05-20 +**Series:** C2 — Confirmation-tag intent normalization +**Builds on:** [ADR-0075](ADR-0075-realizer-slot-type-guard.md) + +## Context + +ADR-0075 established the coherence floor: illegal realizer candidates +must not escape. Its holdout cluster deliberately preserved the observed +failure class: + +```text +Light reveals truth, right? -> Right does not thought. +``` + +C1 routed those candidates to bounded disclosure. C2 moves the fix +upstream by preserving the proposition before the realizer sees it. + +## Decision + +The intent classifier strips terminal confirmation tags only when they +are punctuation-bound after content: + +```text +X reveals Y, right? +X reveals Y. ok? +``` + +Bare or content uses are not stripped: + +```text +yes? +Is right an axis? +``` + +After stripping, a closed declarative relation form is classified as a +`VERIFICATION` proposition carrying: + +```text +subject +relation +object +negated +``` + +The runtime adds a deterministic pack-grounded relation-confirmation +surface for these claims. It emits only when both endpoint lemmas +resolve in mounted packs and the relation is in the closed relation +display table. + +## Invariants + +The ADR-0075 guard remains active. The holdout gate becomes hybrid: + +1. Synthetic illegal candidates are checked directly with + `generate.realizer_guard.check_surface()` to prove the guard still + fires. +2. Runtime confirmation prompts now assert accepted propositional + surfaces with `realizer_guard_status="ok"`. + +The old `walk_surface` grep for `"does not thought"` is retired for C2 +runtime prompts because the rejected candidate is no longer produced. + +Trace-hash values for the C2 holdout-cluster prompts change as a +deliberate consequence of the substantive lift. Register-invariance and +lens-distinctness invariants are unaffected because they are stated +per-prompt, not against frozen hash values. + +## Non-Goals + +- No broad English parser. +- No stochastic repair. +- No relaxation of ADR-0075 guard rules. +- No corpus mutation; relation-confirmation surfaces are reconstructed + from mounted pack lemmas and semantic domains. + diff --git a/evals/realizer_guard/run_holdout.py b/evals/realizer_guard/run_holdout.py index 4376bdbd..c211b900 100644 --- a/evals/realizer_guard/run_holdout.py +++ b/evals/realizer_guard/run_holdout.py @@ -1,15 +1,13 @@ -"""C1 holdout cluster — illegal-articulation prompts that the -ADR-0075 realizer slot-type guard must reject. +"""C1/C2 holdout cluster. -Each prompt is run through ``CognitiveTurnPipeline`` and the -recorded ``TurnEvent`` is checked for: +Hybrid gate after C2: -* ``realizer_guard_status == "rejected"`` -* ``realizer_guard_rule == `` -* ``surface == DISCLOSURE_SURFACE`` -* ``walk_surface`` carries the pre-guard candidate (non-empty, - not the disclosure string itself) -* ``grounding_source == "none"`` +* Synthetic illegal candidates are checked directly against + ``generate.realizer_guard.check_surface`` so the guard-firing + invariant remains pinned after upstream C2 normalization fixes the + runtime prompts. +* The former runtime bug prompts are run through ``CognitiveTurnPipeline`` + and must now produce accepted propositional surfaces. The cluster is reached by priming the vault with three pack-known DEFINITION prompts first, which is the same sequence that exposed @@ -31,7 +29,7 @@ from typing import Any from chat.runtime import ChatRuntime from core.cognition.pipeline import CognitiveTurnPipeline from core.config import RuntimeConfig -from generate.realizer_guard import DISCLOSURE_SURFACE +from generate.realizer_guard import check_surface _PRIMING_PROMPTS: tuple[str, ...] = ( @@ -41,22 +39,19 @@ _PRIMING_PROMPTS: tuple[str, ...] = ( ) -# Each cluster entry is (prompt, expected_rule_id). -# -# The cluster covers the observed bug class: confirmation-tag -# discourse particles (``right`` / ``no`` / ``yes``) that steer the -# realizer to emit `` does not .`` — an illegal -# do-support negation with a noun in the verb slot. -# -# All six prompts run on freshly-primed isolated runtimes (no shared -# vault state across prompts) so each cell is order-independent. -_HOLDOUT_PROMPTS: tuple[tuple[str, str], ...] = ( - ("Light reveals truth, right?", "R2_aux_neg_requires_verb"), - ("Light reveals truth, no?", "R2_aux_neg_requires_verb"), - ("Light reveals truth, yes?", "R2_aux_neg_requires_verb"), - ("Knowledge supports truth, right?", "R2_aux_neg_requires_verb"), - ("Light grounds truth, right?", "R2_aux_neg_requires_verb"), - ("Light supports truth, right?", "R2_aux_neg_requires_verb"), +_SYNTHETIC_ILLEGAL_CANDIDATES: tuple[tuple[str, str], ...] = ( + ("Right does not thought.", "R2_aux_neg_requires_verb"), + ("Light is not reveal.", "R3_be_neg_requires_predicate"), +) + + +_HOLDOUT_PROMPTS: tuple[str, ...] = ( + "Light reveals truth, right?", + "Light reveals truth, no?", + "Light reveals truth, yes?", + "Knowledge supports truth, right?", + "Light grounds truth, right?", + "Light supports truth, right?", ) @@ -69,7 +64,7 @@ def _build_runtime() -> ChatRuntime: )) -def _run_one(prompt: str, expected_rule: str) -> dict[str, Any]: +def _run_runtime_one(prompt: str) -> dict[str, Any]: runtime = _build_runtime() pipeline = CognitiveTurnPipeline(runtime=runtime) for primer in _PRIMING_PROMPTS: @@ -79,56 +74,69 @@ def _run_one(prompt: str, expected_rule: str) -> dict[str, Any]: status = getattr(turn_event, "realizer_guard_status", "") rule = getattr(turn_event, "realizer_guard_rule", "") surface = turn_event.surface - walk_surface = turn_event.walk_surface grounding_source = getattr(turn_event, "grounding_source", "") - rejected = status == "rejected" - rule_matches = (expected_rule == "") or (rule == expected_rule) - surface_is_disclosure = surface == DISCLOSURE_SURFACE - walk_preserves_candidate = bool(walk_surface) and walk_surface != DISCLOSURE_SURFACE - grounding_forced_none = grounding_source == "none" + accepted = status == "ok" + proposition_surface = bool(surface) and "pack-grounded" in surface and grounding_source == "pack" return { "prompt": prompt, - "expected_rule": expected_rule, "realizer_guard_status": status, "realizer_guard_rule": rule, "surface": surface, - "walk_surface": walk_surface, "grounding_source": grounding_source, - "rejected": rejected, - "rule_matches": rule_matches, - "surface_is_disclosure": surface_is_disclosure, - "walk_preserves_candidate": walk_preserves_candidate, - "grounding_forced_none": grounding_forced_none, + "accepted": accepted, + "proposition_surface": proposition_surface, + "cell_supported": accepted and rule == "" and proposition_surface, + } + + +def _run_synthetic_one(candidate: str, expected_rule: str) -> dict[str, Any]: + runtime = _build_runtime() + verdict = check_surface(candidate, pos_lookup=runtime._pos_by_surface.get) + return { + "candidate": candidate, + "expected_rule": expected_rule, + "realizer_guard_status": verdict.status, + "realizer_guard_rule": verdict.rule_id, "cell_supported": ( - rejected - and rule_matches - and surface_is_disclosure - and walk_preserves_candidate - and grounding_forced_none + verdict.status == "rejected" + and verdict.rule_id == expected_rule ), } def run_holdout(*, emit_json: bool = False) -> dict[str, Any]: - cells = [_run_one(p, r) for (p, r) in _HOLDOUT_PROMPTS] - failures = [c for c in cells if not c["cell_supported"]] + synthetic_cells = [ + _run_synthetic_one(candidate, rule) + for candidate, rule in _SYNTHETIC_ILLEGAL_CANDIDATES + ] + runtime_cells = [_run_runtime_one(p) for p in _HOLDOUT_PROMPTS] + failures = [ + c for c in (*synthetic_cells, *runtime_cells) + if not c["cell_supported"] + ] all_supported = not failures if not emit_json: print() print("=" * 76) - print(" ADR-0075 (C1) — realizer guard holdout cluster") + print(" ADR-0075/0076 (C1/C2) — hybrid guard + confirmation cluster") print("=" * 76) print(f" Priming sequence: {list(_PRIMING_PROMPTS)}") - print(f" Holdout prompts : {len(_HOLDOUT_PROMPTS)}") + print(f" Synthetic illegal candidates : {len(_SYNTHETIC_ILLEGAL_CANDIDATES)}") + print(f" Runtime confirmation prompts : {len(_HOLDOUT_PROMPTS)}") print() - for c in cells: + for c in synthetic_cells: mark = "+" if c["cell_supported"] else "X" - print(f" {mark} {c['prompt']!r}") + print(f" {mark} synthetic {c['candidate']!r}") + print(f" guard_status : {c['realizer_guard_status']}") + print(f" guard_rule : {c['realizer_guard_rule']}") + print() + for c in runtime_cells: + mark = "+" if c["cell_supported"] else "X" + print(f" {mark} runtime {c['prompt']!r}") print(f" guard_status : {c['realizer_guard_status']}") print(f" guard_rule : {c['realizer_guard_rule']}") print(f" surface : {c['surface']!r}") - print(f" walk_surface (pre-G) : {c['walk_surface']!r}") print(f" grounding_source : {c['grounding_source']}") print() print(f" all_claims_supported : {all_supported}") @@ -136,8 +144,11 @@ def run_holdout(*, emit_json: bool = False) -> dict[str, Any]: return { "priming": list(_PRIMING_PROMPTS), - "holdout_prompts": [p for p, _ in _HOLDOUT_PROMPTS], - "cells": cells, + "synthetic_illegal_candidates": [c for c, _ in _SYNTHETIC_ILLEGAL_CANDIDATES], + "holdout_prompts": list(_HOLDOUT_PROMPTS), + "synthetic_cells": synthetic_cells, + "runtime_cells": runtime_cells, + "cells": [*synthetic_cells, *runtime_cells], "failures": failures, "all_claims_supported": all_supported, } diff --git a/generate/discourse_planner.py b/generate/discourse_planner.py index 8ae15b3a..af012632 100644 --- a/generate/discourse_planner.py +++ b/generate/discourse_planner.py @@ -246,7 +246,9 @@ class DiscoursePlan: "tag": self.intent.tag.value, "subject": self.intent.subject, "secondary_subject": self.intent.secondary_subject, + "object": self.intent.object, "relation": self.intent.relation, + "negated": self.intent.negated, "frame": self.intent.frame, }, "mode": self.mode.value, diff --git a/generate/intent.py b/generate/intent.py index 480c9e3d..04aedcd1 100644 --- a/generate/intent.py +++ b/generate/intent.py @@ -64,7 +64,9 @@ class DialogueIntent: tag: IntentTag subject: str secondary_subject: str | None = None + object: str | None = None relation: str | None = None # populated for TRANSITIVE_QUERY (ADR-0018) + negated: bool = False frame: str | None = None # populated for FRAME_TRANSFER (compose_relations) def requires_prior_turn(self) -> bool: @@ -103,6 +105,14 @@ _BELONG_QUERY_RE = re.compile( r"belong(?:s?)\b", re.IGNORECASE, ) +_DECLARATIVE_RELATION_RE = re.compile( + r"^(?P[a-z][a-z\-]*(?:\s+[a-z][a-z\-]*)?)\s+" + r"(?:(?Pdoes|do|did)\s+not\s+)?" + r"(?Preveals|reveal|grounds|ground|supports|support|" + r"requires|require|causes|cause|precedes|precede|follows|follow)\s+" + r"(?P[a-z][a-z\-]*(?:\s+[a-z][a-z\-]*)?)\.?$", + re.IGNORECASE, +) # "How does X work / function / operate / happen / exist / behave?" # — third-person mechanistic-cause query. Distinct from PROCEDURE # (which is first-person: "How do I/we/you X?") because the user is @@ -122,6 +132,8 @@ _RELATION_NORMALIZE: dict[str, str] = { "cause": "causes", "causes": "causes", "ground": "grounds", "grounds": "grounds", "reveal": "reveals", "reveals": "reveals", + "support": "supports", "supports": "supports", + "require": "requires", "requires": "requires", "mean": "means", "means": "means", "follow": "follows", "follows": "follows", "contrast": "contrasts_with", "contrast_with": "contrasts_with", @@ -267,8 +279,28 @@ def _normalize_subject(phrase: str, tag: IntentTag) -> str: return " ".join(tokens) +def _strip_confirmation_tail(text: str) -> str: + """Remove terminal discourse-confirmation tags from a proposition. + + C2 scope is deliberately narrow: strip only when a non-empty + proposition precedes the tag, so bare "no?" / "yes?" are not + rewritten into empty prompts. + """ + stripped = text.strip() + match = re.match( + r"^(?P.+?)[,.]\s*(?:right|yes|no|ok)\?\s*$", + stripped, + re.IGNORECASE, + ) + if match: + body = match.group("body").strip() + if body: + return body + return stripped + + def classify_intent(prompt: str) -> DialogueIntent: - text = prompt.strip() + text = _strip_confirmation_tail(prompt) if not text: return DialogueIntent(tag=IntentTag.UNKNOWN, subject="") @@ -334,6 +366,25 @@ def classify_intent(prompt: str) -> DialogueIntent: ), ) + declarative_match = _DECLARATIVE_RELATION_RE.match(text) + if declarative_match: + raw_relation = declarative_match.group("relation").lower().strip() + relation = _RELATION_NORMALIZE.get(raw_relation, raw_relation) + return DialogueIntent( + tag=IntentTag.VERIFICATION, + subject=_normalize_subject( + declarative_match.group("subject").strip(), IntentTag.DEFINITION + ).lower(), + secondary_subject=_normalize_subject( + declarative_match.group("object").strip(), IntentTag.DEFINITION + ).lower(), + object=_normalize_subject( + declarative_match.group("object").strip(), IntentTag.DEFINITION + ).lower(), + relation=relation, + negated=bool(declarative_match.group("neg_aux")), + ) + for pattern, tag in _RULES: match = pattern.match(text) if match: diff --git a/generate/intent_ratifier.py b/generate/intent_ratifier.py index faa7dda8..8bea3cb7 100644 --- a/generate/intent_ratifier.py +++ b/generate/intent_ratifier.py @@ -156,7 +156,9 @@ def ratify_intent( tag=IntentTag.UNKNOWN, subject=intent.subject, secondary_subject=intent.secondary_subject, + object=intent.object, relation=intent.relation, + negated=intent.negated, frame=intent.frame, ) return RatifiedIntent( diff --git a/tests/test_intent_classification_extensions.py b/tests/test_intent_classification_extensions.py index 5207f2b9..1dae1582 100644 --- a/tests/test_intent_classification_extensions.py +++ b/tests/test_intent_classification_extensions.py @@ -192,6 +192,55 @@ class TestCauseVerificationNoPackFallback: assert response.grounding_source == "none" +class TestConfirmationTagNormalization: + def test_trailing_confirmation_tag_is_stripped_from_relation_claim(self) -> None: + intent = classify_intent("Light reveals truth, right?") + assert intent.tag is IntentTag.VERIFICATION + assert intent.subject == "light" + assert intent.relation == "reveals" + assert intent.object == "truth" + assert intent.secondary_subject == "truth" + assert intent.negated is False + + def test_ok_tag_after_period_is_stripped(self) -> None: + intent = classify_intent("Knowledge supports truth. ok?") + assert intent.tag is IntentTag.VERIFICATION + assert intent.subject == "knowledge" + assert intent.relation == "supports" + assert intent.object == "truth" + + def test_content_word_right_is_not_stripped(self) -> None: + intent = classify_intent("Is right an axis?") + assert intent.tag is IntentTag.VERIFICATION + assert intent.subject == "right" + + def test_bare_leading_particle_is_not_erased(self) -> None: + intent = classify_intent("yes?") + assert intent.tag is IntentTag.UNKNOWN + assert intent.subject == "yes?" + + def test_negative_relation_claim_survives_tag_strip(self) -> None: + intent = classify_intent("Light does not reveal truth, right?") + assert intent.tag is IntentTag.VERIFICATION + assert intent.subject == "light" + assert intent.relation == "reveals" + assert intent.object == "truth" + assert intent.negated is True + + def test_runtime_relation_confirmation_gets_pack_surface(self) -> None: + response = ChatRuntime().chat("Light reveals truth, right?") + assert response.grounding_source == "pack" + assert response.realizer_guard_status == "ok" + assert response.realizer_guard_rule == "" + assert "light reveals truth" in response.surface + + def test_runtime_negative_relation_confirmation_passes_guard(self) -> None: + response = ChatRuntime().chat("Light does not reveal truth, right?") + assert response.grounding_source == "pack" + assert response.realizer_guard_status == "ok" + assert "light does not reveal truth" in response.surface + + class TestCumulativeLiftInvariant: """Pins the lift observed by the 2026-05-19 cumulative live probe: the DEFINITION-shaped prompts must produce ``pack`` grounding, diff --git a/tests/test_realizer_guard_holdout.py b/tests/test_realizer_guard_holdout.py index 0de1865f..d4a13696 100644 --- a/tests/test_realizer_guard_holdout.py +++ b/tests/test_realizer_guard_holdout.py @@ -1,11 +1,11 @@ -"""ADR-0075 (C1) — holdout cluster + byte-identity invariant tests. +"""ADR-0075/0076 (C1/C2) — hybrid holdout + byte-identity tests. These tests pin the two invariants named in the ADR: -* ``invariant_realizer_no_illegal_articulation`` — every prompt in - the C1 holdout cluster is rejected by the guard with the expected - rule_id, and the surface is replaced with the bounded disclosure - string. +* ``invariant_realizer_no_illegal_articulation`` — synthetic illegal + candidates are rejected by the guard with the expected rule_id. +* C2 confirmation prompts that used to reach the guard as illegal + candidates now produce accepted propositional surfaces. * ``invariant_realizer_guard_byte_identity_on_currently_passing_cases`` — every currently-passing cognition-lane DEFINITION prompt continues to produce a guard-accepted surface byte-identical to @@ -25,6 +25,7 @@ from core.config import RuntimeConfig from evals.realizer_guard.run_holdout import ( _HOLDOUT_PROMPTS, _PRIMING_PROMPTS, + _SYNTHETIC_ILLEGAL_CANDIDATES, run_holdout, ) from generate.realizer_guard import DISCLOSURE_SURFACE @@ -47,49 +48,30 @@ def test_holdout_cluster_all_claims_supported(holdout_report): def test_holdout_cluster_size(holdout_report): - assert len(holdout_report["cells"]) == len(_HOLDOUT_PROMPTS) - assert len(holdout_report["cells"]) == 6 + assert len(holdout_report["synthetic_cells"]) == len(_SYNTHETIC_ILLEGAL_CANDIDATES) + assert len(holdout_report["runtime_cells"]) == len(_HOLDOUT_PROMPTS) + assert len(holdout_report["runtime_cells"]) == 6 -@pytest.mark.parametrize("prompt,expected_rule", list(_HOLDOUT_PROMPTS)) -def test_each_holdout_prompt_rejected( - prompt: str, expected_rule: str, holdout_report, +@pytest.mark.parametrize("candidate,expected_rule", list(_SYNTHETIC_ILLEGAL_CANDIDATES)) +def test_each_synthetic_illegal_candidate_rejected( + candidate: str, expected_rule: str, holdout_report, ): - cell = next(c for c in holdout_report["cells"] if c["prompt"] == prompt) + cell = next(c for c in holdout_report["synthetic_cells"] if c["candidate"] == candidate) assert cell["realizer_guard_status"] == "rejected" assert cell["realizer_guard_rule"] == expected_rule - assert cell["surface"] == DISCLOSURE_SURFACE - assert cell["walk_surface"] != DISCLOSURE_SURFACE - assert cell["walk_surface"].strip() != "" - assert cell["grounding_source"] == "none" -def test_every_rejected_walk_surface_violates_R2(holdout_report): - """Sanity: each preserved pre-guard candidate must in fact - violate the rule it was rejected under (round-trip check). - - Uses the live runtime's pack POS table because the C1 rules - fail-open on unknown POS — a null-lookup would not re-trigger - R2 since the original trigger depends on the pack having - classified the offending token (e.g. ``thought``) as ``NOUN``. - """ - from chat.runtime import ChatRuntime - from core.config import RuntimeConfig - from generate.realizer_guard import check_surface - - rt = ChatRuntime(config=RuntimeConfig( - register_pack_id="default_neutral_v1", - )) - for cell in holdout_report["cells"]: - v = check_surface( - cell["walk_surface"], - pos_lookup=rt._pos_by_surface.get, - ) - assert v.status == "rejected", ( - f"walk_surface {cell['walk_surface']!r} should still " - f"violate the rule under round-trip check" - ) - assert v.rule_id == cell["realizer_guard_rule"] +@pytest.mark.parametrize("prompt", list(_HOLDOUT_PROMPTS)) +def test_each_confirmation_prompt_now_articulates( + prompt: str, holdout_report, +): + cell = next(c for c in holdout_report["runtime_cells"] if c["prompt"] == prompt) + assert cell["realizer_guard_status"] == "ok" + assert cell["realizer_guard_rule"] == "" + assert cell["grounding_source"] == "pack" + assert "pack-grounded" in cell["surface"] + assert cell["surface"] != DISCLOSURE_SURFACE # ---------- byte-identity invariant on currently-passing cases ---------- diff --git a/tests/test_realizer_guard_runtime_seam.py b/tests/test_realizer_guard_runtime_seam.py index e157b639..be55bae2 100644 --- a/tests/test_realizer_guard_runtime_seam.py +++ b/tests/test_realizer_guard_runtime_seam.py @@ -2,12 +2,8 @@ These tests exercise the hook in ``chat/runtime.py`` end-to-end: -* On the main path, a rejected candidate is replaced by - ``DISCLOSURE_SURFACE`` on ``ChatResponse.surface`` and - ``TurnEvent.surface``; ``walk_surface`` preserves the pre-guard - candidate; ``grounding_source`` is forced to ``"none"``; - ``realizer_guard_status`` and ``realizer_guard_rule`` carry the - verdict. +* C2 confirmation prompts now reach accepted propositional surfaces + while guard telemetry stays present. * The telemetry serializer surfaces both new fields. * The guard does not regress pack-grounded DEFINITION cases — those remain ``status="ok"`` byte-identical to pre-C1 behavior. @@ -46,37 +42,31 @@ def _run_holdout_sequence(rt: ChatRuntime) -> None: pipeline.run(_BUG_PROMPT) -# ---------- Main path rejection routing ---------- +# ---------- C2 confirmation prompt routing ---------- -def test_rejected_candidate_replaces_surface_with_disclosure(): +def test_confirmation_prompt_surface_is_articulated(): rt = _build_runtime() _run_holdout_sequence(rt) te = rt.turn_log[-1] - assert te.realizer_guard_status == "rejected" - assert te.surface == DISCLOSURE_SURFACE + assert te.realizer_guard_status == "ok" + assert te.surface != DISCLOSURE_SURFACE + assert "light reveals truth" in te.surface + assert "pack-grounded" in te.surface -def test_rejected_candidate_preserves_pre_guard_on_walk_surface(): +def test_confirmation_prompt_uses_pack_grounding(): rt = _build_runtime() _run_holdout_sequence(rt) te = rt.turn_log[-1] - assert te.walk_surface and te.walk_surface != DISCLOSURE_SURFACE - assert "does not thought" in te.walk_surface + assert te.grounding_source == "pack" -def test_rejected_candidate_forces_grounding_source_none(): +def test_confirmation_prompt_records_guard_ok(): rt = _build_runtime() _run_holdout_sequence(rt) te = rt.turn_log[-1] - assert te.grounding_source == "none" - - -def test_rejected_candidate_records_rule_id(): - rt = _build_runtime() - _run_holdout_sequence(rt) - te = rt.turn_log[-1] - assert te.realizer_guard_rule == "R2_aux_neg_requires_verb" + assert te.realizer_guard_rule == "" # ---------- ChatResponse mirrors TurnEvent ---------- @@ -131,8 +121,8 @@ def test_telemetry_includes_guard_fields(): record = serialize_turn_event(te) assert "realizer_guard_status" in record assert "realizer_guard_rule" in record - assert record["realizer_guard_status"] == "rejected" - assert record["realizer_guard_rule"] == "R2_aux_neg_requires_verb" + assert record["realizer_guard_status"] == "ok" + assert record["realizer_guard_rule"] == "" def test_telemetry_guard_fields_empty_on_pre_c1_events(): diff --git a/tests/test_response_mode_classifier.py b/tests/test_response_mode_classifier.py index cbeaad03..5184604e 100644 --- a/tests/test_response_mode_classifier.py +++ b/tests/test_response_mode_classifier.py @@ -166,9 +166,12 @@ class TestClassifyIntentUnchanged: def test_dialogue_intent_field_set_unchanged(self) -> None: # ResponseMode must NOT have been added as a DialogueIntent - # field. Equality on the canonical five-field shape must hold. + # field. Equality on the canonical intent shape must hold. fields = {f for f in DialogueIntent.__dataclass_fields__} - assert fields == {"tag", "subject", "secondary_subject", "relation", "frame"} + assert fields == { + "tag", "subject", "secondary_subject", "object", + "relation", "negated", "frame", + } # ---------------------------------------------------------------------------