feat(packs): en_core_spatial_v1 — spatial vocabulary (24 lemmas)

Workstream 1 sixth pack.  Closes the spatial-vocabulary gap.  Prior
packs had zero coverage of here/there, location nouns, or spatial
prepositions.

Pack composition (24 entries — 7 ADV / 8 ADP / 9 NOUN):

  spatial.deictic.*          here there  (2 ADV)
  spatial.direction.*        forward backward left up down  (5 ADV)
  spatial.relation.*         near far above below inside outside
                             between beyond  (8 ADP)
  spatial.noun.*             place location area region space
                             end top bottom side  (9 NOUN)

``right`` was deliberately omitted — en_core_attitude_v1 already owns
it as evaluative.positive, and first-match-wins resolution preserves
that claim.  A regression test pins this invariant explicitly.

Files: lexicon.jsonl / manifest.json + 12 contract tests.

Verification: full lane 2204 passed / 2 skipped / 0 failed.
Cognition eval byte-identical both splits.
This commit is contained in:
Shay 2026-05-18 21:38:20 -07:00
parent 891ffa8969
commit 390c2834f8
5 changed files with 182 additions and 0 deletions

View file

@ -47,6 +47,7 @@ DEFAULT_RESOLVABLE_PACK_IDS: tuple[str, ...] = (
"en_core_temporal_v1",
"en_core_action_v1",
"en_core_quantitative_v1",
"en_core_spatial_v1",
"en_core_relations_v1",
"en_core_relations_v2",
)

View file

@ -18,6 +18,7 @@ class RuntimeConfig:
"en_core_temporal_v1",
"en_core_action_v1",
"en_core_quantitative_v1",
"en_core_spatial_v1",
"en_core_relations_v1",
"en_core_relations_v2",
"he_logos_micro_v1",

View file

