ADR-0064 bound each teaching corpus 1:1 to a single ratified pack; chains whose subject + object resolved to different packs were dropped at load time. Phases 1–3 ratified the per-pack DAGs needed to lift that constraint safely. ADR-0067 introduces a deliberately narrow cross-pack chain shape. Each entry carries explicit subject_pack_id and object_pack_id fields, and the loader verifies per-chain residency. Same-pack entries are rejected as corpus-misfilings (anti-leakage). The cross-pack composer is the fall-through after the in-pack composer, so the cognition lane stays byte-identical. Files: - chat/cross_pack_grounding.py — CrossPackChain + loader + single-chain composer + multi-chain enumerators - teaching/cross_pack_chains/cross_pack_chains_v1.jsonl — 5 seed chains (family×identity, parent×understanding, family×memory, identity×family, understanding×parent) - chat/runtime.py — fall-through wiring in CAUSE/VERIFICATION - chat/narrative_surface.py, chat/example_surface.py — merge cross-pack chains, per-chain pack-residency helpers - tests/test_cross_pack_chains.py — 31 tests covering loader, surface, multi-chain access, runtime integration, in-pack precedence - tests/test_narrative_example_intents.py — corpus-tag assertions widened to allow cross-pack aggregation Verification: - 31 new tests pass - Curated lanes: smoke 67 / cognition 121 / teaching 17 / packs 6 / runtime 19 — all green - Cognition eval byte-identical (public 100/100/91.7/100, holdout 100/100/83.3/100) - Full lane: 2098 passed, 2 skipped, 0 failed in 2:30
182 lines
6.8 KiB
Python
182 lines
6.8 KiB
Python
"""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.cross_pack_grounding import cross_pack_chains_for_subject
|
|
from chat.pack_resolver import _pack_lexicon_for, resolve_lemma
|
|
from chat.teaching_grounding import (
|
|
_all_chains_index,
|
|
_pack_for_corpus,
|
|
)
|
|
from generate.semantic_templates import humanize_predicate
|
|
|
|
|
|
def _object_domains_for_chain(chain) -> tuple[str, ...]:
|
|
"""Return the object lemma's semantic domains for *chain*.
|
|
|
|
Handles both in-pack ``TeachingChain`` (residency via its bound
|
|
corpus pack) and cross-pack ``CrossPackChain`` (residency in
|
|
its declared ``object_pack_id``).
|
|
"""
|
|
object_pack_id = getattr(chain, "object_pack_id", None)
|
|
if object_pack_id:
|
|
return _pack_lexicon_for(object_pack_id).get(chain.object, ())
|
|
return _pack_for_corpus(chain.corpus_id).get(chain.object, ())
|
|
|
|
|
|
def _subject_domains_for_chain(chain) -> tuple[str, ...]:
|
|
"""Same as :func:`_object_domains_for_chain` but for the subject."""
|
|
subject_pack_id = getattr(chain, "subject_pack_id", None)
|
|
if subject_pack_id:
|
|
return _pack_lexicon_for(subject_pack_id).get(chain.subject, ())
|
|
return _pack_for_corpus(chain.corpus_id).get(chain.subject, ())
|
|
|
|
|
|
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: list = [
|
|
chain for (s, _), chain in index.items() if s == key
|
|
]
|
|
# ADR-0067 — merge cross-pack chains rooted on the same subject.
|
|
# In-pack chains take precedence on (intent, connective, object)
|
|
# collision (first-occurrence-wins in dedup loop below).
|
|
matching.extend(cross_pack_chains_for_subject(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_domains = _subject_domains_for_chain(first)
|
|
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_domains = _object_domains_for_chain(chain)
|
|
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"]
|