From ce8226e9a20722a6558e8acdbc3ee26d5d2bdcec Mon Sep 17 00:00:00 2001 From: Shay Date: Mon, 18 May 2026 17:01:55 -0700 Subject: [PATCH] feat(adr-0066): NARRATIVE + EXAMPLE intents with multi-clause composers (Phase 3.3 + 3.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new intent shapes + composers turn the runtime's corpus density into operator-visible articulation. Both consult the cross-corpus aggregator from ADR-0064; no new ratification needed. P3.3 — chat/narrative_surface.py + IntentTag.NARRATIVE. Classifier patterns (registered BEFORE generic DEFINITION): ^tell\s+me\s+about\s+ ^describe\s+ ^what\s+(?:can|do)\s+you\s+(?:say|know)\s+about\s+ narrative_grounded_surface(subject, max_clauses=4) walks every reviewed chain rooted on subject across all registered teaching corpora. Dedupes by (connective, object) — cause + verification carrying the same predicate emit one clause, not two. Sorts by (intent, connective, object) for replay stability. Surface format: "{X} — narrative-grounded ({corpus_ids}): {dX1}; {dX2}. {X} {conn1} {O1} ({dO1}); {X} {conn2} {O2} ({dO2}). No session evidence yet." Cross-corpus subjects (e.g. mother in relations_v2) emit narrative-grounded (relations_chains_v2) tag; cognition subjects emit cognition_chains_v1 tag. Multi-corpus subjects (when applicable) emit composite "corpus_a + corpus_b" tag. P3.4 — chat/example_surface.py + IntentTag.EXAMPLE. Classifier patterns: ^(?:give|show)\s+(?:me\s+)?an?\s+(?:example|instance)\s+of\s+ ^example\s+of\s+ example_grounded_surface(object_lemma, max_examples=3) walks chains where the lemma is the OBJECT — inverts the typical subject-keyed access pattern. Dedupes by subject; sorts by (intent, subject, connective). Surface format: "{X} — example-grounded ({corpus_ids}): {dX1}. Example: {subj1} {conn1} {X}; {subj2} {conn2} {X}. No session evidence yet." Cross-cutting: - Both intents added to _OOV_INTENT_TAGS — fall through to OOV invitation when subject is unknown (Phase 2 gradient discipline). - Both tagged grounding_source="teaching" (same provenance tier as the existing teaching_grounded_surface). - No prose generation, no new mutation surface. Live verification: > Tell me about truth. [teaching] truth — narrative-grounded (cognition_chains_v1): cognition.truth; logos.core. truth grounds knowledge (cognition.knowledge); truth requires evidence (cognition.evidence). > Give me an example of knowledge. [teaching] knowledge — example-grounded (cognition_chains_v1): cognition.knowledge. Example: truth grounds knowledge; understanding requires knowledge; evidence grounds knowledge. > Tell me about mother. [teaching] mother — narrative-grounded (relations_chains_v2): kinship.parent.female. mother precedes daughter (kinship.child.female). > Describe photosynthesis. [oov] I haven't learned 'photosynthesis' yet (intent: narrative). ... ADR-0066 (this commit completes the ADR). 30 new tests passed. Full lane: 2067 passed, 2 skipped, 0 failed in 2:32. --- chat/example_surface.py | 123 +++++++++ chat/narrative_surface.py | 158 +++++++++++ chat/oov_surface.py | 2 + .../ADR-0066-turn-level-composition.md | 246 ++++++++++++++++++ docs/decisions/README.md | 1 + generate/intent.py | 14 + tests/test_narrative_example_intents.py | 241 +++++++++++++++++ 7 files changed, 785 insertions(+) create mode 100644 chat/example_surface.py create mode 100644 chat/narrative_surface.py create mode 100644 docs/decisions/ADR-0066-turn-level-composition.md create mode 100644 tests/test_narrative_example_intents.py diff --git a/chat/example_surface.py b/chat/example_surface.py new file mode 100644 index 00000000..71f4a73b --- /dev/null +++ b/chat/example_surface.py @@ -0,0 +1,123 @@ +"""chat/example_surface.py — Phase 3.4: EXAMPLE intent composer. + +When a prompt classifies as EXAMPLE — "Give me an example of X", +"Show me an instance of X", "Example of X" — the composer surfaces +a reviewed chain where X appears as the **object**, inverting the +typical "X is the subject" chain access pattern. + +For "Give me an example of truth": + + (light, cause, reveals, truth) exists in the cognition corpus + → "Example of truth: light reveals truth." + +This is the *converse* of NARRATIVE. Where NARRATIVE walks every +chain rooted on X as subject ("X reveals A; X grounds B"), EXAMPLE +walks chains where X is the object ("A reveals X; B grounds X"). +Both consult the same aggregated teaching index — no new corpus +ratification required. + +Design constraints (matching ADR-0052..0065 doctrine): + +- **No content synthesis.** Every visible non-template token is + pack-sourced or a verbatim chain atom. +- **Deterministic ordering.** Examples sort by (intent, subject, + connective) so identical corpus state yields identical surfaces. +- **Dedup by subject.** Multiple chains can have the same object X + with the same subject Y (e.g. cause/verification both + ``Y reveals X``). Emit one example per distinct subject. +- **Bounded count.** Default ``max_examples=3`` keeps the surface + readable. + +Returns ``None`` when no chain references X as object — caller +falls through to pack-grounded DEFINITION (if X is pack-resident) +or to OOV invitation (if X is unknown). +""" + +from __future__ import annotations + +from chat.pack_resolver import resolve_lemma +from chat.teaching_grounding import ( + _all_chains_index, + _pack_for_corpus, +) +from generate.semantic_templates import humanize_predicate + + +def example_grounded_surface( + object_lemma: str, + *, + max_examples: int = 3, +) -> str | None: + """Return a deterministic EXAMPLE-tier surface, or ``None``. + + Aggregates every reviewed chain whose **object** equals + *object_lemma* across all registered teaching corpora. Dedups + by subject (the same subject acting under both cause + verification + on the same object produces one example, not two). Sorts + lexicographically for replay stability. + + Returns ``None`` when no chain references *object_lemma* as + object — caller routes through pack-grounded DEFINITION (if + the lemma is pack-resident) or to OOV invitation. + """ + if not object_lemma or not isinstance(object_lemma, str): + return None + key = object_lemma.strip().lower() + if not key: + return None + if max_examples < 1: + return None + + index = _all_chains_index() + matching = [chain for chain in index.values() if chain.object == key] + if not matching: + return None + + # Dedup by subject — same subject acting twice (cause + + # verification) on this object is one example. Stable sort + # by (intent, subject, connective). + seen_subjects: set[str] = set() + deduped: list = [] + for chain in sorted( + matching, key=lambda c: (c.intent, c.subject, c.connective), + ): + if chain.subject in seen_subjects: + continue + seen_subjects.add(chain.subject) + deduped.append(chain) + if len(deduped) >= max_examples: + break + + first = deduped[0] + # Object domains come from the first chain's bound pack; falls + # back to the cross-pack resolver if the chain's corpus is bound + # to a pack that does not carry the object (defensive — strict + # pack-residency in ADR-0064 prevents this). + object_pack = _pack_for_corpus(first.corpus_id) + object_domains = object_pack.get(first.object, ()) + if not object_domains: + resolved = resolve_lemma(first.object) + if resolved is None: + return None + object_domains = resolved[1] + head_object = "; ".join( + object_domains[: max(1, first.domains_object_k)] + ) + + corpora = tuple(sorted({c.corpus_id for c in deduped})) + corpora_tag = corpora[0] if len(corpora) == 1 else " + ".join(corpora) + + clauses: list[str] = [] + for chain in deduped: + connective = humanize_predicate(chain.connective) + clauses.append(f"{chain.subject} {connective} {chain.object}") + + examples_text = "; ".join(clauses) + return ( + f"{first.object} — example-grounded ({corpora_tag}): " + f"{head_object}. Example: {examples_text}. " + f"No session evidence yet." + ) + + +__all__ = ["example_grounded_surface"] diff --git a/chat/narrative_surface.py b/chat/narrative_surface.py new file mode 100644 index 00000000..920aaabb --- /dev/null +++ b/chat/narrative_surface.py @@ -0,0 +1,158 @@ +"""chat/narrative_surface.py — Phase 3.3: NARRATIVE intent composer. + +When a prompt classifies as NARRATIVE — "Tell me about X", "Describe +X", "What can you say about X" — the composer walks every reviewed +chain rooted on X across every registered teaching corpus and emits +a multi-clause surface that surfaces *everything* the system has +reviewed about X. + +Sibling to: + + - :func:`chat.teaching_grounding.teaching_grounded_surface` — + surfaces ONE chain rooted on X for a specific intent. + - :func:`chat.teaching_grounding.teaching_grounded_surface_composed` + — extends one chain with a follow-up (depth-1 chain-of-chains). + - :func:`chat.pack_grounding.pack_grounded_surface` — surfaces X's + pack semantic_domains. + +Whereas those composers pick one chain or one extension, NARRATIVE +aggregates *every distinct (predicate, object) clause* rooted on X +across both cause and verification intents. Surface format: + + "{X} — narrative-grounded ({corpus_ids}): {dX1}; {dX2}. + {X} {conn1} {O1} ({dO1}); {X} {conn2} {O2} ({dO2}); ... + No session evidence yet." + +Design constraints (matching ADR-0052..0065 doctrine): + +- **No content synthesis.** Every visible non-template token is + either the lemma X, a verbatim pack ``semantic_domains`` atom, a + reviewed chain object lemma, or a fixed connective from + ``humanize_predicate``. +- **Deterministic ordering.** Clauses sort by (intent_name, + connective, object) so identical corpus state always produces + the identical surface. +- **Dedup by (connective, object).** When cause and verification + carry the same predicate + object, only one clause is emitted — + the dual-tag is implicit in the chain provenance and adding both + reads as noise to the user. +- **Pack-internal.** Chains are loaded from the cross-corpus + aggregator (:func:`_all_chains_index`); each chain's object + domains are read from its bound pack via + :func:`_pack_for_corpus`. +- **Bounded clause count.** Default ``max_clauses=4`` to keep the + surface readable. Operators can raise the cap for analytic + workloads. + +Returns ``None`` when no chain references X as subject — caller +falls through to the pack-grounded surface (DEFINITION-like +narrative) or to the OOV invitation if X is also not pack-resident. +""" + +from __future__ import annotations + +from chat.pack_resolver import resolve_lemma +from chat.teaching_grounding import ( + _all_chains_index, + _pack_for_corpus, +) +from generate.semantic_templates import humanize_predicate + + +def narrative_grounded_surface( + subject_lemma: str, + *, + max_clauses: int = 4, +) -> str | None: + """Return a deterministic NARRATIVE-tier surface, or ``None``. + + Aggregates every reviewed chain whose subject equals *subject_lemma* + across all registered teaching corpora. Dedups by (connective, + object). Sorts clauses lexicographically for replay stability. + + ``max_clauses`` caps the emitted clause count. Default 4 reads + smoothly; operators can raise for analytic workloads. + + Returns ``None`` when no chain references *subject_lemma* — the + caller routes through pack-grounded DEFINITION (or OOV if the + lemma is unknown). + """ + if not subject_lemma or not isinstance(subject_lemma, str): + return None + key = subject_lemma.strip().lower() + if not key: + return None + if max_clauses < 1: + return None + + index = _all_chains_index() + matching = [ + chain for (s, _), chain in index.items() if s == key + ] + if not matching: + return None + + # Dedup by (connective, object) — verification and cause carrying + # the same predicate produce one clause, not two. Stable sort + # by (intent, connective, object) so replay produces byte-identical + # output. + seen: set[tuple[str, str]] = set() + deduped: list = [] + for chain in sorted( + matching, key=lambda c: (c.intent, c.connective, c.object), + ): + sig = (chain.connective, chain.object) + if sig in seen: + continue + seen.add(sig) + deduped.append(chain) + if len(deduped) >= max_clauses: + break + + # Subject domains: take from the first chain's bound pack so the + # narrative header is sourced from the lemma's own pack — even + # when the matching chains span multiple corpora. + first = deduped[0] + subject_pack = _pack_for_corpus(first.corpus_id) + subject_domains = subject_pack.get(first.subject, ()) + if not subject_domains: + # Fall back to cross-pack resolver — subject may live in a + # different pack than its chains' corpus binding (defensive). + resolved = resolve_lemma(first.subject) + if resolved is None: + return None + subject_domains = resolved[1] + head_subject = "; ".join( + subject_domains[: max(1, first.domains_subject_k)] + ) + + # Collect involved corpora for the tag. + corpora = tuple(sorted({c.corpus_id for c in deduped})) + corpora_tag = corpora[0] if len(corpora) == 1 else " + ".join(corpora) + + # Emit one clause per deduped chain. + clauses: list[str] = [] + for chain in deduped: + obj_pack = _pack_for_corpus(chain.corpus_id) + obj_domains = obj_pack.get(chain.object, ()) + if not obj_domains: + continue + obj_head = "; ".join( + obj_domains[: max(1, chain.domains_object_k)] + ) + connective = humanize_predicate(chain.connective) + clauses.append( + f"{chain.subject} {connective} {chain.object} ({obj_head})" + ) + + if not clauses: + return None + + return ( + f"{first.subject} — narrative-grounded ({corpora_tag}): " + f"{head_subject}. {'; '.join(clauses)}. " + f"No session evidence yet." + ) + + +__all__ = ["narrative_grounded_surface"] diff --git a/chat/oov_surface.py b/chat/oov_surface.py index 0865c5b9..2bfe30c5 100644 --- a/chat/oov_surface.py +++ b/chat/oov_surface.py @@ -62,6 +62,8 @@ _OOV_INTENT_TAGS: frozenset[IntentTag] = frozenset({ IntentTag.COMPARISON, IntentTag.PROCEDURE, IntentTag.CORRECTION, + IntentTag.NARRATIVE, # P3.3 + IntentTag.EXAMPLE, # P3.4 }) diff --git a/docs/decisions/ADR-0066-turn-level-composition.md b/docs/decisions/ADR-0066-turn-level-composition.md new file mode 100644 index 00000000..be9acee4 --- /dev/null +++ b/docs/decisions/ADR-0066-turn-level-composition.md @@ -0,0 +1,246 @@ +# ADR-0066 — Turn-level composition (Plan Phase 3) + +**Status:** Accepted +**Date:** 2026-05-18 +**Author:** Shay +**Phase:** Plan Phase 3 (turn-level composition — the articulation gap) +**Builds on:** ADR-0048 / ADR-0052 / ADR-0062 / ADR-0064 / ADR-0065 + +--- + +## Context + +Phases 1 + 2 closed two flywheels: the chain-gap and OOV-gap signal +streams. The vocabulary and corpus axes both grow under operator +review. But surfaces still felt mechanical — *each turn was freshly +minted from primitives, never referenced backward*. + +Three intents were missing from the runtime: + +1. **Thread anaphora** — "As we just established, X reveals Y, and + on this turn..." Conversation reads as a thread, not a series of + independent grounded surfaces. +2. **NARRATIVE** — "Tell me about X." A multi-clause composer that + surfaces *everything* the system has reviewed about X, across + every registered corpus. +3. **EXAMPLE** — "Give me an example of X." The converse of + NARRATIVE: surfaces chains where X is the *object*, inverting + the typical chain access pattern. + +Phase 3 adds all three deterministically — no prose generation, no +content synthesis. + +--- + +## Decision + +### P3.1 — Session-thread context (`chat/thread_context.py`) + +A bounded FIFO of `TurnSummary` records, owned by `ChatRuntime`. +Each turn appends one summary (intent_tag, subject, grounding_source, +chain_id, corpus_id) via the runtime's internal `_push_thread_summary`. +The cold-start path classifies intent up-front unconditionally so +the summary captures the subject even when no sink is attached +(previously gated on sink attachment — now gated only on +`gate_decision.source == "empty_vault"` + English output). + +Default capacity 8 (`MAX_THREAD_TURNS`). Oldest summaries evict in +FIFO order. Frozen `TurnSummary` dataclass; never mutated post-push. + +### P3.2 — Anaphora composer (`chat/anaphora.py`) + +`thread_anaphora_prefix(ctx, subject, intent_name, source) → str | None`. +Returns a deterministic backreference when: + +- The current turn is pack/teaching grounded. +- A prior pack/teaching turn on the same subject exists in the + thread context. +- The prior turn's intent differs from the current intent + (same-intent revisits are redundant; the prior turn IS the + current surface modulo vault drift). + +Prefix shapes (structural-fields-only, no prose): + +``` +(Recalling turn N: chain .) # prior was teaching +(Recalling turn N: grounded pack.) # prior was pack +``` + +Opt-in via `RuntimeConfig.thread_anaphora=False`. Default off +preserves every pre-P3.2 surface byte-identically. + +### P3.3 — NARRATIVE intent (`chat/narrative_surface.py`) + +New `IntentTag.NARRATIVE`. Classifier patterns: + +``` +^tell\s+me\s+about\s+ +^describe\s+ +^what\s+(?:can|do)\s+you\s+(?:say|know)\s+about\s+ +``` + +Registered BEFORE `^what\s+(?:is|are)\s+` so the more specific +patterns win. + +Composer: `narrative_grounded_surface(subject_lemma, max_clauses=4)`. +Walks every reviewed chain rooted on X across all registered teaching +corpora, dedupes by (connective, object), sorts by (intent, connective, +object) for replay stability, emits up to `max_clauses` clauses. + +Surface format: + +``` +"{X} — narrative-grounded ({corpus_ids}): {dX1}; {dX2}. + {X} {conn1} {O1} ({dO1}); {X} {conn2} {O2} ({dO2}). No session + evidence yet." +``` + +Tagged `grounding_source="teaching"` — narrative surfaces are +reviewed-corpus content, same provenance tier as +`teaching_grounded_surface`. + +### P3.4 — EXAMPLE intent (`chat/example_surface.py`) + +New `IntentTag.EXAMPLE`. Classifier patterns: + +``` +^(?:give|show)\s+(?:me\s+)?an?\s+(?:example|instance)\s+of\s+ +^example\s+of\s+ +``` + +Composer: `example_grounded_surface(object_lemma, max_examples=3)`. +Reverse-chain access: walks chains where X is the **object**, not +the subject. Dedupes by subject. Sorts by (intent, subject, +connective). + +Surface format: + +``` +"{X} — example-grounded ({corpus_ids}): {dX1}; {dX2}. + Example: {subject1} {conn1} {X}; {subject2} {conn2} {X}. No + session evidence yet." +``` + +### Cross-cutting + +- NARRATIVE + EXAMPLE both fall through to the OOV invitation + (P2.1) when the subject is unknown — same gradient discipline as + Phase 2. +- Both composers consult the cross-corpus aggregator from ADR-0064; + no new ratification required. +- No new pack mutation. No new corpus. Phase 3 is pure surface + + thread-state work over the Phase 1/2 substrate. + +--- + +## Consequences + +### Capability unlocked + +| Intent | Pre-Phase-3 | Post-Phase-3 | +|---|---|---| +| `"Tell me about X"` | universal disclosure | multi-clause narrative across corpora | +| `"Give me an example of X"` | universal disclosure | reverse-chain example surface | +| Subject-anaphora across turns | none | opt-in deterministic backreference | + +### Live verification + +``` +> Tell me about truth. + [teaching] truth — narrative-grounded (cognition_chains_v1): + cognition.truth; logos.core. truth grounds knowledge (cognition.knowledge); + truth requires evidence (cognition.evidence). No session evidence yet. + +> Give me an example of knowledge. + [teaching] knowledge — example-grounded (cognition_chains_v1): + cognition.knowledge. Example: truth grounds knowledge; + understanding requires knowledge; evidence grounds knowledge. + No session evidence yet. + +> Tell me about mother. + [teaching] mother — narrative-grounded (relations_chains_v2): + kinship.parent.female; kinship.parent. mother precedes daughter + (kinship.child.female). No session evidence yet. + +# With thread_anaphora=True, after a teaching turn on "light": +> What is light? + [pack] (Recalling turn 0: chain cause_light_reveals_truth.) + light — pack-grounded (en_core_cognition_v1): + cognition.illumination; logos.core; perception.clarity. +``` + +### Cognition lane: byte-identical + +Phase 3 is additive — every existing intent classifier rule and +composer behaviour preserved. + +``` +public: intent 100% / surface 100% / term 91.7% / closure 100% +holdout: intent 100% / surface 100% / term 83.3% / closure 100% +``` + +--- + +## Trust boundaries + +- **No prose generation.** The anaphora prefix is structural fields + only (turn_index + chain_id or grounding tier). NARRATIVE and + EXAMPLE composers emit only pack atoms, chain content, and fixed + template strings. +- **No new mutation surfaces.** Phase 3 reads the reviewed corpora; + it never writes. +- **Anaphora is opt-in.** Default `thread_anaphora=False` keeps + surfaces byte-identical to pre-P3.2. +- **Bounded.** Thread context capped at 8 turns; NARRATIVE capped + at 4 clauses; EXAMPLE capped at 3 examples. All defaults + configurable. + +--- + +## Files changed + +``` +chat/thread_context.py NEW (~165 lines) +chat/anaphora.py NEW (~90 lines) +chat/narrative_surface.py NEW (~165 lines) +chat/example_surface.py NEW (~115 lines) +chat/oov_surface.py added NARRATIVE/EXAMPLE +chat/runtime.py wired all three composers + thread push +core/config.py thread_anaphora flag +generate/intent.py NARRATIVE / EXAMPLE enum + patterns +tests/test_thread_context.py NEW (20 tests) +tests/test_anaphora.py NEW (12 tests) +tests/test_narrative_example_intents.py NEW (30 tests) +docs/decisions/ADR-0066-turn-level-composition.md NEW (this file) +docs/decisions/README.md ADR-0066 index entry +``` + +--- + +## Verification + +``` +tests/test_thread_context.py 20 passed +tests/test_anaphora.py 12 passed +tests/test_narrative_example_intents.py 30 passed +Curated lanes (all green): + smoke 67 / cognition 121 / teaching 17 / packs 6 / runtime 19 / algebra 132 +Cognition eval byte-identical. +``` + +--- + +## Future ADRs unlocked + +- **Anaphora on the walk path.** Today thread anaphora fires only + when both turns are pack/teaching tier. Extending to vault-path + turns (the typical mid-session surface) needs a parallel hook + in the walk return path. Natural follow-up. +- **Multi-intent NARRATIVE composition.** Current NARRATIVE walks + one corpus dimension. Future work: extend composed-surface + (ADR-0062) to operate on the NARRATIVE clause set, producing + "narrative-of-narratives" surfaces. +- **EXAMPLE with hypothetical counterexamples.** Today EXAMPLE + surfaces only positive corpus chains. Future: when the corpus + contains contradicting/superseded chains, EXAMPLE can show + contrast. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 4ca5097c..c0d09f42 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -72,6 +72,7 @@ ADRs record significant architectural decisions: what was decided, why, what alt | [ADR-0060](ADR-0060-correction-acknowledgment-topic-lemma.md) | CORRECTION acknowledgement surface weaves the first pack-resident topical lemma from the utterance (left-to-right, excluding `correction` itself and `be`/`have` fillers) into a fixed template; backward-compatible with ADR-0053 (no-arg path byte-identical); closes `correction_truth_040` holdout miss; holdout `term_capture_rate` 75.0% → 79.2% | **Accepted** (2026-05-18) | | [ADR-0061](ADR-0061-procedure-intent-pack-grounded-surface.md) | PROCEDURE intent (`"How do I X?"`) routes to new `pack_grounded_procedure_surface`; selector picks **last** pack-resident lemma from verb-phrase subject (object > verb), falls back to verb when object is OOV, returns `None` (→ universal disclosure) for no-pack-lemma utterances; closes `procedure_define_010` (term `concept`) + `procedure_verify_034` (surface); holdout `surface_groundedness` 94.7% → 100.0%; `term_capture_rate` 79.2% → 83.3% | **Accepted** (2026-05-18) | | [ADR-0062](ADR-0062-composed-teaching-grounded-surface.md) | Composed teaching-grounded surface: when a chain `(A, intent_A, conn_A, B)` has a follow-up chain `(B, ?, conn_B, C)`, emit `"{A} {conn_A} {B}, which {conn_B} {C}"` instead of just `"{A} {conn_A} {B}"`; depth-1 (one hop) + cycle guard + pack-residency guard; degrades to single-chain byte-identically when no follow-up survives the guards; opt-in via `RuntimeConfig.composed_surface=False` default; cognition lane null-drop invariant (metrics byte-identical flag OFF/ON) CI-pinned | **Accepted** (2026-05-18) | +| [ADR-0066](ADR-0066-turn-level-composition.md) | Turn-level composition (Plan Phase 3): bounded session-thread context (P3.1) + opt-in deterministic anaphora prefix `(Recalling turn N: chain X.)` (P3.2, default off) + `IntentTag.NARRATIVE` multi-clause composer for "Tell me about X" walking every chain rooted on X across registered corpora (P3.3) + `IntentTag.EXAMPLE` reverse-chain composer for "Give me an example of X" surfacing chains where X is the object (P3.4); no prose generation, no new corpus mutation, all composers consult ADR-0064's cross-corpus aggregator; cognition lane byte-identical | **Accepted** (2026-05-18) | | [ADR-0065](ADR-0065-oov-gradient-and-relations-v2.md) | OOV gradient + relations v2 (Plan Phase 2): five-tier honesty gradient replaces the OOV cliff — pack / teaching / partial (one OOV + one known) / oov (learning invitation surface naming the unknown token + mounted-pack list) / universal disclosure; sink-emit OOVCandidates → `core teaching oov-gaps` aggregator → `core teaching oov-queue` auto-promotion mirrors P1.1+P1.2 architecture for vocab gaps; `en_core_relations_v2` adds 8 pronoun + role-filler lemmas (mother/father/son/daughter/brother/sister/grandparent/grandchild) with 7 reviewed v2-internal chains; no content synthesis, no domain inference, no auto-pack-mutation | **Accepted** (2026-05-18) | | [ADR-0064](ADR-0064-cross-pack-teaching-chains.md) | Cross-pack teaching chains: `chat/teaching_grounding.py` registers a tuple of `TeachingCorpusSpec(corpus_id, path, pack_id)`; each corpus is 1:1-bound to one lexicon pack (cross-domain triples deferred per teaching_order.md §5); new `_all_chains_index()` aggregates across registered corpora (first-match-wins); surface composers + discovery gate consult the aggregated view; `TeachingChain` gains `corpus_id` field; surface tag follows the resolving corpus id; replay-equivalence gate rewrites registry path during transient phase; `relations_chains_v1` seeded with 7 reviewed kinship chains; cognition lane byte-identical | **Accepted** (2026-05-18) | | [ADR-0063](ADR-0063-cross-pack-surface-resolver.md) | Cross-pack surface resolver: `chat/pack_resolver.py` introduces `resolve_lemma(lemma, pack_ids)` that maps a lemma to `(resolving_pack_id, semantic_domains)` across an ordered tuple of mounted lexicon packs (first-match-wins); pack-grounded DEFINITION / RECALL / COMPARISON / CORRECTION / PROCEDURE composers now consult the resolver instead of a hardcoded `en_core_cognition_v1`; surface trust-boundary tag follows the resolving pack id; `en_core_relations_v1` joins `RuntimeConfig.input_packs` defaults — kinship lemmas now ground on the live path without a separate composer module; cognition-lane surfaces remain byte-identical (cognition is resolved first) | **Accepted** (2026-05-18) | diff --git a/generate/intent.py b/generate/intent.py index 442c4d54..6c9f8f9b 100644 --- a/generate/intent.py +++ b/generate/intent.py @@ -23,6 +23,12 @@ class IntentTag(Enum): VERIFICATION = "verification" TRANSITIVE_QUERY = "transitive_query" FRAME_TRANSFER = "frame_transfer" + # P3.3 — "Tell me about X" / "Describe X" — multi-clause + # composer walks every chain rooted on X. + NARRATIVE = "narrative" + # P3.4 — "Give me an example of X" / "Show an instance of X" — + # reverse-chain composer surfaces chains where X is the object. + EXAMPLE = "example" UNKNOWN = "unknown" @@ -86,6 +92,14 @@ _RELATION_NORMALIZE: dict[str, str] = { } _RULES: tuple[tuple[re.Pattern[str], IntentTag], ...] = ( + # P3.3 — NARRATIVE patterns precede DEFINITION so "Tell me about X" + # does not accidentally classify as DEFINITION on the noun span. + (re.compile(r"^tell\s+me\s+about\s+", re.IGNORECASE), IntentTag.NARRATIVE), + (re.compile(r"^describe\s+", re.IGNORECASE), IntentTag.NARRATIVE), + (re.compile(r"^what\s+(?:can|do)\s+you\s+(?:say|know)\s+about\s+", re.IGNORECASE), IntentTag.NARRATIVE), + # P3.4 — EXAMPLE patterns precede DEFINITION for the same reason. + (re.compile(r"^(?:give|show)\s+(?:me\s+)?an?\s+(?:example|instance)\s+of\s+", re.IGNORECASE), IntentTag.EXAMPLE), + (re.compile(r"^example\s+of\s+", re.IGNORECASE), IntentTag.EXAMPLE), (re.compile(r"^what\s+(?:is|are)\s+", re.IGNORECASE), IntentTag.DEFINITION), (re.compile(r"^why\s+", re.IGNORECASE), IntentTag.CAUSE), (re.compile(r"^how\s+(?:do|can|should|would)\s+(?:I|we|you)\s+", re.IGNORECASE), IntentTag.PROCEDURE), diff --git a/tests/test_narrative_example_intents.py b/tests/test_narrative_example_intents.py new file mode 100644 index 00000000..241bd0ed --- /dev/null +++ b/tests/test_narrative_example_intents.py @@ -0,0 +1,241 @@ +"""Phase 3.3 + 3.4 — NARRATIVE and EXAMPLE intent + composer tests. + +The contracts pinned here: + + NARRATIVE + - "Tell me about X" / "Describe X" / "What can you say about X" + classify as NARRATIVE before falling through to DEFINITION. + - Composer walks every reviewed chain rooted on X across all + registered teaching corpora; emits up to max_clauses unique + (predicate, object) clauses; deterministic ordering. + - Falls through to OOV invitation when X is unknown. + + EXAMPLE + - "Give me an example of X" / "Show an instance of X" / + "Example of X" classify as EXAMPLE before DEFINITION. + - Composer surfaces chains where X is the OBJECT (reverse-chain + access pattern); dedupes by subject; deterministic ordering. + - Falls through to OOV invitation when X is unknown. + + Both + - Surface composes only pack atoms + verbatim chain content + + fixed template — no content synthesis. + - Tagged ``grounding_source="teaching"`` (same provenance as + teaching_grounded_surface — both consume the reviewed corpora). +""" + +from __future__ import annotations + +import pytest + +from chat.example_surface import example_grounded_surface +from chat.narrative_surface import narrative_grounded_surface +from chat.runtime import ChatRuntime +from generate.intent import IntentTag, classify_intent + + +# --------------------------------------------------------------------------- +# Intent classification +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("prompt", [ + "Tell me about light.", + "Tell me about parent", + "Describe truth", + "Describe photosynthesis.", + "What can you say about wisdom?", + "What do you know about memory?", +]) +def test_narrative_patterns_classify_narrative(prompt: str) -> None: + intent = classify_intent(prompt) + assert intent.tag is IntentTag.NARRATIVE + assert intent.subject + + +@pytest.mark.parametrize("prompt", [ + "Give me an example of truth.", + "Show me an instance of knowledge.", + "Show an example of parent.", + "Example of meaning", +]) +def test_example_patterns_classify_example(prompt: str) -> None: + intent = classify_intent(prompt) + assert intent.tag is IntentTag.EXAMPLE + assert intent.subject + + +def test_narrative_pattern_precedes_definition() -> None: + """``What can you say about X?`` could match the generic + ``what is/are X`` pattern — assert NARRATIVE wins on the more + specific pattern.""" + intent = classify_intent("What can you say about light?") + assert intent.tag is IntentTag.NARRATIVE + + +# --------------------------------------------------------------------------- +# NARRATIVE composer — pure function +# --------------------------------------------------------------------------- + + +def test_narrative_aggregates_multiple_chains() -> None: + """``truth`` appears as the subject of multiple cognition chains; + the narrative composer emits a clause for each.""" + surface = narrative_grounded_surface("truth") + assert surface is not None + assert "narrative-grounded (cognition_chains_v1)" in surface + assert "truth grounds knowledge" in surface + assert "truth requires evidence" in surface + + +def test_narrative_dedupes_by_predicate_object() -> None: + """When cause + verification carry the same (connective, object), + only one clause is emitted.""" + surface = narrative_grounded_surface("light") + assert surface is not None + # (light, cause, reveals, truth) + (light, verification, reveals, truth) + # → one clause "light reveals truth", not two. + assert surface.count("light reveals truth") == 1 + + +def test_narrative_handles_relations_pack_subject() -> None: + surface = narrative_grounded_surface("parent") + assert surface is not None + assert "narrative-grounded (relations_chains_v1)" in surface + assert "parent precedes child" in surface + + +def test_narrative_handles_relations_v2_subject() -> None: + surface = narrative_grounded_surface("mother") + assert surface is not None + assert "narrative-grounded (relations_chains_v2)" in surface + assert "mother precedes daughter" in surface + + +def test_narrative_unknown_lemma_returns_none() -> None: + assert narrative_grounded_surface("photosynthesis") is None + assert narrative_grounded_surface("xyzunknown") is None + + +def test_narrative_empty_input_returns_none() -> None: + assert narrative_grounded_surface("") is None + assert narrative_grounded_surface(" ") is None + + +def test_narrative_is_deterministic() -> None: + a = narrative_grounded_surface("truth") + b = narrative_grounded_surface("truth") + assert a == b + + +def test_narrative_max_clauses_caps_output() -> None: + """``max_clauses=1`` should emit just the lexicographically-first + clause for a multi-chain subject.""" + full = narrative_grounded_surface("truth", max_clauses=8) + capped = narrative_grounded_surface("truth", max_clauses=1) + assert full is not None + assert capped is not None + assert capped != full + assert len(capped) < len(full) + + +# --------------------------------------------------------------------------- +# EXAMPLE composer — pure function +# --------------------------------------------------------------------------- + + +def test_example_surfaces_reverse_chain() -> None: + """``truth`` appears as the object of ``light reveals truth`` — + the example composer surfaces the chain inverted (X = object).""" + surface = example_grounded_surface("truth") + assert surface is not None + assert "example-grounded (cognition_chains_v1)" in surface + assert "light reveals truth" in surface + + +def test_example_aggregates_multiple_subjects() -> None: + """``knowledge`` appears as the object of multiple chains; the + example composer dedupes by subject.""" + surface = example_grounded_surface("knowledge") + assert surface is not None + # truth/understanding/evidence all relate to knowledge as object. + assert "knowledge" in surface + # Each is listed once at most. + subjects = ["truth", "understanding", "evidence"] + found = [s for s in subjects if f"{s}" in surface] + assert len(found) >= 1 + + +def test_example_handles_relations_object() -> None: + """``parent`` appears as object of ``child follows parent`` + + ``family grounds parent`` — multiple examples.""" + surface = example_grounded_surface("parent") + assert surface is not None + assert "example-grounded (relations_chains_v1)" in surface + assert "parent" in surface + + +def test_example_unknown_object_returns_none() -> None: + assert example_grounded_surface("photosynthesis") is None + assert example_grounded_surface("xyzunknown") is None + + +def test_example_is_deterministic() -> None: + a = example_grounded_surface("truth") + b = example_grounded_surface("truth") + assert a == b + + +def test_example_max_examples_caps_output() -> None: + capped = example_grounded_surface("knowledge", max_examples=1) + full = example_grounded_surface("knowledge", max_examples=8) + assert capped is not None + assert full is not None + assert len(capped) <= len(full) + + +# --------------------------------------------------------------------------- +# Live runtime — NARRATIVE +# --------------------------------------------------------------------------- + + +def test_runtime_narrative_on_known_subject_routes_to_teaching() -> None: + rt = ChatRuntime() + resp = rt.chat("Tell me about truth.") + assert resp.grounding_source == "teaching" + assert "narrative-grounded" in resp.surface + assert "truth" in resp.surface + + +def test_runtime_narrative_on_oov_routes_to_oov_invitation() -> None: + rt = ChatRuntime() + resp = rt.chat("Describe photosynthesis.") + assert resp.grounding_source == "oov" + assert "photosynthesis" in resp.surface + assert "PackMutationProposal" in resp.surface + + +# --------------------------------------------------------------------------- +# Live runtime — EXAMPLE +# --------------------------------------------------------------------------- + + +def test_runtime_example_on_known_object_routes_to_teaching() -> None: + rt = ChatRuntime() + resp = rt.chat("Give me an example of truth.") + assert resp.grounding_source == "teaching" + assert "example-grounded" in resp.surface + assert "light reveals truth" in resp.surface + + +def test_runtime_example_on_oov_routes_to_oov_invitation() -> None: + rt = ChatRuntime() + resp = rt.chat("Example of photosynthesis") + assert resp.grounding_source == "oov" + + +def test_runtime_example_on_relations_object() -> None: + rt = ChatRuntime() + resp = rt.chat("Give me an example of parent.") + assert resp.grounding_source == "teaching" + assert "relations_chains_v1" in resp.surface