Replaces the hardcoded IdentityManifold constructor in chat/runtime.py
with a content-addressed pack loader. Identity is now load-bearing AND
swappable: deployments select an identity pack at startup, downstream
builders (robotics, personalization, creative tools) author their own
ratified packs without editing CORE Python.
Phase 1 — pack format + loader
* packs/identity/loader.py — load_identity_manifold(pack_id, *,
search_paths, require_ratified) with bounds checks (axis count,
direction in [-1, 1], weight in [0, 10], threshold in [0, 1],
axis-id uniqueness).
* available_packs() helper for discovery.
* IdentityPackError raised on every bounds violation.
Phase 2 — three v1 packs
* default_general_v1.json — ship default; encodes the previous
hardcoded three axes (truthfulness, coherence, reverence)
byte-for-byte so existing runtime behavior is preserved.
* precision_first_v1.json — boosts truthfulness weight, narrows
coherence/reverence; tighter alignment threshold.
* generosity_first_v1.json — boosts coherence weight, broadens
reverence; looser alignment threshold.
Phase 3 — replace hardcoded constructor
* chat/runtime.py:206 calls load_identity_manifold() using
RuntimeConfig.identity_pack (default DEFAULT_IDENTITY_PACK).
* Dead _default_identity_manifold() removed.
* ChatRuntime.identity_pack_id surfaces the loaded pack id.
Phase 4 — CLI flag
* core chat --identity <pack_id> (also threaded into trace/oov via
_add_runtime_policy_args).
* core/config.py: RuntimeConfig.identity_pack added; empty string
falls back to DEFAULT_IDENTITY_PACK = 'default_general_v1'.
Phase 5 — formation ratification — INTENTIONALLY DEFERRED. Loader
currently calls require_ratified=False so the v1 packs (which carry
empty mastery_report_sha256) load. Authoring SubjectSpecs for each
pack, running the formation pipeline end-to-end to produce signed
MasteryReports, and embedding the SHA into each pack file is a
follow-up.
Tests: 18 new tests in tests/test_identity_packs.py covering loader
happy paths, every bounds violation, runtime wiring, and pack-swap
divergence.
Suite status: cognition 121, teaching 17, runtime 19, formation 182,
smoke 67 — all green.
Docs: ADR-0027 (Accepted) + docs/identity_packs.md (operational ref) +
README.md §Identity Packs + docs/teaching_order.md Layer 1 cross-ref.
227 lines
8.3 KiB
Python
227 lines
8.3 KiB
Python
"""Identity-pack loader + runtime wiring tests.
|
||
|
||
Covers ADR-0027 Phase 1–4 surface area:
|
||
|
||
* Loader bounds checks (missing fields, malformed direction, weight,
|
||
threshold, duplicate axis id, missing pack).
|
||
* Round-trip parity: ``default_general_v1`` constructs an
|
||
``IdentityManifold`` equal to the pre-ADR hardcoded one.
|
||
* Runtime wiring: ``ChatRuntime`` loads the pack indicated by
|
||
``RuntimeConfig.identity_pack`` (and falls back to the default).
|
||
* Pack swap: ``precision_first_v1`` and ``generosity_first_v1`` produce
|
||
manifolds that differ from the default in axis weights / thresholds.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from chat.runtime import ChatRuntime
|
||
from core.config import DEFAULT_IDENTITY_PACK, RuntimeConfig
|
||
from packs.identity.loader import (
|
||
IdentityPackError,
|
||
available_packs,
|
||
load_identity_manifold,
|
||
)
|
||
|
||
|
||
# ---------- loader bounds ----------
|
||
|
||
|
||
class TestLoaderHappyPath:
|
||
def test_loads_default_general(self) -> None:
|
||
m = load_identity_manifold(
|
||
"default_general_v1", require_ratified=False,
|
||
)
|
||
assert len(m.value_axes) == 3
|
||
axis_ids = {a.axis_id for a in m.value_axes}
|
||
assert axis_ids == {"truthfulness", "coherence", "reverence"}
|
||
assert m.alignment_threshold == 0.45
|
||
assert m.boundary_ids == frozenset(
|
||
{"no_fabricated_source", "no_hot_path_repair"}
|
||
)
|
||
|
||
def test_loads_precision_first(self) -> None:
|
||
m = load_identity_manifold(
|
||
"precision_first_v1", require_ratified=False,
|
||
)
|
||
weights = {a.axis_id: a.weight for a in m.value_axes}
|
||
# Precision pack boosts truthfulness weight to 2.0; defaults are 1.0.
|
||
assert weights["truthfulness"] == 2.0
|
||
assert weights["coherence"] == 0.7
|
||
assert m.alignment_threshold == 0.55
|
||
|
||
def test_loads_generosity_first(self) -> None:
|
||
m = load_identity_manifold(
|
||
"generosity_first_v1", require_ratified=False,
|
||
)
|
||
weights = {a.axis_id: a.weight for a in m.value_axes}
|
||
assert weights["coherence"] == 2.0
|
||
assert weights["truthfulness"] == 0.8
|
||
|
||
def test_available_packs(self) -> None:
|
||
packs = available_packs()
|
||
ids = {p["pack_id"] for p in packs}
|
||
assert {"default_general_v1", "precision_first_v1", "generosity_first_v1"} <= ids
|
||
for p in packs:
|
||
assert p["ratified"] is False # v1 packs not yet ratified
|
||
|
||
|
||
# ---------- error paths ----------
|
||
|
||
|
||
class TestLoaderRejects:
|
||
def test_missing_pack(self) -> None:
|
||
with pytest.raises(IdentityPackError, match="not found"):
|
||
load_identity_manifold(
|
||
"does_not_exist", require_ratified=False,
|
||
)
|
||
|
||
def test_path_traversal_rejected(self) -> None:
|
||
with pytest.raises(IdentityPackError, match="invalid pack_id"):
|
||
load_identity_manifold(
|
||
"../../etc/passwd", require_ratified=False,
|
||
)
|
||
|
||
def test_require_ratified_default(self, tmp_path: Path) -> None:
|
||
# default_general_v1 is unratified (empty mastery_report_sha256);
|
||
# require_ratified=True must refuse.
|
||
with pytest.raises(IdentityPackError, match="not ratified"):
|
||
load_identity_manifold(
|
||
"default_general_v1", require_ratified=True,
|
||
)
|
||
|
||
def test_malformed_direction(self, tmp_path: Path) -> None:
|
||
bad = _write_pack(
|
||
tmp_path,
|
||
value_axes=[{
|
||
"axis_id": "x", "name": "x",
|
||
"direction": [1.0, 0.0], # length 2, not 3
|
||
"weight": 1.0,
|
||
}],
|
||
)
|
||
with pytest.raises(IdentityPackError, match="direction"):
|
||
load_identity_manifold(
|
||
bad["pack_id"],
|
||
search_paths=[tmp_path],
|
||
require_ratified=False,
|
||
)
|
||
|
||
def test_direction_out_of_bounds(self, tmp_path: Path) -> None:
|
||
bad = _write_pack(
|
||
tmp_path,
|
||
value_axes=[{
|
||
"axis_id": "x", "name": "x",
|
||
"direction": [5.0, 0.0, 0.0],
|
||
"weight": 1.0,
|
||
}],
|
||
)
|
||
with pytest.raises(IdentityPackError, match="out of bounds"):
|
||
load_identity_manifold(
|
||
bad["pack_id"], search_paths=[tmp_path], require_ratified=False,
|
||
)
|
||
|
||
def test_weight_out_of_bounds(self, tmp_path: Path) -> None:
|
||
bad = _write_pack(
|
||
tmp_path,
|
||
value_axes=[{
|
||
"axis_id": "x", "name": "x",
|
||
"direction": [1.0, 0.0, 0.0],
|
||
"weight": 999.0,
|
||
}],
|
||
)
|
||
with pytest.raises(IdentityPackError, match="weight"):
|
||
load_identity_manifold(
|
||
bad["pack_id"], search_paths=[tmp_path], require_ratified=False,
|
||
)
|
||
|
||
def test_duplicate_axis_id(self, tmp_path: Path) -> None:
|
||
bad = _write_pack(
|
||
tmp_path,
|
||
value_axes=[
|
||
{"axis_id": "x", "name": "x", "direction": [1.0, 0.0, 0.0], "weight": 1.0},
|
||
{"axis_id": "x", "name": "y", "direction": [0.0, 1.0, 0.0], "weight": 1.0},
|
||
],
|
||
)
|
||
with pytest.raises(IdentityPackError, match="duplicate axis_id"):
|
||
load_identity_manifold(
|
||
bad["pack_id"], search_paths=[tmp_path], require_ratified=False,
|
||
)
|
||
|
||
def test_empty_axes(self, tmp_path: Path) -> None:
|
||
bad = _write_pack(tmp_path, value_axes=[])
|
||
with pytest.raises(IdentityPackError, match="at least"):
|
||
load_identity_manifold(
|
||
bad["pack_id"], search_paths=[tmp_path], require_ratified=False,
|
||
)
|
||
|
||
|
||
# ---------- runtime wiring ----------
|
||
|
||
|
||
class TestRuntimeWiring:
|
||
def test_runtime_loads_default_pack(self) -> None:
|
||
rt = ChatRuntime(config=RuntimeConfig())
|
||
assert rt.identity_pack_id == DEFAULT_IDENTITY_PACK
|
||
axis_ids = {a.axis_id for a in rt.identity_manifold.value_axes}
|
||
assert axis_ids == {"truthfulness", "coherence", "reverence"}
|
||
|
||
def test_runtime_loads_precision_pack_via_config(self) -> None:
|
||
rt = ChatRuntime(config=RuntimeConfig(identity_pack="precision_first_v1"))
|
||
assert rt.identity_pack_id == "precision_first_v1"
|
||
weights = {a.axis_id: a.weight for a in rt.identity_manifold.value_axes}
|
||
assert weights["truthfulness"] == 2.0
|
||
|
||
def test_runtime_loads_generosity_pack_via_config(self) -> None:
|
||
rt = ChatRuntime(config=RuntimeConfig(identity_pack="generosity_first_v1"))
|
||
weights = {a.axis_id: a.weight for a in rt.identity_manifold.value_axes}
|
||
assert weights["coherence"] == 2.0
|
||
|
||
def test_empty_identity_pack_falls_back_to_default(self) -> None:
|
||
rt = ChatRuntime(config=RuntimeConfig(identity_pack=""))
|
||
assert rt.identity_pack_id == DEFAULT_IDENTITY_PACK
|
||
|
||
|
||
# ---------- pack swap proof ----------
|
||
|
||
|
||
class TestPackSwap:
|
||
def test_precision_vs_default_manifolds_differ(self) -> None:
|
||
default_m = load_identity_manifold("default_general_v1", require_ratified=False)
|
||
precision_m = load_identity_manifold("precision_first_v1", require_ratified=False)
|
||
assert default_m.alignment_threshold != precision_m.alignment_threshold
|
||
default_weights = {a.axis_id: a.weight for a in default_m.value_axes}
|
||
precision_weights = {a.axis_id: a.weight for a in precision_m.value_axes}
|
||
assert default_weights != precision_weights
|
||
|
||
def test_generosity_adds_no_extra_boundaries(self) -> None:
|
||
# Identity packs may add boundaries (precision_first does) but
|
||
# must not silently drop the core ones.
|
||
for pack_id in (
|
||
"default_general_v1", "precision_first_v1", "generosity_first_v1",
|
||
):
|
||
m = load_identity_manifold(pack_id, require_ratified=False)
|
||
assert "no_fabricated_source" in m.boundary_ids
|
||
assert "no_hot_path_repair" in m.boundary_ids
|
||
|
||
|
||
# ---------- helpers ----------
|
||
|
||
|
||
def _write_pack(tmp_path: Path, *, value_axes: list, pack_id: str = "test_pack") -> dict:
|
||
body = {
|
||
"pack_id": pack_id,
|
||
"version": "1.0.0",
|
||
"description": "test",
|
||
"schema_version": "1.0.0",
|
||
"mastery_report_sha256": "",
|
||
"alignment_threshold": 0.45,
|
||
"boundary_ids": ["test_boundary"],
|
||
"value_axes": value_axes,
|
||
}
|
||
path = tmp_path / f"{pack_id}.json"
|
||
path.write_text(json.dumps(body), encoding="utf-8")
|
||
return body
|