core/generate/linguistic_pipeline/articulation.py
Shay e0d1b4754a feat(cognition): fail-closed linguistic governance + trilingual constraint pipeline
Close residual answer-authority debt: typed CoherenceRefusal/ContractViolation/
FieldFailure, structured ProofTrace (atoms→operators→closure), and Shadow Gate
paths that refuse certified answers when contract_assessment is None or geometry
is open. Add shared semantic primitives and Layers A/B/C (English/Hebrew/Koine)
as constraint producers only, with Cl(4,1) structure-sensitive embedding (no
unitize in generate/), three field outcomes, and an articulation firewall that
blocks payload-value citation bypass and uncertified content.
2026-07-20 16:16:56 -07:00

212 lines
6.6 KiB
Python

"""Proof-preserving articulation + firewall.
Every articulated claim must:
1. cite only valid citation keys (step_id or kind:symbol — never bare payload values);
2. have every content token certified by the cited steps' symbols/payloads
(or a closed function-word lexicon for English glue).
Claims failing either check are HALLUCINATED and suppressed.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from core.cognition.fail_closed import CoherenceRefusal, FailureClass, ResidualState
from core.cognition.proof_trace import ProofTrace
from core.semantic_primitives import ValidationError
from generate.linguistic_pipeline.field_integration import (
AmbiguousFieldState,
CoherentFieldState,
FieldOutcome,
)
# Do not treat sentence punctuation (.) as part of a token — otherwise
# "closed." fails certification against certified token "closed".
_TOKEN_RE = re.compile(r"[a-z0-9_:-]+", re.IGNORECASE)
# English glue only — never domain content (entities, quantities, causes).
_FUNCTION_WORDS: frozenset[str] = frozenset(
{
"a",
"an",
"the",
"is",
"are",
"was",
"were",
"be",
"been",
"of",
"to",
"for",
"and",
"or",
"as",
"in",
"on",
"at",
"by",
"with",
"from",
"that",
"this",
"these",
"those",
"it",
"its",
"class",
"operator",
"admissible",
"geometric",
"contract",
"closed",
"field",
"state",
"versor",
"condition",
"residual",
"goldtether",
"embedding",
"applied",
"selected",
"unique",
"configuration",
}
)
@dataclass(frozen=True, slots=True)
class ArticulatedClaim:
text: str
trace_refs: tuple[str, ...]
def __post_init__(self) -> None:
if not self.text.strip():
raise ValidationError("ArticulatedClaim.text must be non-empty")
if not self.trace_refs:
raise ValidationError(
"ArticulatedClaim.trace_refs must be non-empty (firewall precondition)"
)
@dataclass(frozen=True, slots=True)
class ArticulationFirewallVerdict:
legal_claims: tuple[ArticulatedClaim, ...]
hallucinated: tuple[str, ...]
surface: str
@property
def clean(self) -> bool:
return not self.hallucinated
def _content_tokens(text: str) -> frozenset[str]:
return frozenset(m.group(0).lower() for m in _TOKEN_RE.finditer(text))
def _claim_is_certified(claim: ArticulatedClaim, proof_trace: ProofTrace) -> bool:
"""True iff all refs are valid citations AND all content tokens are certified."""
citation_keys = proof_trace.all_citation_keys()
if not claim.trace_refs:
return False
if not all(ref in citation_keys for ref in claim.trace_refs):
return False
# Every ref must resolve to at least one step (no dangling synonym).
resolved = proof_trace.steps_for_refs(claim.trace_refs)
if len(resolved) == 0:
return False
if len({r for r in claim.trace_refs}) != len(claim.trace_refs):
pass # duplicates ok
# All refs must independently resolve
for ref in claim.trace_refs:
if not proof_trace.steps_for_refs((ref,)):
return False
certified = set(proof_trace.certified_content_for_refs(claim.trace_refs))
certified |= set(_FUNCTION_WORDS)
for token in _content_tokens(claim.text):
if token in certified:
continue
# Allow numeric fragments only if the exact token is certified (e.g. value text)
return False
return True
def firewall_check(
claims: tuple[ArticulatedClaim, ...] | list[ArticulatedClaim],
proof_trace: ProofTrace,
) -> ArticulationFirewallVerdict:
"""Diff claims against proof-trace citations + certified content; suppress rest."""
if not isinstance(proof_trace, ProofTrace):
raise ValidationError("firewall requires a ProofTrace")
legal: list[ArticulatedClaim] = []
hallucinated: list[str] = []
for claim in claims:
if _claim_is_certified(claim, proof_trace):
legal.append(claim)
else:
hallucinated.append(claim.text)
surface = " ".join(c.text for c in legal)
return ArticulationFirewallVerdict(
legal_claims=tuple(legal),
hallucinated=tuple(hallucinated),
surface=surface,
)
def articulate_from_proof(outcome: FieldOutcome) -> ArticulationFirewallVerdict | CoherenceRefusal:
"""Render only certified state. Ambiguous/refusal never invent answers."""
if isinstance(outcome, CoherenceRefusal):
return outcome
if isinstance(outcome, AmbiguousFieldState):
return CoherenceRefusal(
failure_class=FailureClass.AMBIGUITY,
violated_condition="unique_operator_class",
residual_state=ResidualState(detail=outcome.reason),
refusal_reason=(
"ambiguous field state — no single answer may be articulated"
),
surface_message=(
"I cannot certify a unique answer: multiple operator classes remain "
f"admissible ({', '.join(c.value for c in outcome.candidate_operator_classes)})."
),
)
if not isinstance(outcome, CoherentFieldState):
raise ValidationError("articulate_from_proof requires a FieldOutcome")
trace = outcome.proof_trace
op = outcome.selected_operator_class.value
# Claim text uses only function-word glue + tokens certified on the cited steps.
# Never invent magnitudes or entities not present on those steps.
claims = [
ArticulatedClaim(
text="The geometric contract closed.",
trace_refs=("closure:0",),
),
ArticulatedClaim(
text=f"The admissible operator class is {op}.",
trace_refs=("atom:operator",),
),
]
return firewall_check(claims, trace)
def inject_uncertified_claim(
verdict: ArticulationFirewallVerdict,
*,
bogus_text: str,
proof_trace: ProofTrace,
trace_refs: tuple[str, ...] | None = None,
) -> ArticulationFirewallVerdict:
"""Test helper: re-run firewall with an injected claim.
Default uses a totally-absent ref. Callers may pass payload-value refs
(e.g. ``("2",)``) to prove the whitelist bypass is closed.
"""
refs = trace_refs if trace_refs is not None else ("not_in_trace_ever",)
claims = list(verdict.legal_claims) + [
ArticulatedClaim(text=bogus_text, trace_refs=refs)
]
return firewall_check(tuple(claims), proof_trace)