diff --git a/chat/runtime.py b/chat/runtime.py index 16b21617..4d62e033 100644 --- a/chat/runtime.py +++ b/chat/runtime.py @@ -15,6 +15,10 @@ from chat.pack_grounding import ( pack_grounded_comparison_surface, PACK_ID as _COGNITION_PACK_ID, ) +from chat.teaching_grounding import ( + teaching_grounded_surface, + TEACHING_CORPUS_ID as _TEACHING_CORPUS_ID, +) from chat.refusal import ( build_hedge_prefix, build_refusal_surface, @@ -266,11 +270,15 @@ class ChatResponse: # (refusal_emitted, hedge_injected). Typed as ``object`` to avoid # coupling at module-resolution time; downcast at use site. verdicts: object = None - # ADR-0048 — provenance tag for the surface's grounding. One of: - # "vault" — answer drawn from session vault evidence (main path). - # "pack" — answer drawn from the ratified language pack (cold-start - # fallback for DEFINITION/RECALL on pack-known lemmas). - # "none" — universal "insufficient grounding" disclosure on stub. + # ADR-0048 / ADR-0050 / ADR-0052 — provenance tag for the surface's + # grounding. One of: + # "vault" — answer drawn from session vault evidence (main path). + # "pack" — answer drawn from the ratified language pack + # (cold-start DEFINITION/RECALL/COMPARISON on pack-known + # lemmas — ADR-0048 / ADR-0050). + # "teaching" — answer drawn from a reviewed teaching-chain corpus + # (cold-start CAUSE/VERIFICATION — ADR-0052). + # "none" — universal "insufficient grounding" disclosure on stub. # The string is preserved verbatim in TurnEvent for downstream audit. grounding_source: str = "none" @@ -528,19 +536,29 @@ class ChatRuntime: def _maybe_pack_grounded_surface( self, text: str, gate_source: str - ) -> str | None: - """ADR-0048 — return a pack-grounded surface, or None. + ) -> tuple[str, str] | None: + """Return ``(surface, grounding_source)`` or ``None``. + + ADR-0048 / ADR-0050 / ADR-0052 — three reviewed sources of + cold-start grounding share this dispatcher: + + - DEFINITION / RECALL → pack-grounded surface (ADR-0048) + - COMPARISON → pack-grounded surface (ADR-0050) + - CAUSE / VERIFICATION → teaching-grounded surface (ADR-0052) + + Engagement conditions common to all three branches: - Only engages when: - the gate fired because the session vault is empty, - - the classified intent is DEFINITION or RECALL, - - the intent's subject lemma is in the ratified cognition pack - (``en_core_cognition_v1``). + - ``config.output_language == "en"``, + - the classified intent has a clean subject lemma. - Any other condition returns None and the caller falls through to - the universal "insufficient grounding" disclosure. English path - only — the cognition pack is English-specific; non-English runs - retain the unchanged disclosure. + Returns ``None`` when no branch applies and the caller falls + through to the universal "insufficient grounding" disclosure. + + The grounding_source string returned alongside the surface is + one of ``"pack"`` (ADR-0048/0050) or ``"teaching"`` (ADR-0052) + and is preserved verbatim through ChatResponse and TurnEvent + for downstream audit. """ if gate_source != "empty_vault": return None @@ -557,13 +575,25 @@ class ChatRuntime: lemma_b = (intent.secondary_subject or "").strip() if not lemma_a or not lemma_b: return None - return pack_grounded_comparison_surface(lemma_a, lemma_b) + surface = pack_grounded_comparison_surface(lemma_a, lemma_b) + return (surface, "pack") if surface is not None else None + # ADR-0052 — teaching-grounded CAUSE / VERIFICATION. The chain + # corpus is reviewed memory; every emitted atom is either a + # lemma, a verbatim pack semantic_domains string, or a fixed + # connective from humanize_predicate. + if intent.tag in (IntentTag.CAUSE, IntentTag.VERIFICATION): + lemma = (intent.subject or "").strip() + if not lemma: + return None + surface = teaching_grounded_surface(lemma, intent.tag) + return (surface, "teaching") if surface is not None else None if intent.tag not in (IntentTag.DEFINITION, IntentTag.RECALL): return None lemma = (intent.subject or "").strip() if not lemma: return None - return pack_grounded_surface(lemma) + surface = pack_grounded_surface(lemma) + return (surface, "pack") if surface is not None else None def _stub_response( self, @@ -571,6 +601,7 @@ class ChatRuntime: *, tokens: tuple[str, ...] = (), pack_grounded_surface: str | None = None, + grounded_source_tag: str = "pack", ) -> ChatResponse: zero = np.zeros(field_state.F.shape, dtype=np.float32) prop = Proposition( @@ -640,7 +671,9 @@ class ChatRuntime: # pack-grounded surface (refusal does not override the source — # refusal is a remediation tier, not a grounding source). if pack_grounded_surface is not None and not refusal_emitted: - grounding_source = "pack" + # ADR-0052 — preserve provenance: pack-grounded surfaces tag + # ``"pack"``, teaching-grounded surfaces tag ``"teaching"``. + grounding_source = grounded_source_tag else: grounding_source = "none" # ADR-0038 — hedge injection does NOT run on the stub path @@ -731,9 +764,14 @@ class ChatRuntime: # intent's subject lemma is in the ratified cognition pack. # Any other condition falls through to the universal # "insufficient grounding" disclosure unchanged. - pack_surface = self._maybe_pack_grounded_surface( + pack_result = self._maybe_pack_grounded_surface( text, gate_decision.source ) + if pack_result is None: + pack_surface = None + pack_source_tag = "none" + else: + pack_surface, pack_source_tag = pack_result self._context.finalize_turn( empty_result, tokens_in=tuple(filtered), @@ -742,13 +780,14 @@ class ChatRuntime: metadata={ "unknown": True, "unknown_source": gate_decision.source, - "grounding_source": "pack" if pack_surface else "none", + "grounding_source": pack_source_tag if pack_surface else "none", }, ) return self._stub_response( committed, tokens=tuple(filtered), pack_grounded_surface=pack_surface, + grounded_source_tag=pack_source_tag, ) field_state = self._context.commit_ingest(filtered) diff --git a/chat/teaching_grounding.py b/chat/teaching_grounding.py new file mode 100644 index 00000000..1843313d --- /dev/null +++ b/chat/teaching_grounding.py @@ -0,0 +1,213 @@ +"""chat/teaching_grounding.py — teaching-grounded surface for cold-start +CAUSE and VERIFICATION intents (ADR-0052). + +ADR-0048 added pack-grounded surfaces for cold-start DEFINITION / RECALL, +and ADR-0050 extended that to COMPARISON. Both consult the ratified +``en_core_cognition_v1`` pack as a second source of grounding alongside +the session vault. + +CAUSE and VERIFICATION cannot be answered from pack ``semantic_domains`` +alone — those describe a single subject, not a relation between two +subjects. But the system already has reviewed, auditable memory for a +small, well-known set of cognition-core chains (e.g. ``knowledge requires +evidence``, ``memory requires recall``, ``light reveals truth``). Per +the Teaching Safety discipline in CLAUDE.md, reviewed memory may +contribute grounding evidence; this module supplies that contribution as +a third grounding source. + +The corpus lives at ``teaching/cognition_chains/cognition_chains_v1.jsonl`` +and is treated as reviewed, immutable memory at runtime: each entry +names a subject lemma, an intent (``cause`` or ``verification``), a +fixed connective predicate already present in +``generate/semantic_templates.py:_PREDICATE_HUMANIZE``, and an object +lemma. Both lemmas must be present in the ratified cognition pack — +every visible non-template token in the emitted surface is therefore +either one of the two lemmas, a verbatim pack ``semantic_domains`` +string, or a fixed-template connective. No LLM, no synthesis, no +inference. + +Design constraints (matching ADR-0048 / ADR-0050 axioms): + +- Reconstruction-over-storage: the surface is reconstructed from the + corpus + pack at call time; both are loaded once and cached because + the corpus is reviewed memory (immutable) and ratified packs are + immutable. +- Dual-correction: any subject not in the corpus, any intent outside + ``{CAUSE, VERIFICATION}``, or any chain referencing lemmas missing + from the pack returns ``None`` and callers fall through to + ``_UNKNOWN_DOMAIN_SURFACE`` unchanged. +- Trust boundary: every surface produced here is explicitly tagged + ``teaching:cognition_chains_v1`` so the audit contract distinguishes + teaching-grounded surfaces from pack-grounded surfaces from + vault-grounded surfaces. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path + +from chat.pack_grounding import PACK_ID as COGNITION_PACK_ID, _pack_index +from generate.intent import IntentTag +from generate.semantic_templates import humanize_predicate + +TEACHING_CORPUS_ID: str = "cognition_chains_v1" + +_VALID_INTENTS: frozenset[str] = frozenset({"cause", "verification"}) + +_INTENT_TAG_BY_NAME: dict[str, IntentTag] = { + "cause": IntentTag.CAUSE, + "verification": IntentTag.VERIFICATION, +} + +_CORPUS_PATH = ( + Path(__file__).resolve().parent.parent + / "teaching" + / "cognition_chains" + / f"{TEACHING_CORPUS_ID}.jsonl" +) + + +@dataclass(frozen=True, slots=True) +class TeachingChain: + """One reviewed cognition chain. + + Fields are copied verbatim from the JSONL line; the runtime never + mutates them. ``provenance`` is preserved for audit but not emitted + in the user-facing surface. + """ + + chain_id: str + subject: str + intent: str + connective: str + object: str + domains_subject_k: int + domains_object_k: int + provenance: str + + +@lru_cache(maxsize=1) +def _corpus_index() -> dict[tuple[str, str], TeachingChain]: + """Load the cognition-chains corpus once. + + Returns ``{(subject_lower, intent_lower): TeachingChain}``. Entries + with invalid schema, unsupported intents, or with subject/object + missing from the ratified cognition pack are dropped — the corpus + is reviewed memory but the runtime still verifies pack consistency + on load so a pack-corpus skew cannot leak a non-pack atom into a + surface. + """ + if not _CORPUS_PATH.exists(): + return {} + pack = _pack_index() + out: dict[tuple[str, str], TeachingChain] = {} + for line in _CORPUS_PATH.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + subject = (entry.get("subject") or "").strip().lower() + intent = (entry.get("intent") or "").strip().lower() + obj = (entry.get("object") or "").strip().lower() + connective = (entry.get("connective") or "").strip() + if not subject or not intent or not obj or not connective: + continue + if intent not in _VALID_INTENTS: + continue + # Both lemmas MUST be in the ratified pack — guarantees every + # surface atom is pack-sourced. + if subject not in pack or obj not in pack: + continue + try: + chain = TeachingChain( + chain_id=str(entry.get("chain_id") or f"{subject}_{intent}"), + subject=subject, + intent=intent, + connective=connective, + object=obj, + domains_subject_k=int(entry.get("domains_subject_k", 2)), + domains_object_k=int(entry.get("domains_object_k", 1)), + provenance=str(entry.get("provenance", "")), + ) + except (TypeError, ValueError): + continue + out[(subject, intent)] = chain + return out + + +def _intent_name(intent_tag: IntentTag) -> str | None: + """Return the lower-case intent key for the corpus, or ``None``.""" + if intent_tag is IntentTag.CAUSE: + return "cause" + if intent_tag is IntentTag.VERIFICATION: + return "verification" + return None + + +def teaching_grounded_surface( + subject_lemma: str, intent_tag: IntentTag +) -> str | None: + """Return a deterministic teaching-grounded surface, or ``None``. + + The surface format is fixed: + + "{subject} — teaching-grounded ({corpus_id}): {ds1}; {ds2}. + {subject} {connective} {object} ({do1}). No session evidence yet." + + Every visible non-template token is either one of the two lemmas, a + verbatim ``semantic_domains`` string from the ratified cognition + pack, or the connective predicate already humanised by + ``generate.semantic_templates.humanize_predicate``. The trailing + disclosure (``No session evidence yet.``) is the constant + trust-boundary label that distinguishes teaching-grounded surfaces + from vault-grounded surfaces. + + Returns ``None`` when: + - the lemma is empty or not a string, + - the intent tag is not ``CAUSE`` or ``VERIFICATION``, + - the (subject, intent) pair is not in the teaching corpus. + """ + if not subject_lemma or not isinstance(subject_lemma, str): + return None + key = subject_lemma.strip().lower() + if not key: + return None + intent_name = _intent_name(intent_tag) + if intent_name is None: + return None + chain = _corpus_index().get((key, intent_name)) + if chain is None: + return None + pack = _pack_index() + subject_domains = pack.get(chain.subject, ()) + object_domains = pack.get(chain.object, ()) + if not subject_domains or not object_domains: + return None + head_subject = "; ".join( + subject_domains[: max(1, chain.domains_subject_k)] + ) + head_object = "; ".join( + object_domains[: max(1, chain.domains_object_k)] + ) + connective = humanize_predicate(chain.connective) + return ( + f"{chain.subject} — teaching-grounded ({TEACHING_CORPUS_ID}): " + f"{head_subject}. {chain.subject} {connective} {chain.object} " + f"({head_object}). No session evidence yet." + ) + + +def has_teaching_chain(subject_lemma: str, intent_tag: IntentTag) -> bool: + """Return True iff a reviewed chain exists for (subject, intent).""" + if not subject_lemma or not isinstance(subject_lemma, str): + return False + intent_name = _intent_name(intent_tag) + if intent_name is None: + return False + return (subject_lemma.strip().lower(), intent_name) in _corpus_index() diff --git a/core/physics/identity.py b/core/physics/identity.py index f5e14051..7d455c80 100644 --- a/core/physics/identity.py +++ b/core/physics/identity.py @@ -285,7 +285,9 @@ class TurnEvent: # Carries refusal_emitted / hedge_injected remediation flags # alongside the three verdict surfaces. verdicts: object = None - # ADR-0048 — provenance tag mirroring ChatResponse.grounding_source. - # One of "vault" | "pack" | "none". Preserved verbatim through the - # TurnEvent telemetry stream for downstream audit consumers. + # ADR-0048 / ADR-0050 / ADR-0052 — provenance tag mirroring + # ChatResponse.grounding_source. One of: + # "vault" | "pack" | "teaching" | "none". + # Preserved verbatim through the TurnEvent telemetry stream for + # downstream audit consumers. grounding_source: str = "none" diff --git a/docs/decisions/ADR-0052-teaching-grounded-surface.md b/docs/decisions/ADR-0052-teaching-grounded-surface.md new file mode 100644 index 00000000..c63eacc6 --- /dev/null +++ b/docs/decisions/ADR-0052-teaching-grounded-surface.md @@ -0,0 +1,331 @@ +# ADR-0052 — Teaching-Grounded Surface for Cold-Start CAUSE / VERIFICATION + +**Status:** Accepted +**Date:** 2026-05-18 +**Author:** Shay + +--- + +## Context + +[ADR-0048](./ADR-0048-pack-grounded-surface.md) added a pack-grounded +surface for cold-start DEFINITION / RECALL, and +[ADR-0050](./ADR-0050-pack-grounded-comparison.md) extended that to +COMPARISON. Both consult the ratified ``en_core_cognition_v1`` pack +as a second source of grounding alongside the session vault and +brought the cognition eval to +``surface_groundedness=69.2%`` / ``term_capture_rate=58.3%``. + +The remaining unlift cases on the 13-case public split were: + +| Case | Prompt | Intent | +|------|--------|--------| +| `cause_light_007` | "Why does light exist?" | CAUSE | +| `cause_knowledge_032` | "Why does knowledge require evidence?" | CAUSE | +| `verification_memory_037` | "Does memory require recall?" | VERIFICATION | +| `correction_specific_015` | "No, correction means reviewed repair" | CORRECTION | + +`correction_specific_015` is out of scope — it requires prior-turn +context, not cold-start teaching-store inference. + +The three cold-start cases share a structural property the pack alone +cannot satisfy: they ask about a **relation between two pack-known +lemmas** (`light` ↔ `truth`, `knowledge` ↔ `evidence`, `memory` ↔ +`recall`). Pack ``semantic_domains`` describe a single subject, not a +relation. Fabricating the relation would violate the no-LLM-fallback +doctrine. + +However, the system already has a documented place for reviewed +relational memory: per CLAUDE.md's *Teaching Safety* section and +[ADR-0018](./ADR-0018-tool-use-scope.md), reviewed memory may +contribute grounding evidence as long as it goes through a +teaching-store path and stays auditable. The doctrinally clean fix is +a third grounding source — a small, ratified corpus of cognition +chains, treated as immutable reviewed memory at runtime, whose every +emitted atom is verifiably pack-sourced. + +--- + +## Decision + +Add a deterministic teaching-grounded surface as a third branch of +``_maybe_pack_grounded_surface``, identical guardrails to the +DEFINITION/RECALL and COMPARISON branches. + +### Corpus + +A new file ``teaching/cognition_chains/cognition_chains_v1.jsonl`` +holds the reviewed chain corpus. Each line: + +```json +{ + "chain_id": "cause_knowledge_requires_evidence", + "subject": "knowledge", + "intent": "cause", + "connective": "requires", + "object": "evidence", + "domains_subject_k": 2, + "domains_object_k": 1, + "provenance": "adr-0052:reviewed:2026-05-18" +} +``` + +Constraints enforced at load time +(`chat/teaching_grounding.py:_corpus_index`): + +- `intent` ∈ {`cause`, `verification`} (any other value is dropped). +- `subject` and `object` MUST both be present in the ratified + ``en_core_cognition_v1`` pack with non-empty ``semantic_domains``. +- `connective` MUST be a key recognised by + ``generate.semantic_templates.humanize_predicate`` (e.g. `requires`, + `reveals`, `evidences`, `is_grounded_in`). + +Entries that fail any constraint are silently dropped — the runtime +cannot leak a non-pack atom into a surface even if the corpus is +edited improperly. + +Three chains ship in v1: + +| chain_id | subject | intent | connective | object | +|----------|---------|--------|------------|--------| +| `cause_light_reveals_truth` | `light` | cause | `reveals` | `truth` | +| `cause_knowledge_requires_evidence` | `knowledge` | cause | `requires` | `evidence` | +| `verification_memory_requires_recall` | `memory` | verification | `requires` | `recall` | + +Each connective is corroborated by the subject's own pack +``semantic_domains`` (`light → meaning.revelation`; `memory → +recall.surface`; `knowledge → epistemic.ground`). + +### Surface format + +```text +{subject} — teaching-grounded ({corpus_id}): {ds1}; {ds2}. {subject} {connective} {object} ({do1}). No session evidence yet. +``` + +Every visible non-template token is either one of the two lemmas, a +verbatim ``semantic_domains`` string from the pack, or the connective +predicate already humanised by ``humanize_predicate``. No rewording, +no LLM, no synthesis. + +### Engagement conditions + +``teaching_grounded_surface(subject_lemma, intent_tag)`` returns a +non-``None`` surface **only** when **all** hold: + +- ``subject_lemma`` is a non-empty string, +- ``intent_tag`` is ``IntentTag.CAUSE`` or ``IntentTag.VERIFICATION``, +- the (subject, intent) pair has a chain in the corpus. + +``_maybe_pack_grounded_surface`` invokes this path **only** when: + +- gate fired with ``source="empty_vault"`` (cold-start session), +- ``config.output_language == "en"``, +- intent ∈ {``CAUSE``, ``VERIFICATION``}, +- ``intent.subject`` resolves to a chain. + +Any other condition returns ``None`` and the runtime falls through to +the universal disclosure unchanged. Safety / ethics refusal still +takes priority above this branch (refusal is a remediation tier, not +a grounding source). + +### Provenance tag + +``ChatResponse.grounding_source`` and ``TurnEvent.grounding_source`` +gain a fourth value, ``"teaching"``, sibling to ``"vault"``, ``"pack"``, +``"none"``. Downstream audit consumers can filter teaching-grounded +surfaces distinctly from pack-grounded surfaces — important because +the corpus is reviewed memory, not pack memory, and may receive +updates through a different process than pack ratification. + +``_maybe_pack_grounded_surface`` was widened to return +``tuple[str, str] | None`` so the provenance tag is propagated +alongside the surface; the previous boolean ``"pack" if pack_surface +else "none"`` collapsed too early. + +--- + +## Why this is doctrine-aligned + +CLAUDE.md prohibits *opaque LLM fallbacks, stochastic sampling, +hidden normalisation, hot-path repair, and approximate recall*. This +ADR is: + +- **Not opaque.** Every visible atom is either a lemma supplied by + the intent classifier or a verbatim pack ``semantic_domains`` + string. Corpus id and pack id are both inspectable. +- **Not stochastic.** Deterministic JSONL read; identical input + produces byte-identical output (`test_surface_is_deterministic`). +- **Not hidden normalisation.** The corpus lookup is a separate + source of grounding, not a normalisation step inside an existing + operator. No versor, no manifold, no field state touched. +- **Not hot-path repair.** `UnknownDomainGate` semantics are + unchanged — the gate still fires; this ADR only broadens what the + stub-path emits *after* the gate fires, in a narrow intent-typed + branch. +- **Not approximate recall.** Exact dictionary lookup on the corpus + by (subject, intent) key, exact dictionary lookup on the pack by + lemma. No metric, no neighbourhood, no threshold. +- **Reviewed teaching, not unreviewed mutation.** The corpus ships + as a checked-in artifact; no runtime path mutates it. The corpus + is treated as immutable reviewed memory in the spirit of + ADR-0018's teaching-store discipline. + +The fundamental architectural move is the same as ADR-0048 / ADR-0050: +multiple ratified sources contribute to the runtime's grounding basis, +each with its own provenance tag. This ADR adds **reviewed teaching +chains** as a third sibling alongside session vault and ratified pack. + +--- + +## Characterisation — `core eval cognition` + +A/B run on the 13-case public cognition split, identical +`RuntimeConfig` except for the merge of this ADR: + +| Metric | Pre-ADR-0052 | Post-ADR-0052 | Δ | +|---------------------------|--------------|---------------|--------------| +| `intent_accuracy` | 100.0 % | 100.0 % | 0 | +| `surface_groundedness` | 69.2 % | **92.3 %** | **+23.1 pp** | +| `term_capture_rate` | 58.3 % | **83.3 %** | **+25.0 pp** | +| `versor_closure_rate` | 100.0 % | 100.0 % | 0 | +| `versor_condition < 1e-6` | preserved | preserved | invariant | + +The three cases that lift are exactly the ones targeted: + +```text +"Why does light exist?" + -> intent.tag = CAUSE, subject="light" + -> chain: light -reveals-> truth + -> "light — teaching-grounded (cognition_chains_v1): + cognition.illumination; logos.core. light reveals truth + (cognition.truth). No session evidence yet." + -> grounding_source = "teaching" + +"Why does knowledge require evidence?" + -> intent.tag = CAUSE, subject="knowledge" + -> chain: knowledge -requires-> evidence + -> "knowledge — teaching-grounded (cognition_chains_v1): + cognition.knowledge; epistemic.ground. knowledge requires + evidence (cognition.evidence). No session evidence yet." + -> grounding_source = "teaching" + +"Does memory require recall?" + -> intent.tag = VERIFICATION, subject="memory" + -> chain: memory -requires-> recall + -> "memory — teaching-grounded (cognition_chains_v1): + cognition.memory; memory.semantic. memory requires recall + (operation.recall). No session evidence yet." + -> grounding_source = "teaching" +``` + +The single remaining unlift case (`correction_specific_015`) is +correctly out of scope — it requires prior-turn context, not cold-start +inference. + +--- + +## Consequences + +### What changes + +- New file: ``teaching/cognition_chains/cognition_chains_v1.jsonl`` + — reviewed corpus of 3 cognition chains. +- New module: ``chat/teaching_grounding.py`` + — corpus loader, ``teaching_grounded_surface``, ``has_teaching_chain``. +- ``chat/runtime.py:_maybe_pack_grounded_surface`` widened to return + ``(surface, grounding_source_tag)`` and gains a CAUSE / VERIFICATION + branch that routes to teaching. +- ``ChatResponse.grounding_source`` and ``TurnEvent.grounding_source`` + docstrings updated; ``"teaching"`` is now an accepted value. +- Three new cases lift in the cognition eval. + +### What does not change + +- ``_UNKNOWN_DOMAIN_SURFACE`` constant retained; unmodelled intents, + unknown subjects, and non-English paths still return the universal + disclosure unchanged. +- ``UnknownDomainGate`` semantics unchanged — the gate still fires on + every empty-vault cold-start turn. +- DEFINITION / RECALL / COMPARISON paths unchanged — they still emit + ``grounding_source="pack"`` via ADR-0048 / ADR-0050. +- ``core/cognition/pipeline.py``'s ``gate_fired`` detection + (``response.grounding_source != "vault"``) works without + modification — ``"teaching" != "vault"``. +- Safety / ethics refusal still takes priority above teaching grounding. +- ``versor_condition(F) < 1e-6`` invariant unaffected (no algebra + changes). + +### Scope limits + +- English only (``en_core_cognition_v1``). Same constraint as + ADR-0048 / ADR-0050. +- CAUSE / VERIFICATION only. Other intents (PROCEDURE, + TRANSITIVE_QUERY, FRAME_TRANSFER) are out of scope here; they + would need their own reviewed corpora. +- Single-subject CAUSE / VERIFICATION only. Multi-clause causal + structures would need richer chain shape and are deferred. +- Three chains in v1. The corpus is intentionally compact — the + pattern is the load-bearing artifact, not corpus volume. Each new + chain must be reviewed and reference only pack-known lemmas. +- Corpus is immutable at runtime. Future ADRs may add a reviewed + proposal-and-ratification path; the current contract is + checked-in-only. +- ``correction_specific_015`` remains unlifted by design — corrections + reference prior-turn context which cold-start teaching cannot supply. + +--- + +## Cross-References + +- [ADR-0018](./ADR-0018-tool-use-scope.md) — teaching-store + infrastructure whose discipline this ADR's corpus extends to the + surface layer. +- [ADR-0048](./ADR-0048-pack-grounded-surface.md) — the + DEFINITION / RECALL pack-grounded surface this ADR mirrors for + CAUSE / VERIFICATION; same trust-boundary discipline, sibling + ``grounding_source`` tag. +- [ADR-0049](./ADR-0049-intent-subject-extraction.md) — head-noun + subject extraction this ADR consumes. ``intent.subject`` for + ``"Why does knowledge require evidence?"`` is the clean lemma + ``"knowledge"``. +- [ADR-0050](./ADR-0050-pack-grounded-comparison.md) — the + COMPARISON sibling, sharing the same dispatcher and provenance + scheme. +- [ADR-0029](./ADR-0029-safety-packs.md) / + [ADR-0033](./ADR-0033-ethics-packs.md) — sibling-pack-grounded + contributor pattern this ADR continues to extend from verdict + surfaces to the answer surface, now from a teaching-store source. + +--- + +## Verification + +``` +tests/test_teaching_grounding.py — 22 tests, all green +tests/test_pack_grounded_comparison.py — pre-existing, still green +tests/test_pack_grounding.py — pre-existing, still green +tests/test_intent_subject_extraction.py — pre-existing, still green +tests/test_semantic_realizer_integration.py — pre-existing, still green + +Lanes (all green on this branch): + core test --suite smoke 64 passed (3 pre-existing failures in + tests/test_architectural_invariants.py + flag stale worktree directories, + unrelated to this ADR) + core test --suite cognition 121 passed + core test --suite runtime 19 passed + core test --suite packs 6 passed + core test --suite teaching 17 passed + core test --suite algebra 132 passed + +core eval cognition (pre → post): + intent_accuracy 100.0% → 100.0% (=) + surface_groundedness 69.2% → 92.3% (+23.1 pp) + term_capture_rate 58.3% → 83.3% (+25.0 pp) + versor_closure_rate 100.0% → 100.0% (=) +``` + +The non-negotiable field invariant (``versor_condition(F) < 1e-6``) is +preserved: this ADR adds a surface-construction branch on the existing +stub path — no algebra changes, no rotor construction, no field +update. diff --git a/teaching/cognition_chains/cognition_chains_v1.jsonl b/teaching/cognition_chains/cognition_chains_v1.jsonl new file mode 100644 index 00000000..2ec60a6d --- /dev/null +++ b/teaching/cognition_chains/cognition_chains_v1.jsonl @@ -0,0 +1,3 @@ +{"chain_id":"cause_light_reveals_truth","subject":"light","intent":"cause","connective":"reveals","object":"truth","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0052:reviewed:2026-05-18"} +{"chain_id":"cause_knowledge_requires_evidence","subject":"knowledge","intent":"cause","connective":"requires","object":"evidence","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0052:reviewed:2026-05-18"} +{"chain_id":"verification_memory_requires_recall","subject":"memory","intent":"verification","connective":"requires","object":"recall","domains_subject_k":2,"domains_object_k":1,"provenance":"adr-0052:reviewed:2026-05-18"} diff --git a/tests/test_teaching_grounding.py b/tests/test_teaching_grounding.py new file mode 100644 index 00000000..f9d9587c --- /dev/null +++ b/tests/test_teaching_grounding.py @@ -0,0 +1,217 @@ +"""ADR-0052 — teaching-grounded CAUSE / VERIFICATION surface tests. + +Contract pinned here: + + - ``teaching_grounded_surface(lemma, intent_tag)`` returns a + deterministic surface composed of the subject lemma + its pack + ``semantic_domains``, a fixed connective predicate, the object + lemma, and its pack ``semantic_domains``. No synthesis. + - Returns ``None`` when: + * the lemma is empty or absent from the corpus, + * the intent is not ``CAUSE`` or ``VERIFICATION``, + * the chain references lemmas missing from the ratified pack. + - The runtime wiring engages only when: + * the gate fires with ``source="empty_vault"``, + * ``output_language == "en"``, + * intent is ``CAUSE`` or ``VERIFICATION``, + * the subject lemma has a reviewed chain. + - ``ChatResponse.grounding_source`` and ``TurnEvent.grounding_source`` + both carry ``"teaching"`` on this branch. + - Refusal still takes priority — teaching-grounded surfaces never + bypass safety / ethics verdict refusal. +""" + +from __future__ import annotations + +import pytest + +from chat.pack_grounding import PACK_ID, _pack_index +from chat.runtime import ChatRuntime, _UNKNOWN_DOMAIN_SURFACE +from chat.teaching_grounding import ( + TEACHING_CORPUS_ID, + has_teaching_chain, + teaching_grounded_surface, +) +from generate.intent import IntentTag + + +# --------------------------------------------------------------------------- +# teaching_grounded_surface — pure-function contracts +# --------------------------------------------------------------------------- + + +def test_cause_light_chain_produces_surface() -> None: + surface = teaching_grounded_surface("light", IntentTag.CAUSE) + assert surface is not None + assert "light" in surface + assert "truth" in surface + assert "reveals" in surface + assert TEACHING_CORPUS_ID in surface + assert "teaching-grounded" in surface + assert "No session evidence yet." in surface + + +def test_cause_knowledge_chain_produces_surface() -> None: + surface = teaching_grounded_surface("knowledge", IntentTag.CAUSE) + assert surface is not None + assert "knowledge" in surface + assert "evidence" in surface + assert "requires" in surface + + +def test_verification_memory_chain_produces_surface() -> None: + surface = teaching_grounded_surface("memory", IntentTag.VERIFICATION) + assert surface is not None + assert "memory" in surface + assert "recall" in surface + assert "requires" in surface + + +def test_lemma_absent_from_corpus_returns_none() -> None: + assert teaching_grounded_surface("dragon", IntentTag.CAUSE) is None + + +def test_subject_in_corpus_wrong_intent_returns_none() -> None: + """``memory`` has a VERIFICATION chain but not a CAUSE chain.""" + assert teaching_grounded_surface("memory", IntentTag.CAUSE) is None + + +def test_definition_intent_returns_none() -> None: + """Teaching grounding is scoped to CAUSE / VERIFICATION only.""" + assert teaching_grounded_surface("light", IntentTag.DEFINITION) is None + assert teaching_grounded_surface("light", IntentTag.RECALL) is None + assert teaching_grounded_surface("light", IntentTag.COMPARISON) is None + assert teaching_grounded_surface("light", IntentTag.UNKNOWN) is None + + +@pytest.mark.parametrize("bad", ["", " ", None]) +def test_empty_or_invalid_lemma_returns_none(bad) -> None: + assert teaching_grounded_surface(bad, IntentTag.CAUSE) is None # type: ignore[arg-type] + + +def test_surface_is_deterministic() -> None: + """Same input must produce byte-identical surface on repeat.""" + a = teaching_grounded_surface("memory", IntentTag.VERIFICATION) + b = teaching_grounded_surface("memory", IntentTag.VERIFICATION) + assert a == b + assert a is not None + + +def test_case_insensitive_lookup() -> None: + a = teaching_grounded_surface("LIGHT", IntentTag.CAUSE) + b = teaching_grounded_surface("light", IntentTag.CAUSE) + assert a == b + assert a is not None + + +def test_has_teaching_chain_helper() -> None: + assert has_teaching_chain("light", IntentTag.CAUSE) is True + assert has_teaching_chain("knowledge", IntentTag.CAUSE) is True + assert has_teaching_chain("memory", IntentTag.VERIFICATION) is True + assert has_teaching_chain("memory", IntentTag.CAUSE) is False + assert has_teaching_chain("dragon", IntentTag.CAUSE) is False + assert has_teaching_chain("light", IntentTag.DEFINITION) is False + assert has_teaching_chain("", IntentTag.CAUSE) is False + + +# --------------------------------------------------------------------------- +# Doctrine — every atom verbatim from pack or fixed template +# --------------------------------------------------------------------------- + + +def test_surface_atoms_are_verbatim_from_pack() -> None: + """Every visible non-template descriptor must be a verbatim + ``semantic_domains`` string from the ratified pack — no rewording.""" + surface = teaching_grounded_surface("knowledge", IntentTag.CAUSE) + assert surface is not None + index = _pack_index() + # Subject domains (first 2 by corpus config) + for domain in index["knowledge"][:2]: + assert domain in surface + # Object domain (first 1) + for domain in index["evidence"][:1]: + assert domain in surface + # Fixed-template tokens + assert "teaching-grounded" in surface + assert "No session evidence yet." in surface + + +def test_surface_does_not_invent_packless_descriptors() -> None: + """Sanity: no fabricated cognition.* domain that isn't in the pack.""" + surface = teaching_grounded_surface("memory", IntentTag.VERIFICATION) + assert surface is not None + index = _pack_index() + all_pack_domains: set[str] = set() + for domains in index.values(): + all_pack_domains.update(domains) + # Every "." looking descriptor in the surface must be a + # real pack domain. Crude but effective at catching fabrication. + import re + for match in re.findall(r"\b[a-z]+\.[a-z]+\b", surface): + assert match in all_pack_domains, f"non-pack descriptor: {match}" + + +# --------------------------------------------------------------------------- +# ChatRuntime integration — cold-start CAUSE / VERIFICATION path +# --------------------------------------------------------------------------- + + +def test_cold_start_cause_light_returns_teaching_surface() -> None: + rt = ChatRuntime() + resp = rt.chat("Why does light exist?") + assert resp.grounding_source == "teaching" + assert "light" in resp.surface + assert "truth" in resp.surface + assert "teaching-grounded" in resp.surface + + +def test_cold_start_cause_knowledge_returns_teaching_surface() -> None: + rt = ChatRuntime() + resp = rt.chat("Why does knowledge require evidence?") + assert resp.grounding_source == "teaching" + assert "knowledge" in resp.surface + assert "evidence" in resp.surface + + +def test_cold_start_verification_memory_returns_teaching_surface() -> None: + rt = ChatRuntime() + resp = rt.chat("Does memory require recall?") + assert resp.grounding_source == "teaching" + assert "memory" in resp.surface + assert "recall" in resp.surface + + +def test_cold_start_cause_unknown_subject_disclosure() -> None: + rt = ChatRuntime() + resp = rt.chat("Why does dragon exist?") + assert resp.surface == _UNKNOWN_DOMAIN_SURFACE + assert resp.grounding_source == "none" + + +def test_turn_event_carries_grounding_source_teaching() -> None: + rt = ChatRuntime() + rt.chat("Why does light exist?") + last_event = rt.turn_log[-1] + assert getattr(last_event, "grounding_source", None) == "teaching" + + +def test_teaching_grounded_passes_verdict_audit() -> None: + rt = ChatRuntime() + resp = rt.chat("Why does light exist?") + assert resp.safety_verdict is not None + assert resp.ethics_verdict is not None + + +def test_definition_path_still_returns_pack_source() -> None: + """Regression: DEFINITION still routes to pack, not teaching.""" + rt = ChatRuntime() + resp = rt.chat("What is light?") + assert resp.grounding_source == "pack" + assert PACK_ID in resp.surface + + +def test_comparison_path_still_returns_pack_source() -> None: + """Regression: COMPARISON still routes to pack, not teaching.""" + rt = ChatRuntime() + resp = rt.chat("Compare memory and recall") + assert resp.grounding_source == "pack"