feat(adr-0066): NARRATIVE + EXAMPLE intents with multi-clause composers (Phase 3.3 + 3.4)

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.
This commit is contained in:
Shay 2026-05-18 17:01:55 -07:00
parent fe4cc2cd1f
commit ce8226e9a2
7 changed files with 785 additions and 0 deletions

123
chat/example_surface.py Normal file
View file

@ -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"]

158
chat/narrative_surface.py Normal file
View file

@ -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"]

View file

@ -62,6 +62,8 @@ _OOV_INTENT_TAGS: frozenset[IntentTag] = frozenset({
IntentTag.COMPARISON,
IntentTag.PROCEDURE,
IntentTag.CORRECTION,
IntentTag.NARRATIVE, # P3.3
IntentTag.EXAMPLE, # P3.4
})

View file

@ -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 <chain_id>.) # prior was teaching
(Recalling turn N: <subject> 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.

View file

@ -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) |

View file

@ -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),

View file

@ -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