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:
parent
fe4cc2cd1f
commit
ce8226e9a2
7 changed files with 785 additions and 0 deletions
123
chat/example_surface.py
Normal file
123
chat/example_surface.py
Normal 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
158
chat/narrative_surface.py
Normal 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"]
|
||||
|
|
@ -62,6 +62,8 @@ _OOV_INTENT_TAGS: frozenset[IntentTag] = frozenset({
|
|||
IntentTag.COMPARISON,
|
||||
IntentTag.PROCEDURE,
|
||||
IntentTag.CORRECTION,
|
||||
IntentTag.NARRATIVE, # P3.3
|
||||
IntentTag.EXAMPLE, # P3.4
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
246
docs/decisions/ADR-0066-turn-level-composition.md
Normal file
246
docs/decisions/ADR-0066-turn-level-composition.md
Normal 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.
|
||||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
241
tests/test_narrative_example_intents.py
Normal file
241
tests/test_narrative_example_intents.py
Normal 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
|
||||
Loading…
Reference in a new issue