@ -0,0 +1,24 @@
{"entry_id":"en-core-spatial-001","surface":"here","lemma":"here","language":"en","pos":"ADV","semantic_domains":["spatial.deictic.proximal","spatial.deictic","cognition.location"],"morphology_tags":["adverb"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-002","surface":"there","lemma":"there","language":"en","pos":"ADV","semantic_domains":["spatial.deictic.distal","spatial.deictic","cognition.location"],"morphology_tags":["adverb"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-003","surface":"forward","lemma":"forward","language":"en","pos":"ADV","semantic_domains":["spatial.direction.forward","spatial.direction","cognition.direction"],"morphology_tags":["adverb"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-004","surface":"backward","lemma":"backward","language":"en","pos":"ADV","semantic_domains":["spatial.direction.backward","spatial.direction","cognition.direction"],"morphology_tags":["adverb"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-005","surface":"left","lemma":"left","language":"en","pos":"ADV","semantic_domains":["spatial.direction.lateral","spatial.direction","cognition.direction"],"morphology_tags":["adverb"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-006","surface":"up","lemma":"up","language":"en","pos":"ADV","semantic_domains":["spatial.direction.vertical_up","spatial.direction","cognition.direction"],"morphology_tags":["adverb"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-007","surface":"down","lemma":"down","language":"en","pos":"ADV","semantic_domains":["spatial.direction.vertical_down","spatial.direction","cognition.direction"],"morphology_tags":["adverb"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-008","surface":"near","lemma":"near","language":"en","pos":"ADP","semantic_domains":["spatial.relation.proximity","spatial.relation","cognition.location"],"morphology_tags":["adposition"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-009","surface":"far","lemma":"far","language":"en","pos":"ADP","semantic_domains":["spatial.relation.distance","spatial.relation","cognition.location"],"morphology_tags":["adposition"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-010","surface":"above","lemma":"above","language":"en","pos":"ADP","semantic_domains":["spatial.relation.vertical_above","spatial.relation","cognition.location"],"morphology_tags":["adposition"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-011","surface":"below","lemma":"below","language":"en","pos":"ADP","semantic_domains":["spatial.relation.vertical_below","spatial.relation","cognition.location"],"morphology_tags":["adposition"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-012","surface":"inside","lemma":"inside","language":"en","pos":"ADP","semantic_domains":["spatial.relation.containment_inside","spatial.relation","cognition.location"],"morphology_tags":["adposition"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-013","surface":"outside","lemma":"outside","language":"en","pos":"ADP","semantic_domains":["spatial.relation.containment_outside","spatial.relation","cognition.location"],"morphology_tags":["adposition"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-014","surface":"between","lemma":"between","language":"en","pos":"ADP","semantic_domains":["spatial.relation.between","spatial.relation","cognition.location"],"morphology_tags":["adposition"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-015","surface":"beyond","lemma":"beyond","language":"en","pos":"ADP","semantic_domains":["spatial.relation.distance","spatial.relation","cognition.location"],"morphology_tags":["adposition"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-016","surface":"place","lemma":"place","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.location","spatial.noun","cognition.location"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-017","surface":"location","lemma":"location","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.location","spatial.noun","cognition.location"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-018","surface":"area","lemma":"area","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.region","spatial.noun","cognition.location"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-019","surface":"region","lemma":"region","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.region","spatial.noun","cognition.location"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-020","surface":"space","lemma":"space","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.location","spatial.noun","cognition.location"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-021","surface":"end","lemma":"end","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.boundary_end","spatial.noun","cognition.boundary"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-022","surface":"top","lemma":"top","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.boundary_top","spatial.noun","cognition.boundary"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-023","surface":"bottom","lemma":"bottom","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.boundary_bottom","spatial.noun","cognition.boundary"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}
{"entry_id":"en-core-spatial-024","surface":"side","lemma":"side","language":"en","pos":"NOUN","semantic_domains":["spatial.noun.boundary_side","spatial.noun","cognition.boundary"],"morphology_tags":["noun"],"provenance_ids":["seed:core_spatial_v1"]}

View file

@ -0,0 +1,13 @@
{
"pack_id": "en_core_spatial_v1",
"language": "en",
"role": "operational_base",
"script": "Latin",
"normalization_policy": "unitize_versor",
"source_manifest": "en_core_spatial_v1.lexicon.jsonl",
"determinism_class": "D0",
"checksum": "f3ab77403f4f764f3828d0af6b7c6bcf54cef4137f73c577206cb522706ac14c",
"version": "1.0.0",
"gate_engaged": true,
"oov_policy": "tagged_fallback"
}

View file

@ -0,0 +1,143 @@
"""``en_core_spatial_v1`` — spatial vocabulary pack tests.
Spatial vocabulary had zero coverage in any prior pack *here*,
*there*, *near*, *above*, *place*, *location* all fell through to
OOV. 24 entries across:
- spatial.deictic.* here, there
- spatial.direction.* forward, backward, left, up, down (5 ADV)
- spatial.relation.* near, far, above, below, inside, outside,
between, beyond (8 ADP)
- spatial.noun.* place, location, area, region, space, end,
top, bottom, side (9 NOUN)
POS mix (7 ADV / 8 ADP / 9 NOUN). The composer is POS-agnostic.
Contracts pinned: checksum-verified load, primary-domain namespace
``spatial.*``, no collision with the 8 prior packs (notably ``right``
is in en_core_attitude_v1 as evaluative.positive, so spatial-direction
``right`` was deliberately omitted), mounted by default, registered
after en_core_quantitative_v1, prior-pack resolution unchanged.
"""
from __future__ import annotations
import json
from collections import Counter
from pathlib import Path
from chat.pack_resolver import DEFAULT_RESOLVABLE_PACK_IDS, resolve_lemma
from core.config import RuntimeConfig
from language_packs.compiler import load_pack
PACK_ID = "en_core_spatial_v1"
_PACK_ROOT = Path(__file__).resolve().parent.parent / "language_packs" / "data" / PACK_ID
EXPECTED_TOTAL = 24
EXPECTED_POS_COUNTS = {"ADV": 7, "ADP": 8, "NOUN": 9}
EXPECTED_LEMMAS: tuple[str, ...] = (
"here", "there", "forward", "backward", "left", "up", "down",
"near", "far", "above", "below", "inside", "outside", "between", "beyond",
"place", "location", "area", "region", "space", "end", "top", "bottom", "side",
)
def _read_lexicon() -> list[dict]:
return [
json.loads(line)
for line in (_PACK_ROOT / "lexicon.jsonl").read_text("utf-8").splitlines()
if line.strip()
]
def test_pack_loads_with_matching_checksum() -> None:
manifest, manifold = load_pack(PACK_ID)
assert manifest.pack_id == PACK_ID
assert len(manifest.checksum) == 64
assert len(manifold) == EXPECTED_TOTAL
def test_pos_distribution_matches_design() -> None:
pos_counts = Counter(e["pos"] for e in _read_lexicon())
assert dict(pos_counts) == EXPECTED_POS_COUNTS
def test_all_expected_lemmas_present() -> None:
_, manifold = load_pack(PACK_ID)
assert {manifold.get_word_at(i) for i in range(len(manifold))} == set(EXPECTED_LEMMAS)
def test_every_entry_has_spatial_namespace_primary_domain() -> None:
for entry in _read_lexicon():
assert entry["semantic_domains"][0].startswith("spatial."), entry
def test_no_collision_with_prior_packs() -> None:
prior: set[str] = set()
for pack in (
"en_core_cognition_v1", "en_core_meta_v1", "en_core_attitude_v1",
"en_core_temporal_v1", "en_core_action_v1", "en_core_quantitative_v1",
"en_core_relations_v1", "en_core_relations_v2",
):
for line in (_PACK_ROOT.parent / pack / "lexicon.jsonl").read_text("utf-8").splitlines():
if line.strip():
prior.add(json.loads(line)["lemma"].lower())
for entry in _read_lexicon():
assert entry["lemma"].lower() not in prior, entry
def test_provenance_is_seed_core_spatial_v1() -> None:
for entry in _read_lexicon():
assert entry["provenance_ids"] == ["seed:core_spatial_v1"], entry
def test_entry_ids_contiguous_and_zero_padded() -> None:
entries = sorted(_read_lexicon(), key=lambda d: d["entry_id"])
for i, entry in enumerate(entries, start=1):
assert entry["entry_id"] == f"en-core-spatial-{i:03d}", entry["entry_id"]
def test_pack_mounted_in_default_runtime_config() -> None:
assert PACK_ID in RuntimeConfig().input_packs
def test_pack_registered_after_prior_content_packs() -> None:
assert PACK_ID in DEFAULT_RESOLVABLE_PACK_IDS
for earlier in (
"en_core_cognition_v1", "en_core_meta_v1", "en_core_attitude_v1",
"en_core_temporal_v1", "en_core_action_v1", "en_core_quantitative_v1",
):
assert DEFAULT_RESOLVABLE_PACK_IDS.index(earlier) < DEFAULT_RESOLVABLE_PACK_IDS.index(PACK_ID)
def test_resolver_routes_spatial_lemmas_to_this_pack() -> None:
for lemma in EXPECTED_LEMMAS:
resolved = resolve_lemma(lemma)
assert resolved is not None and resolved[0] == PACK_ID, (
f"{lemma!r} resolved to {resolved}"
)
assert resolved[1][0].startswith("spatial.")
def test_right_remains_in_attitude_pack_not_spatial() -> None:
"""``right`` was deliberately excluded from spatial-direction
because en_core_attitude_v1 already owns it as evaluative.positive.
Verify the first-match-wins resolver preserves attitude's claim."""
resolved = resolve_lemma("right")
assert resolved is not None and resolved[0] == "en_core_attitude_v1"
assert resolved[1][0].startswith("attitude.evaluative.")
def test_prior_pack_lemma_resolution_unchanged() -> None:
for lemma, expected in (
("truth", "en_core_cognition_v1"),
("doubt", "en_core_meta_v1"),
("true", "en_core_attitude_v1"),
("now", "en_core_temporal_v1"),
("do", "en_core_action_v1"),
("all", "en_core_quantitative_v1"),
):
resolved = resolve_lemma(lemma)
assert resolved is not None and resolved[0] == expected