feat(packs): en_core_relations_v1 — kinship starter pack (8 lemmas)

Per teaching_order.md §5 — pick one commercial domain and run the
full 1→4 progression inside it before opening a second.  Kinship is
the doctrinally classic starter: tight DAG, well-bounded primitives,
and orthogonal to the cognition pack.

Lemmas (8): parent, child, sibling, family, ancestor, descendant,
spouse, offspring.  Each carries ≥2 semantic_domains under a
deterministic taxonomy (kinship.*, lineage.*, biology.*, social.*).

Deliberate exclusions:
  - `person` — lives in en_core_cognition_v1; orthogonality test
    pins that boundary.
  - Specializations (mother/father/son/daughter/grandparent/...) —
    derived from v1 primitives; land in v2 after v1 produces
    reviewed chains.
  - Quantifiers (one/two/many) — separate domain
    (en_core_quantification_v1); cross-domain triples come last.
  - Verbs of relation (begets/marries/...) — separate composer
    work; no relations_chains_v1.jsonl yet.

Engagement is opt-in:
  - Pack is NOT in RuntimeConfig.input_packs defaults.
  - Programmatic mount via RuntimeConfig(input_packs=(..., "en_core_relations_v1")).
  - CLI: core chat --pack en_core_relations_v1 (existing surface).
  - Default-not-mounted preserves the cognition lane unchanged
    until cross-pack teaching-grounded composition exists.

- language_packs/data/en_core_relations_v1/lexicon.jsonl
  — 8 entries, JSONL format matching en_core_cognition_v1.
- language_packs/data/en_core_relations_v1/manifest.json
  — pack_id, language, role=operational_base, checksum
  (SHA-256 of lexicon bytes per CLAUDE.md pack-discipline),
  version 1.0.0, determinism_class D0, oov_policy tagged_fallback.
- tests/test_en_core_relations_v1_pack.py — 6 tests pin:
  checksum-match load, lemma roster, per-lemma primary domain,
  ≥2 domains/lemma (composer headroom), zero collision with
  cognition pack (kinship DAG stays orthogonal), pack-not-in-
  default-input-packs (opt-in engagement contract).
- docs/curriculum/relations_pack_v1.md — full pack log:
  rationale per included/excluded lemma, opt-in engagement path,
  4-step ADR roadmap (cross-pack composition → first kinship
  chains → pronoun v2 → cross-domain triples).

Mounted-manifold sanity check (en_core_cognition_v1 +
en_core_relations_v1): 93 lemmas combined, no collisions, both
packs' surfaces individually addressable.

Lanes (regression): smoke 67 / packs 6 / algebra 132 / relations-pack 6.
The non-negotiable field invariant (versor_condition < 1e-6) is
unaffected: this is pure pack data + a contract test.
This commit is contained in:
Shay 2026-05-18 14:40:54 -07:00
parent c492014815
commit f0c57eb32e
4 changed files with 342 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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