feat(adr-0246): §3.4/§3.5 lawful-only identity-path ledger (pure, off-serving)

Second Ring-1 unit per the brief. Stacked on feat/adr-0246-induced-action-primitives.

core/physics/identity_action.py:
  + PathBudget            two-level budget (epsilon_turn, epsilon_session)  (§3.4)
  + IdentityChainScope    pack/geometry/policy/session/biography scope key   (§3.5)
  + IdentityPathLedger    immutable path snapshot; full-SHA-256 chain_id +
                          ledger_digest (LE f64 bytes, no default=str)       (§4.2/§4.3)
  + advance_identity_path fold one turn: lawful iff d_stab<=epsilon_turn;
                          only lawful turns compose; refused turns are break
                          markers (path_break), never soft-projected I;
                          scope change = hard break onto a fresh chain
  + raw_path_product      forensic-only (proves lawful product != raw product)

Enforces the §3.4 doctrine: lawful-only composition; no soft-projection of an
unlawful action onto I (non-goal #11); raw and lawful kept separate. Hard breaks
on every scope dimension (§3.5). Pure numpy+identity_manifold; off-serving; no
gate/threshold/axis/H_id/flag change (identity_wave_gate stays default-off).
Gate admit-surface wiring + eval matrix are subsequent units (brief §0a).

[Verification]: uv run core test --suite smoke -q => 176 passed;
tests/test_adr_0246_path_ledger.py 16 passed (RED-first, incl. small-rotation
session-accumulation, hard-break on each scope dim, raw!=lawful forensic pin,
immutability); identity surfaces (induced_action, mismatch-diagnostic,
identity_manifold, identity_gate_wave) 85 passed together.
This commit is contained in:
Shay 2026-07-17 21:55:24 -07:00
parent 4941cf188e
commit 6efe4ad80c
3 changed files with 435 additions and 5 deletions

View file

@ -25,14 +25,19 @@ lawful action: ``d_stab`` is a pass/fail distance, and callers must NOT soft-pro
Enlarging ``H_id`` is a future, explicit, reviewed pack/policy change never an
implicit convenience here.
Pure (numpy + identity_manifold only), deterministic, float64, off-serving. The
lawful-only path composition and hard-break ledger (§3.4/§3.5) are a separate,
later unit; this module provides only the per-turn stabilizer defect.
Pure (numpy + identity_manifold only), deterministic, float64, off-serving. This
module owns both the per-turn stabilizer defect (``d_stab``) and the lawful-only
path ledger (§3.4/§3.5): the identity path composes ONLY the induced actions of
turns certified lawful, refused turns insert break markers (never a soft-projected
``I``), and a scope change forces a hard break onto a new chain.
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from typing import Any, Sequence
import numpy as np
@ -129,3 +134,199 @@ def stabilizer_defect_for_versor(
if stabilizer is None:
stabilizer = IdentityStabilizer.singleton(action.shape[0])
return stabilizer_defect(action, geometry.gram, stabilizer)
# -- ADR-0246 §3.4/§3.5 lawful-only identity-path ledger -----------------------
@dataclass(frozen=True)
class PathBudget:
"""Two-level lawfulness budget (ADR-0246 §3.4).
``epsilon_turn`` bounds a single turn's ``d_stab`` (a large one-turn departure
refuses immediately); ``epsilon_session`` bounds the composed lawful path's
``d_stab`` (slow accumulation of individually-lawful turns eventually refuses).
"""
epsilon_turn: float
epsilon_session: float
@dataclass(frozen=True)
class IdentityChainScope:
"""The scope that keys an identity-action chain (ADR-0246 §3.5).
Any change to these forces a **hard break** a new chain that does NOT
continue the previous composed path. The frame, its geometry, the lawfulness
policy, the session, and (when explicit) the biography epoch each redefine
what "the path" means, so a path may only compose within a single scope.
"""
pack_content_digest: str
geometry_version: str
policy_version: str
session_id: str
biography_epoch: str | None = None
def as_dict(self) -> dict[str, Any]:
return {
"pack_content_digest": self.pack_content_digest,
"geometry_version": self.geometry_version,
"policy_version": self.policy_version,
"session_id": self.session_id,
"biography_epoch": self.biography_epoch,
}
def _chain_id(scope: IdentityChainScope, chain_index: int) -> str:
"""Deterministic full-SHA-256 chain id (ADR-0245 §2.3 — no truncation).
Includes ``chain_index`` so a scope that recurs later in a session (e.g. a
pack A B A cycle) still yields a distinct chain id.
"""
payload = json.dumps(
{**scope.as_dict(), "chain_index": int(chain_index)},
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
@dataclass(frozen=True)
class IdentityPathLedger:
"""Immutable snapshot of the lawful-only identity path (ADR-0246 §4.2).
``a_path_lawful`` is the time-forward product of the induced actions of the
turns certified lawful within this chain (``I`` for an empty chain). Refused
turns are counted in ``break_count`` and excluded from the product never
composed as ``I`` (that would be the soft-projection §3.4 forbids).
"""
chain_id: str
scope: IdentityChainScope
chain_index: int
dimension: int
a_path_lawful: np.ndarray
d_stab_path: float
composed_turn_count: int
break_count: int
session_admit: bool
def ledger_digest(self) -> str:
"""Full-SHA-256 content id over the path state (LE f64 byte-order)."""
digest = hashlib.sha256()
digest.update(self.chain_id.encode("utf-8"))
digest.update(
np.ascontiguousarray(self.a_path_lawful, dtype=np.dtype("<f8")).tobytes()
)
digest.update(
json.dumps(
[self.chain_index, self.composed_turn_count, self.break_count],
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
)
return digest.hexdigest()
def as_dict(self) -> dict[str, Any]:
return {
"schema_version": "identity_path_v1",
"chain_id": self.chain_id,
"scope": self.scope.as_dict(),
"chain_index": self.chain_index,
"dimension": self.dimension,
"a_path_lawful": [
[float(x) for x in row] for row in self.a_path_lawful
],
"d_stab_path": float(self.d_stab_path),
"composed_turn_count": self.composed_turn_count,
"break_count": self.break_count,
"session_admit": self.session_admit,
"ledger_digest": self.ledger_digest(),
}
def advance_identity_path(
ledger: IdentityPathLedger | None,
scope: IdentityChainScope,
action: np.ndarray,
gram: np.ndarray,
budget: PathBudget,
) -> tuple[IdentityPathLedger, dict[str, Any]]:
"""Fold one turn's raw induced action into the lawful-only path (§3.4/§3.5).
Returns ``(new_ledger, turn_record)``. ``turn_record`` reports this turn's
``lawful`` / ``d_stab_turn`` / ``path_break`` / ``hard_break``. A turn is
lawful iff ``d_stab(action) epsilon_turn`` under the locked singleton
``H_id={I}``; only lawful turns compose. A scope change (or an absent prior
ledger) is a hard break that starts a fresh chain the previous path is NOT
continued. Immutable: ``ledger`` is never mutated.
"""
action = np.asarray(action, dtype=np.float64)
if action.ndim != 2 or action.shape[0] != action.shape[1]:
raise ValueError(
f"induced action must be a square matrix, got shape {action.shape}"
)
dimension = action.shape[0]
stabilizer = IdentityStabilizer.singleton(dimension)
d_stab_turn = stabilizer_defect(action, gram, stabilizer)
lawful = d_stab_turn <= budget.epsilon_turn
hard_break = ledger is None or ledger.scope != scope
if hard_break:
chain_index = 0 if ledger is None else ledger.chain_index + 1
a_path = np.eye(dimension, dtype=np.float64)
composed = 0
breaks = 0
else:
assert ledger is not None # not a hard break ⇒ prior ledger exists
chain_index = ledger.chain_index
a_path = ledger.a_path_lawful
composed = ledger.composed_turn_count
breaks = ledger.break_count
if lawful:
# time-forward: later turns act on the left of the accumulated frame action
a_path = action @ a_path
composed += 1
path_break = False
else:
# break marker — excluded from the product; NOT composed as identity
breaks += 1
path_break = True
d_stab_path = stabilizer_defect(a_path, gram, stabilizer)
new_ledger = IdentityPathLedger(
chain_id=_chain_id(scope, chain_index),
scope=scope,
chain_index=chain_index,
dimension=dimension,
a_path_lawful=a_path,
d_stab_path=d_stab_path,
composed_turn_count=composed,
break_count=breaks,
session_admit=(d_stab_path <= budget.epsilon_session),
)
turn_record = {
"lawful": lawful,
"d_stab_turn": d_stab_turn,
"path_break": path_break,
"hard_break": hard_break,
}
return new_ledger, turn_record
def raw_path_product(actions: Sequence[np.ndarray]) -> np.ndarray:
"""Time-forward product of ALL actions, lawful or not — **forensic only**.
This is the category error §3.4 forbids for the live path (it mixes refused,
ill-conditioned, and leaked actions into a fake "holonomy"). It exists solely
so tests and forensics can demonstrate that the lawful-only product differs
from the naive raw product. Never use it to admit a turn.
"""
if not actions:
raise ValueError("raw_path_product requires at least one action")
result = np.eye(np.asarray(actions[0]).shape[0], dtype=np.float64)
for action in actions:
result = np.asarray(action, dtype=np.float64) @ result
return result

View file

@ -53,8 +53,8 @@ structure is stabilized would instrument lawfulness on a frame the dynamics igno
| Unit | Scope | Status |
|------|-------|--------|
| **Slice 0** — mismatch diagnostic | evidence-only classification of the benign mismatch (foreign leakage vs in-span-unlawful vs numerical vs path vs semantic-coupling-absent) | **merged** `main` (quarantined diagnostic artifact); found: structural e4/e5 foreign leakage, declared frame dynamically unspecial |
| **§3 primitives** (this unit) | pure `A(F)`, `d_orth`, `d_stab` vs locked `H_id={I}`, typed residual channels in `core/physics/identity_manifold.py` + `identity_action.py`; slice-0 eval rewired to consume them (single source of truth) | in progress — RED→GREEN, off-serving, flag untouched |
| §3.4/§3.5 path ledger | lawful-only composition + hard breaks | next unit |
| **§3 primitives** | pure `A(F)`, `d_orth`, `d_stab` vs locked `H_id={I}`, typed residual channels in `core/physics/identity_manifold.py` + `identity_action.py`; slice-0 eval rewired to consume them (single source of truth) | **committed** `feat/adr-0246-induced-action-primitives` (RED→GREEN, off-serving, flag untouched) — awaiting review |
| **§3.4/§3.5 path ledger** (this unit) | lawful-only composition + hard breaks: `PathBudget`, `IdentityChainScope`, `IdentityPathLedger`, `advance_identity_path` in `identity_action.py`; refused turns = break markers (never soft-projected `I`); scope change = hard break onto new chain | in progress — RED→GREEN, off-serving; stacked on the primitives branch |
| §3.7 gate admit surface | wire d_orth/d_stab/typed channels into `IdentityScore` (flag default-off) | after ledger |
| §6 eval matrix + §11 feasibility | synthetic + path suites + discrimination report; the invariant/semantic-grounding feasibility study runs here as the **first consumer** of the §3 primitives (fixed cohort splits, synthetic recovery controls, typed e4/e5 generator analysis, precision pairs, adversarial discrimination) | after gate surface |
| ADR-0246 body + acceptance packet | §10 criteria; no self-Accept | last |

View file

@ -0,0 +1,229 @@
"""ADR-0246 §3.4/§3.5 — lawful-only identity-path ledger pins (§6.2 path suite).
The path ledger composes ONLY the induced actions of turns that were individually
certified lawful (``d_stab ε_turn`` under the locked singleton ``H_id={I}``).
Refused turns insert a break marker and are excluded from the product never a
soft-projected identity matrix masquerading as a pass (brief §3.4 / non-goal #11).
A scope change (pack digest / geometry / policy / session / biography epoch) forces
a hard break: a new ``chain_id`` and a fresh path (§3.5). Pure, off-serving.
"""
from __future__ import annotations
import numpy as np
import pytest
from algebra.cl41 import N_COMPONENTS
from core.physics.identity_manifold import IdentityManifoldGeometry
from core.physics.identity_action import (
IdentityChainScope,
PathBudget,
advance_identity_path,
raw_path_product,
)
_E12, _E13 = 6, 7
def _rotor(biv: int, theta: float) -> np.ndarray:
r = np.zeros(N_COMPONENTS, dtype=np.float64)
r[0] = np.cos(theta / 2.0)
r[biv] = np.sin(theta / 2.0)
return r
def _identity_versor() -> np.ndarray:
v = np.zeros(N_COMPONENTS, dtype=np.float64)
v[0] = 1.0
return v
@pytest.fixture(scope="module")
def geometry() -> IdentityManifoldGeometry:
return IdentityManifoldGeometry.from_directions(
((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
)
def _scope(pack="packA", geom="geomV1", policy="polV1", session="sess1", bio=None):
return IdentityChainScope(
pack_content_digest=pack,
geometry_version=geom,
policy_version=policy,
session_id=session,
biography_epoch=bio,
)
_BUDGET = PathBudget(epsilon_turn=0.1, epsilon_session=0.3)
def _action(geometry, versor):
return geometry.induced_action(versor)
def test_first_lawful_turn_starts_chain_near_identity(geometry):
ledger, rec = advance_identity_path(
None, _scope(), _action(geometry, _identity_versor()), geometry.gram, _BUDGET
)
assert rec["hard_break"] is True
assert rec["lawful"] is True and rec["path_break"] is False
assert ledger.composed_turn_count == 1 and ledger.break_count == 0
assert np.allclose(ledger.a_path_lawful, np.eye(3), atol=1e-12)
assert ledger.d_stab_path < 1e-12
assert ledger.session_admit is True
def test_lawful_identity_sequence_stays_admitted(geometry):
ledger = None
for _ in range(20):
ledger, _ = advance_identity_path(
ledger, _scope(), _action(geometry, _identity_versor()), geometry.gram, _BUDGET
)
assert ledger.composed_turn_count == 20 and ledger.break_count == 0
assert np.allclose(ledger.a_path_lawful, np.eye(3), atol=1e-12)
assert ledger.session_admit is True
def test_small_rotations_accumulate_to_session_refusal(geometry):
small = _action(geometry, _rotor(_E12, 0.05)) # each turn is lawful (small d_stab)
ledger = None
admitted_turns = 0
for _ in range(40):
ledger, rec = advance_identity_path(ledger, _scope(), small, geometry.gram, _BUDGET)
assert rec["lawful"] is True # each small step passes ε_turn
admitted_turns += 1
if not ledger.session_admit:
break
# per-turn always lawful, but the composed path eventually breaches ε_session
assert ledger.session_admit is False
assert ledger.d_stab_path > _BUDGET.epsilon_session
assert ledger.composed_turn_count == admitted_turns
def test_refused_turn_is_break_and_excluded(geometry):
ledger, _ = advance_identity_path(
None, _scope(), _action(geometry, _identity_versor()), geometry.gram, _BUDGET
)
before = ledger.a_path_lawful.copy()
big = _action(geometry, _rotor(_E12, np.pi / 2.0)) # 90° rotation: d_stab huge
ledger, rec = advance_identity_path(ledger, _scope(), big, geometry.gram, _BUDGET)
assert rec["lawful"] is False and rec["path_break"] is True
assert ledger.break_count == 1 and ledger.composed_turn_count == 1
# the refused action does NOT compose (no soft-projected I either): path unchanged
assert np.allclose(ledger.a_path_lawful, before, atol=1e-12)
def test_interleaved_refuse_admit_records_raw(geometry):
ident = _action(geometry, _identity_versor())
big = _action(geometry, _rotor(_E12, np.pi / 2.0))
seq = [ident, big, ident, big, ident]
ledger = None
records = []
for a in seq:
ledger, rec = advance_identity_path(ledger, _scope(), a, geometry.gram, _BUDGET)
records.append(rec)
assert ledger.composed_turn_count == 3 and ledger.break_count == 2
assert [r["path_break"] for r in records] == [False, True, False, True, False]
def test_hard_break_on_pack_digest_change(geometry):
ident = _action(geometry, _identity_versor())
ledger, _ = advance_identity_path(None, _scope(pack="packA"), ident, geometry.gram, _BUDGET)
id_a = ledger.chain_id
# drift the path a little so "not continued" is observable
small = _action(geometry, _rotor(_E12, 0.05))
ledger, _ = advance_identity_path(ledger, _scope(pack="packA"), small, geometry.gram, _BUDGET)
drifted = ledger.a_path_lawful.copy()
# pack change → hard break
ledger, rec = advance_identity_path(ledger, _scope(pack="packB"), ident, geometry.gram, _BUDGET)
assert rec["hard_break"] is True
assert ledger.chain_id != id_a
assert ledger.composed_turn_count == 1 and ledger.break_count == 0 # fresh chain
assert not np.allclose(ledger.a_path_lawful, drifted) # old path not continued
assert np.allclose(ledger.a_path_lawful, np.eye(3), atol=1e-12)
@pytest.mark.parametrize(
"changed",
[
{"geom": "geomV2"},
{"policy": "polV2"},
{"session": "sess2"},
{"bio": "epoch2"},
],
)
def test_hard_break_on_each_scope_dimension(geometry, changed):
ident = _action(geometry, _identity_versor())
ledger, _ = advance_identity_path(None, _scope(), ident, geometry.gram, _BUDGET)
base_id = ledger.chain_id
ledger, rec = advance_identity_path(ledger, _scope(**changed), ident, geometry.gram, _BUDGET)
assert rec["hard_break"] is True
assert ledger.chain_id != base_id
def test_raw_product_differs_from_lawful_product(geometry):
ident = _action(geometry, _identity_versor())
big = _action(geometry, _rotor(_E12, np.pi / 2.0))
seq = [ident, big, ident]
ledger = None
for a in seq:
ledger, _ = advance_identity_path(ledger, _scope(), a, geometry.gram, _BUDGET)
# forensic pin: composing ALL raw actions (incl. the refused 90°) gives a very
# different result than the lawful-only product — the category error §3.4 forbids.
raw = raw_path_product(seq)
assert not np.allclose(raw, ledger.a_path_lawful)
assert np.allclose(ledger.a_path_lawful, np.eye(3), atol=1e-12) # lawful excludes the big turn
def test_chain_id_is_deterministic_full_sha256(geometry):
ident = _action(geometry, _identity_versor())
l1, _ = advance_identity_path(None, _scope(), ident, geometry.gram, _BUDGET)
l2, _ = advance_identity_path(None, _scope(), ident, geometry.gram, _BUDGET)
assert l1.chain_id == l2.chain_id
assert len(l1.chain_id) == 64
int(l1.chain_id, 16) # valid hex
def test_ledger_digest_deterministic_and_path_sensitive(geometry):
ident = _action(geometry, _identity_versor())
small = _action(geometry, _rotor(_E13, 0.05))
l1, _ = advance_identity_path(None, _scope(), ident, geometry.gram, _BUDGET)
l1b, _ = advance_identity_path(None, _scope(), ident, geometry.gram, _BUDGET)
assert l1.ledger_digest() == l1b.ledger_digest()
assert len(l1.ledger_digest()) == 64
l2, _ = advance_identity_path(l1, _scope(), small, geometry.gram, _BUDGET)
assert l2.ledger_digest() != l1.ledger_digest()
def test_advance_is_immutable(geometry):
ident = _action(geometry, _identity_versor())
ledger, _ = advance_identity_path(None, _scope(), ident, geometry.gram, _BUDGET)
snapshot = ledger.a_path_lawful.copy()
count_before = ledger.composed_turn_count
_new, _ = advance_identity_path(
ledger, _scope(), _action(geometry, _rotor(_E12, 0.05)), geometry.gram, _BUDGET
)
assert ledger.composed_turn_count == count_before # original untouched
assert np.allclose(ledger.a_path_lawful, snapshot)
def test_as_dict_shape(geometry):
ledger, _ = advance_identity_path(
None, _scope(), _action(geometry, _identity_versor()), geometry.gram, _BUDGET
)
d = ledger.as_dict()
assert d["schema_version"] == "identity_path_v1"
assert d["chain_id"] == ledger.chain_id
assert d["composed_turn_count"] == 1
assert d["break_count"] == 0
assert d["session_admit"] is True
assert "a_path_lawful" in d and "d_stab_path" in d and "ledger_digest" in d
def test_module_is_pure_offserving():
import core.physics.identity_action as a
with open(a.__file__, encoding="utf-8") as fh:
src = fh.read()
assert "chat.runtime" not in src and "import chat" not in src