From 6761fc097435d32c29a3124a582d21e14aa2cd4a Mon Sep 17 00:00:00 2001 From: Shay Date: Wed, 20 May 2026 11:11:28 -0700 Subject: [PATCH] =?UTF-8?q?feat(realizer):=20C1.5=20=E2=80=94=20articulati?= =?UTF-8?q?on=20legality=20at=20the=20realizer=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a typed legality check that catches a narrow class of incoherent finite-predicate surfaces before they ship. Scope is deliberately narrow: - generate/articulation_legality.py: - SlotKind enum {VERB, NON_VERB, UNKNOWN} - ArticulationLegality enum {LEGAL, ILLEGAL_NON_VERB_FINITE_PREDICATE} - classify_predicate_slot_kind() — token allowlists for known verbs and known non-verb nouns - validate_finite_predicate_legality() — fails on negated + NON_VERB; fail-open on UNKNOWN to preserve canary behavior - generate/templates.py: - _inflect_predicate: copular-aware negation ("is X" -> "is not X" instead of the default "does not be X") - render_step: invokes the legality validator; returns "I cannot realize that proposition coherently yet." when an illegal shape is detected The check is upstream of register / anchor-lens transforms (presentation + substantive axes both downstream of the realizer); no interaction with R6 / ADR-0073 layering. Tests pin: - NON_VERB + negated -> ILLEGAL_NON_VERB_FINITE_PREDICATE - UNKNOWN + negated -> LEGAL (fail-open preserved) - render_step returns the disclosure string when illegal detected - render_step still produces the fall-through surface on UNKNOWN Validation: - Cognition eval byte-identical (100/100/91.7/100) - 370 realizer / lens / register / pack / lane tests pass - anchor-lens-tour + register-tour both green --- generate/articulation_legality.py | 101 ++++++++++++++++++++++++++++ generate/templates.py | 26 +++++++ tests/test_articulation_legality.py | 50 ++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 generate/articulation_legality.py create mode 100644 tests/test_articulation_legality.py diff --git a/generate/articulation_legality.py b/generate/articulation_legality.py new file mode 100644 index 00000000..82e204be --- /dev/null +++ b/generate/articulation_legality.py @@ -0,0 +1,101 @@ +"""Typed articulation legality checks at the realizer boundary. + +C1.5 scope: + - Catch a narrow class of known-illegal finite-predicate shapes. + - Fail open when predicate slot kind is unknown. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + + +class SlotKind(Enum): + VERB = "verb" + NON_VERB = "non_verb" + UNKNOWN = "unknown" + + +class ArticulationLegality(Enum): + LEGAL = "legal" + ILLEGAL_NON_VERB_FINITE_PREDICATE = "illegal_non_verb_finite_predicate" + + +@dataclass(frozen=True, slots=True) +class ArticulationLegalityVerdict: + legality: ArticulationLegality + predicate_kind: SlotKind + + @property + def is_legal(self) -> bool: + return self.legality is ArticulationLegality.LEGAL + + +_KNOWN_NON_VERB_PREDICATES: frozenset[str] = frozenset( + { + "right", + "truth", + "light", + "knowledge", + "wisdom", + "evidence", + "thought", + "memory", + } +) + +_KNOWN_VERB_PREDICATES: frozenset[str] = frozenset( + { + "is", + "are", + "has", + "have", + "belongs", + "supports", + "requires", + "grounds", + "reveals", + "defines", + "means", + "follows", + "precedes", + "causes", + "answers", + "verifies", + "evidences", + } +) + + +def classify_predicate_slot_kind(predicate_humanized: str) -> SlotKind: + token = predicate_humanized.strip().split(" ", 1)[0].lower() + if token in _KNOWN_VERB_PREDICATES: + return SlotKind.VERB + if token in _KNOWN_NON_VERB_PREDICATES: + return SlotKind.NON_VERB + return SlotKind.UNKNOWN + + +def validate_finite_predicate_legality( + *, + predicate_humanized: str, + negated: bool, +) -> ArticulationLegalityVerdict: + kind = classify_predicate_slot_kind(predicate_humanized) + if not negated: + return ArticulationLegalityVerdict( + legality=ArticulationLegality.LEGAL, + predicate_kind=kind, + ) + if kind is SlotKind.NON_VERB: + return ArticulationLegalityVerdict( + legality=ArticulationLegality.ILLEGAL_NON_VERB_FINITE_PREDICATE, + predicate_kind=kind, + ) + # Fail-open by design for UNKNOWN to preserve canary behavior. + return ArticulationLegalityVerdict( + legality=ArticulationLegality.LEGAL, + predicate_kind=kind, + ) + diff --git a/generate/templates.py b/generate/templates.py index c278e5b6..944d93b9 100644 --- a/generate/templates.py +++ b/generate/templates.py @@ -12,6 +12,10 @@ consumes these as constraints rather than final output. from __future__ import annotations +from generate.articulation_legality import ( + ArticulationLegality, + validate_finite_predicate_legality, +) from generate.graph_planner import RhetoricalMove from generate.morphology import base_form, past_participle, past_tense, present_participle @@ -142,6 +146,10 @@ def _inflect_predicate( instead of "all molecule binds enzyme" (english_fluency_ood G2). """ verb = predicate_h + copular = any( + predicate_h.startswith(prefix) + for prefix in ("is ", "are ", "has ", "have ", "belongs ") + ) base = base_form(verb) match (aspect, tense, negated, plural_subject): @@ -163,6 +171,18 @@ def _inflect_predicate( return f"will {base}" case (_, _, True, True): return f"do not {base}" + case (_, _, True, False) if copular: + if predicate_h.startswith("is "): + return "is not " + predicate_h[3:] + if predicate_h.startswith("are "): + return "are not " + predicate_h[4:] + if predicate_h.startswith("has "): + return "has not " + predicate_h[4:] + if predicate_h.startswith("have "): + return "have not " + predicate_h[5:] + if predicate_h.startswith("belongs "): + return "does not belong " + predicate_h[8:] + return f"is not {base}" case (_, _, True, False): return f"does not {base}" case (_, _, False, True): @@ -191,6 +211,12 @@ def render_step( is_mass = is_mass_noun(subject) plural = plural_q and not is_mass predicate_h = _humanize_predicate(predicate) + legality = validate_finite_predicate_legality( + predicate_humanized=predicate_h, + negated=negated, + ) + if legality.legality is ArticulationLegality.ILLEGAL_NON_VERB_FINITE_PREDICATE: + return "I cannot realize that proposition coherently yet." predicate_h = _inflect_predicate( predicate_h, negated=negated, tense=tense, aspect=aspect, diff --git a/tests/test_articulation_legality.py b/tests/test_articulation_legality.py new file mode 100644 index 00000000..5479c19c --- /dev/null +++ b/tests/test_articulation_legality.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from generate.articulation_legality import ( + ArticulationLegality, + SlotKind, + validate_finite_predicate_legality, +) +from generate.graph_planner import RhetoricalMove +from generate.templates import render_step + + +def test_c15_rejects_known_nonverb_finite_predicate_under_negation() -> None: + verdict = validate_finite_predicate_legality( + predicate_humanized="thought", + negated=True, + ) + assert verdict.legality is ArticulationLegality.ILLEGAL_NON_VERB_FINITE_PREDICATE + assert verdict.predicate_kind is SlotKind.NON_VERB + + +def test_c15_fail_open_on_unknown_predicate_kind() -> None: + verdict = validate_finite_predicate_legality( + predicate_humanized="quuxified", + negated=True, + ) + assert verdict.legality is ArticulationLegality.LEGAL + assert verdict.predicate_kind is SlotKind.UNKNOWN + + +def test_render_step_discloses_when_illegal_shape_detected() -> None: + surface = render_step( + RhetoricalMove.ASSERT, + "right", + "thought", + "truth", + negated=True, + ) + assert surface == "I cannot realize that proposition coherently yet." + + +def test_render_step_keeps_unknown_predicate_fail_open() -> None: + surface = render_step( + RhetoricalMove.ASSERT, + "signal", + "quuxified", + "truth", + negated=True, + ) + assert surface == "signal does not quuxified truth" +