feat(intent): normalize confirmation-tag propositions (#45)

This commit is contained in:
Shay 2026-05-19 22:55:28 -07:00 committed by GitHub
parent 7cc2888ed2
commit d7499c80b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 356 additions and 123 deletions

View file

@ -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``).

View file

@ -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,

View file

@ -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.

View file

@ -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 == <expected rule id>``
* ``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 ``<particle> does not <noun>.`` — 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,
}

View file

@ -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,

View file

@ -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<subject>[a-z][a-z\-]*(?:\s+[a-z][a-z\-]*)?)\s+"
r"(?:(?P<neg_aux>does|do|did)\s+not\s+)?"
r"(?P<relation>reveals|reveal|grounds|ground|supports|support|"
r"requires|require|causes|cause|precedes|precede|follows|follow)\s+"
r"(?P<object>[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<body>.+?)[,.]\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:

View file

@ -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(

View file

@ -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,

View file

@ -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 ----------

View file

@ -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():

View file

@ -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",
}
# ---------------------------------------------------------------------------