feat(intent): normalize confirmation-tag propositions (#45)
This commit is contained in:
parent
7cc2888ed2
commit
d7499c80b3
11 changed files with 356 additions and 123 deletions
|
|
@ -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``).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
78
docs/decisions/ADR-0076-confirmation-tag-normalization.md
Normal file
78
docs/decisions/ADR-0076-confirmation-tag-normalization.md
Normal 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.
|
||||
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ----------
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue