diff --git a/docs/curriculum/relations_pack_v1.md b/docs/curriculum/relations_pack_v1.md new file mode 100644 index 00000000..506b7c57 --- /dev/null +++ b/docs/curriculum/relations_pack_v1.md @@ -0,0 +1,181 @@ +# Pack Unit — `en_core_relations_v1` (Kinship Starter) + +**Date:** 2026-05-18 +**Author:** Shay +**Pack ID:** `en_core_relations_v1` +**Lemma count:** 8 (kinship-only; deliberately tight) +**Status:** Ratified (checksum verified). **Not yet mounted** on the default runtime. + +--- + +## Why kinship first + +Per [`teaching_order.md`](../teaching_order.md) §5 — "Pick *one* +commercial domain and run the full 1→4 progression *inside* that +domain before opening a second domain. Cross-domain triples come +last and only after both domains have ratified their own internal +DAG." + +Kinship is the classic starter domain for relation-curriculum work: + +- Tight DAG: ~10 primitives + transitive closures over them. +- Doctrinally well-behaved: "teach `parent_of` before `grandparent_of`, + then `ancestor_of`" maps directly to the formation pipeline's + prerequisite gates (`ratify.py` G3 — every relation's `head` and + `tail` must already be mastered). +- Orthogonal to the cognition pack: zero lemma overlap, zero + semantic-domain prefix overlap. Two pack DAGs ratify in isolation. + +--- + +## What's in v1 + +Eight kinship lemmas, each carrying ≥2 semantic_domains in a +deterministic taxonomy under `kinship.*`, `lineage.*`, +`biology.*`, `social.*`: + +| Lemma | Primary domain | Secondary domains | +|---|---|---| +| `parent` | `kinship.ascendant.direct` | `kinship.parent`, `biology.progenitor` | +| `child` | `kinship.descendant.direct` | `kinship.child`, `biology.offspring` | +| `sibling` | `kinship.lateral.direct` | `kinship.sibling`, `social.peer` | +| `family` | `kinship.unit` | `social.group.kin`, `kinship.group` | +| `ancestor` | `kinship.ascendant.transitive` | `lineage.upward`, `kinship.elder` | +| `descendant` | `kinship.descendant.transitive` | `lineage.downward`, `kinship.successor` | +| `spouse` | `kinship.partner` | `kinship.lateral.affinal`, `social.marriage` | +| `offspring` | `kinship.descendant.direct` | `biology.progeny`, `kinship.child` | + +**Note:** `person` is **not** in this pack. It lives in +`en_core_cognition_v1` (which carries it as a cognition primitive +covering "the experiencer of cognitive acts"). The orthogonality +test pins that boundary; if `person` ever drifts between the two +packs, that test fails as a deliberate signal that the domain +DAG boundary needs an ADR. + +--- + +## What's NOT in v1 (and why) + +### Pronouns + role-fillers + +`mother`, `father`, `son`, `daughter`, `brother`, `sister`, `aunt`, +`uncle`, `cousin`, `grandparent`, `grandchild`, `niece`, `nephew`. + +These are **specializations** of the v1 primitives — `mother` +is-a `parent` with a gender filler; `grandparent` is `parent of +parent` (a composed kinship relation). Following teaching-order +doctrine: teach the **atomic** primitives first; specializations +land in a v2 only after the v1 DAG has produced reviewed relation +chains over the primitives. + +### Quantifiers + ordinals + +`one`, `two`, `many`, `first`, `second`. Useful for kinship +statements ("a person has two parents") but a separate domain +(`en_core_quantification_v1`) with its own DAG. Cross-domain +triples (`one(parent)`, `two(parent)`) come after both domains +ratify internally. + +### Verbs of relation + +`begets`, `marries`, `descends-from`. These are predicates, not +nouns. The cognition pack carries general predicates already +(`reveals`, `grounds`, `requires`); kinship-specific predicates +will land alongside the first reviewed kinship chains (a +follow-up ADR — there is no `relations_chains_v1.jsonl` yet). + +--- + +## Engagement (opt-in only) + +The pack is **not** in the default `RuntimeConfig.input_packs`. +Mounting it changes the runtime's mounted manifold (89 → 93 +lemmas in the cognition+relations combination). Until cross-pack +teaching-grounded composition exists, mounting the relations +pack would expose lemmas to vault recall and intent classification +without a corresponding ratified surface composer for them. That +asymmetry would silently lower the deterministic-grounding +fraction of any kinship prompt. + +To opt in for development: + +```python +from core.config import RuntimeConfig +from chat.runtime import ChatRuntime + +cfg = RuntimeConfig(input_packs=RuntimeConfig().input_packs + ("en_core_relations_v1",)) +rt = ChatRuntime(config=cfg) +``` + +Programmatic mounting works today. CLI-level support +(`core chat --pack en_core_relations_v1`) is already in place via +the `--pack` flag (see `core chat --help`). + +--- + +## Verification + +``` +tests/test_en_core_relations_v1_pack.py 6 passed + - pack_loads_with_matching_checksum + - all_expected_lemmas_present + - each_lemma_carries_expected_primary_domain + - every_lemma_has_multiple_semantic_domains + - no_lemma_collision_with_cognition_pack + - pack_is_not_in_default_input_packs + +Lanes (regression): + core test --suite smoke 67 passed + core test --suite packs 6 passed + core test --suite algebra 132 passed +``` + +The non-negotiable field invariant +(`versor_condition(F) < 1e-6`) is unaffected: this is pure pack +data + a contract test. No runtime code path changed. + +--- + +## Path forward (future ADRs in priority order) + +1. **Cross-pack teaching-grounded composition.** + `chat/pack_grounding.py` and `chat/teaching_grounding.py` are + currently hardcoded to `en_core_cognition_v1`. A + `pack_resolver` abstraction that picks the right pack(s) for a + given intent/subject is the natural unlock — composes the + relations pack into the live surface path without a separate + composer module. + +2. **First kinship reviewed chains** — a + `relations_chains_v1.jsonl` or extension of the cognition + chains to a `domain` field. Triples like + `(parent, verification, is_a, ancestor)`, + `(child, verification, is_a, descendant)` form the first + ratified kinship DAG. + +3. **Pronoun + role-filler v2.** Once the v1 DAG produces + reviewed chains, add `mother`/`father`/`son`/`daughter` as + specializations. + +4. **Cross-domain triples.** After both relations v2 AND + cognition v1 are mature (and the + pack-resolver / cross-pack composer exist), open the cross- + domain frontier — e.g., `family causes belonging`, + `parent grounds identity`. + +--- + +## Cross-References + +- [`teaching_order.md`](../teaching_order.md) — the + prerequisite-topological doctrine that scoped this pack. +- [Curriculum: cognition saturation v2](cognition_saturation_v2.md) + — the sibling cognition-pack saturation that produced the 21 + chains the cognition lane composes over today. +- [ADR-0027](../decisions/ADR-0027-identity-packs.md) — pack + loading + ratification surface that this pack consumes + unchanged. +- [ADR-0062](../decisions/ADR-0062-composed-teaching-grounded-surface.md) + — the composed-surface ADR; the relations pack will become + the second domain that composer composes over, once cross-pack + composition lands. diff --git a/language_packs/data/en_core_relations_v1/lexicon.jsonl b/language_packs/data/en_core_relations_v1/lexicon.jsonl new file mode 100644 index 00000000..50b06c8b --- /dev/null +++ b/language_packs/data/en_core_relations_v1/lexicon.jsonl @@ -0,0 +1,8 @@ +{"entry_id":"en-core-rel-001","surface":"parent","lemma":"parent","language":"en","pos":"NOUN","semantic_domains":["kinship.ascendant.direct","kinship.parent","biology.progenitor"],"morphology_tags":["noun"],"provenance_ids":["seed:core_relations_v1"]} +{"entry_id":"en-core-rel-002","surface":"child","lemma":"child","language":"en","pos":"NOUN","semantic_domains":["kinship.descendant.direct","kinship.child","biology.offspring"],"morphology_tags":["noun"],"provenance_ids":["seed:core_relations_v1"]} +{"entry_id":"en-core-rel-003","surface":"sibling","lemma":"sibling","language":"en","pos":"NOUN","semantic_domains":["kinship.lateral.direct","kinship.sibling","social.peer"],"morphology_tags":["noun"],"provenance_ids":["seed:core_relations_v1"]} +{"entry_id":"en-core-rel-004","surface":"family","lemma":"family","language":"en","pos":"NOUN","semantic_domains":["kinship.unit","social.group.kin","kinship.group"],"morphology_tags":["noun"],"provenance_ids":["seed:core_relations_v1"]} +{"entry_id":"en-core-rel-005","surface":"ancestor","lemma":"ancestor","language":"en","pos":"NOUN","semantic_domains":["kinship.ascendant.transitive","lineage.upward","kinship.elder"],"morphology_tags":["noun"],"provenance_ids":["seed:core_relations_v1"]} +{"entry_id":"en-core-rel-006","surface":"descendant","lemma":"descendant","language":"en","pos":"NOUN","semantic_domains":["kinship.descendant.transitive","lineage.downward","kinship.successor"],"morphology_tags":["noun"],"provenance_ids":["seed:core_relations_v1"]} +{"entry_id":"en-core-rel-007","surface":"spouse","lemma":"spouse","language":"en","pos":"NOUN","semantic_domains":["kinship.partner","kinship.lateral.affinal","social.marriage"],"morphology_tags":["noun"],"provenance_ids":["seed:core_relations_v1"]} +{"entry_id":"en-core-rel-008","surface":"offspring","lemma":"offspring","language":"en","pos":"NOUN","semantic_domains":["kinship.descendant.direct","biology.progeny","kinship.child"],"morphology_tags":["noun"],"provenance_ids":["seed:core_relations_v1"]} diff --git a/language_packs/data/en_core_relations_v1/manifest.json b/language_packs/data/en_core_relations_v1/manifest.json new file mode 100644 index 00000000..c459796e --- /dev/null +++ b/language_packs/data/en_core_relations_v1/manifest.json @@ -0,0 +1,13 @@ +{ + "pack_id": "en_core_relations_v1", + "language": "en", + "role": "operational_base", + "script": "Latin", + "normalization_policy": "unitize_versor", + "source_manifest": "en_core_relations_v1.lexicon.jsonl", + "determinism_class": "D0", + "checksum": "1a013b464f6a0fad4ef96f9efaf1406c44729643e422c1f76a516099476e61d3", + "version": "1.0.0", + "gate_engaged": true, + "oov_policy": "tagged_fallback" +} diff --git a/tests/test_en_core_relations_v1_pack.py b/tests/test_en_core_relations_v1_pack.py new file mode 100644 index 00000000..922401fb --- /dev/null +++ b/tests/test_en_core_relations_v1_pack.py @@ -0,0 +1,140 @@ +"""Contract tests for the ``en_core_relations_v1`` kinship starter pack. + +Following the teaching-order doctrine +([`docs/teaching_order.md`](../docs/teaching_order.md) §5), +domain expansion starts with kinship — a tight, well-bounded +domain whose triples exercise every formation gate end-to-end. + +This pack is **not yet mounted** on the default runtime: it is +not in ``RuntimeConfig.input_packs`` defaults. Operator opt-in via +explicit ``RuntimeConfig(input_packs=(..., "en_core_relations_v1"))`` +is the engagement path until a follow-up ADR introduces cross-pack +composition for teaching-grounded surfaces. + +These tests pin: + + - The pack loads via ``load_pack("en_core_relations_v1")`` without + a checksum mismatch (manifest.checksum matches the bytes on + disk per CLAUDE.md's pack-discipline). + - The 9 kinship lemmas are all present. + - Each lemma carries the expected canonical semantic_domains + (deterministic taxonomy under ``kinship.*``, ``lineage.*``, + ``biology.*``, ``social.*``). + - No accidental cross-pack lemma collision with + ``en_core_cognition_v1`` (orthogonal domains; deliberate + separation per teaching_order.md). +""" + +from __future__ import annotations + +from language_packs.compiler import load_pack, load_pack_entries + + +PACK_ID = "en_core_relations_v1" + +EXPECTED_LEMMAS: tuple[str, ...] = ( + "parent", + "child", + "sibling", + "family", + "ancestor", + "descendant", + "spouse", + "offspring", +) + +# Note: `person` is intentionally NOT in this pack — it lives in +# `en_core_cognition_v1` and the orthogonality test below pins +# that boundary. + +EXPECTED_PRIMARY_DOMAINS: dict[str, str] = { + "parent": "kinship.ascendant.direct", + "child": "kinship.descendant.direct", + "sibling": "kinship.lateral.direct", + "family": "kinship.unit", + "ancestor": "kinship.ascendant.transitive", + "descendant": "kinship.descendant.transitive", + "spouse": "kinship.partner", + "offspring": "kinship.descendant.direct", +} + + +def test_pack_loads_with_matching_checksum() -> None: + """``load_pack`` raises ``ValueError`` on checksum mismatch. A + clean load implies manifest.checksum equals SHA-256 of the + bytes actually on disk — CLAUDE.md's pack-discipline.""" + manifest, manifold = load_pack(PACK_ID) + assert manifest.pack_id == PACK_ID + assert len(manifest.checksum) == 64 + assert all(c in "0123456789abcdef" for c in manifest.checksum) + assert len(manifold) == len(EXPECTED_LEMMAS) + + +def test_all_expected_lemmas_present() -> None: + _, manifold = load_pack(PACK_ID) + surfaces = {manifold.get_word_at(i) for i in range(len(manifold))} + assert surfaces == set(EXPECTED_LEMMAS) + + +def test_each_lemma_carries_expected_primary_domain() -> None: + """The first ``semantic_domains`` entry is the load-bearing + primary domain — drives mounted-pack resonance grouping.""" + entries = load_pack_entries(PACK_ID) + by_lemma = {e.lemma: e for e in entries} + for lemma, expected_primary in EXPECTED_PRIMARY_DOMAINS.items(): + assert lemma in by_lemma, f"missing lemma: {lemma}" + actual = by_lemma[lemma].semantic_domains + assert actual, f"{lemma!r} has empty semantic_domains" + assert actual[0] == expected_primary, ( + f"{lemma!r}: primary domain drifted from {expected_primary!r} " + f"to {actual[0]!r} — kinship taxonomy is load-bearing for the " + f"first relations curriculum unit; updating requires an ADR." + ) + + +def test_every_lemma_has_multiple_semantic_domains() -> None: + """Pack-grounded discipline: every lemma must surface a list of + domains so the surface composer can pick top-k. A 1-domain + entry would collapse the composer's information surface.""" + entries = load_pack_entries(PACK_ID) + for entry in entries: + assert len(entry.semantic_domains) >= 2, ( + f"{entry.lemma!r} has only {len(entry.semantic_domains)} " + f"semantic_domains; need ≥2 for surface composition headroom." + ) + + +def test_no_lemma_collision_with_cognition_pack() -> None: + """The relations pack and the cognition pack are deliberately + orthogonal (teaching_order.md §5 — domain DAGs ratify in + isolation before cross-domain triples). Any lemma in both + packs would create silent mounting ambiguity at the + ``_load_mounted_packs_cached`` layer.""" + _, relations_manifold = load_pack(PACK_ID) + _, cognition_manifold = load_pack("en_core_cognition_v1") + relations_surfaces = { + relations_manifold.get_word_at(i) + for i in range(len(relations_manifold)) + } + cognition_surfaces = { + cognition_manifold.get_word_at(i) + for i in range(len(cognition_manifold)) + } + overlap = relations_surfaces & cognition_surfaces + assert not overlap, ( + f"unexpected lemma collision between en_core_relations_v1 and " + f"en_core_cognition_v1: {overlap}. Either the relations pack " + f"absorbed a cognition lemma (reject) or the cognition pack " + f"grew a kinship lemma (reject) — domain DAGs must stay " + f"orthogonal per teaching_order.md §5." + ) + + +def test_pack_is_not_in_default_input_packs() -> None: + """Engagement is opt-in. Adding the pack to the default + ``input_packs`` is a follow-up ADR scope: cross-pack teaching- + grounded composition does not exist yet, and silently mounting + the pack would change the runtime's recall surface without a + corresponding ratification path.""" + from core.config import RuntimeConfig + assert PACK_ID not in RuntimeConfig().input_packs