From ed54dddacb8c1f282dedbb85364b6e999139632e Mon Sep 17 00:00:00 2001 From: Shay Date: Fri, 17 Jul 2026 22:09:32 -0700 Subject: [PATCH] =?UTF-8?q?draft(adr-0246):=20slice-1=20scaffold=20?= =?UTF-8?q?=E2=80=94=20=C2=A76.1/=C2=A76.2=20eval=20suite=20+=20malformed-?= =?UTF-8?q?F=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BOUNDED AUTONOMOUS SCAFFOLD DRAFT (Fable 5) — not a PR, not merged, no status flip, no main push. For Opus 4.8 + Shay audit before anything proceeds toward main. Stacked on the verified §3-primitives + §3.4/3.5-ledger stack (reuse, not re-derive; all descend from main @ 04d67ca5). Adds (directive steps 3-4): + evals/adr_0246_geometric_suite/ runnable §6.1 synthetic geometric suite (identity, pi-inversion, 90deg-permutation, mild drift, alien tilt e14, boost e15, near-singular Gram, malformed F) + §6.2 path/holonomy suite (lawful sequence, small-rotation session accumulation, interleaved refuse, pack-change hard break, raw!=lawful forensic). 14/14 cases pass. + tests/test_adr_0246_geometric_suite.py pins every case + explicit §6.1 pins (pi-inversion s=-1, 90deg s=0, near-singular Gram error, malformed-F error) + identity_manifold.py: MalformedVersorError + _validate_versor guard on induced_action / typed_residual_energy (§6.1 fail-closed on malformed F) + docs/handoff/adr-0246-slice1-scaffold-notes.md placeholder list, §5-§7 uncertainties, constraint-compliance record, explicit Opus/human TODOs + docs/audit/artifacts/adr-0246-slice1-scaffold-runlog.txt actual run output Constraints honored: H_id={I} only; no soft-projection of unlawful A; path composes lawful actions only (never raw product); no C_id corrector; chat/runtime, flags, D4 gate wiring untouched (A-04 quarantine pinned); no discrimination report, no ADR body, no claims language (deferred TODO: Opus/human); D4 plan not modified. epsilon_turn/epsilon_session are UNCERTIFIED PLACEHOLDERS, never baked into a module default (PathBudget is caller-supplied) — flagged in notes + run log. [Verification]: uv run core test --suite smoke -q => 176 passed; python -m evals.adr_0246_geometric_suite => 14/14 all_passed; ADR-0246 suites + adjacent D4 identity surfaces => 114 passed (see run log). --- core/physics/identity_manifold.py | 32 +- .../adr-0246-slice1-scaffold-runlog.txt | 38 +++ ...ity-action-and-path-integrity-preflight.md | 7 +- .../handoff/adr-0246-slice1-scaffold-notes.md | 132 ++++++++ evals/adr_0246_geometric_suite/__init__.py | 301 ++++++++++++++++++ evals/adr_0246_geometric_suite/__main__.py | 42 +++ tests/test_adr_0246_geometric_suite.py | 98 ++++++ 7 files changed, 645 insertions(+), 5 deletions(-) create mode 100644 docs/audit/artifacts/adr-0246-slice1-scaffold-runlog.txt create mode 100644 docs/handoff/adr-0246-slice1-scaffold-notes.md create mode 100644 evals/adr_0246_geometric_suite/__init__.py create mode 100644 evals/adr_0246_geometric_suite/__main__.py create mode 100644 tests/test_adr_0246_geometric_suite.py diff --git a/core/physics/identity_manifold.py b/core/physics/identity_manifold.py index bed9c133..2183f084 100644 --- a/core/physics/identity_manifold.py +++ b/core/physics/identity_manifold.py @@ -160,6 +160,28 @@ def euclidean_norm(s: np.ndarray) -> float: return float(np.linalg.norm(np.asarray(s, dtype=np.float64), ord=2)) +class MalformedVersorError(ValueError): + """Raised when a versor handed to an ADR-0246 primitive is not a well-formed + ``N_COMPONENTS``-vector of finite floats. + + Fail-closed at the primitive boundary (ADR-0246 §6.1 "Malformed F → typed + error"): a NaN/inf or wrong-shape field must raise, never propagate a silent + NaN into an induced action or a residual-channel split. + """ + + +def _validate_versor(versor: np.ndarray) -> np.ndarray: + """Coerce to a finite float64 ``(N_COMPONENTS,)`` array or fail closed.""" + array = np.asarray(versor, dtype=np.float64) + if array.shape != (N_COMPONENTS,): + raise MalformedVersorError( + f"versor must have shape ({N_COMPONENTS},), got {array.shape}" + ) + if not np.all(np.isfinite(array)): + raise MalformedVersorError("versor has non-finite (NaN/inf) components") + return array + + def orthogonality_defect_of_action(action: np.ndarray, gram: np.ndarray) -> float: """``‖AᵀGA − G‖_F`` for a precomputed induced action ``A`` (ADR-0246 §3.2). @@ -270,8 +292,11 @@ class IdentityManifoldGeometry: When ``F`` preserves the subspace isometrically, ``A`` is ``G``-orthogonal (``AᵀGA = G``). Built only from existing primitives (Gram, signed inner product, sandwich); no new algebra. + + Raises :class:`MalformedVersorError` on a non-finite or wrong-shape versor + (ADR-0246 §6.1 fail-closed on malformed F). """ - versor = np.asarray(versor, dtype=np.float64) + versor = _validate_versor(versor) n = len(self.axes_psi) overlaps = np.empty((n, n), dtype=np.float64) for j, axis_j in enumerate(self.axes_psi): @@ -310,8 +335,11 @@ class IdentityManifoldGeometry: Retains the positive-definite Euclidean coefficient norm (never the indefinite ``⟨·,·⟩₀``) so a boost/e5 component cannot silently vanish. + + Raises :class:`MalformedVersorError` on a non-finite or wrong-shape versor + (ADR-0246 §6.1 fail-closed on malformed F). """ - versor = np.asarray(versor, dtype=np.float64) + versor = _validate_versor(versor) e4_energy = e5_energy = spatial_foreign = unclassified = total = 0.0 for axis in self.axes_psi: rotated = sandwich(versor, axis) diff --git a/docs/audit/artifacts/adr-0246-slice1-scaffold-runlog.txt b/docs/audit/artifacts/adr-0246-slice1-scaffold-runlog.txt new file mode 100644 index 00000000..728d1738 --- /dev/null +++ b/docs/audit/artifacts/adr-0246-slice1-scaffold-runlog.txt @@ -0,0 +1,38 @@ +ADR-0246 slice-1 scaffold — local run log +branch: feat/adr-0246-slice1-scaffold +base: feat/adr-0246-path-ledger (contains §3 primitives + §3.4/3.5 ledger); all descend from main @ 04d67ca5 +commit (pre-scaffold-commit HEAD): 6efe4ad80cc9a4914fc97e0e51ec92c1dd43ec72 +python: Python 3.12.13 + +=== §6.1/§6.2 eval harness: python -m evals.adr_0246_geometric_suite === + +[geometric_suite] + PASS identity_versor + PASS inplane_pi_inversion_e12 + PASS inplane_90deg_permutation_e12 + PASS mild_inplane_drift_e12_0.02 + PASS alien_tilt_e14_1.5 + PASS boost_e15_1.0 + PASS near_singular_gram + PASS malformed_f_nan + PASS malformed_f_wrong_shape + +[path_suite] + PASS lawful_near_identity_sequence + PASS small_rotations_accumulate_to_session_refusal + PASS interleaved_refuse_admit + PASS hard_break_on_pack_change + PASS raw_product_differs_from_lawful + +14/14 cases passed; all_passed=True +placeholders (uncertified): {'epsilon_turn': 0.1, 'epsilon_session': 0.3, 'note': 'UNCERTIFIED — D4 Phase 3 certified only gamma_id; ε not calibrated'} + +=== pytest: ADR-0246 suites + adjacent identity surfaces === +........................................................................ [ 63%] +.......................................... [100%] +114 passed in 52.42s + +=== uv run core test --suite smoke -q === + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +176 passed, 1 warning in 130.75s (0:02:10) diff --git a/docs/briefs/ADR-0246-induced-identity-action-and-path-integrity-preflight.md b/docs/briefs/ADR-0246-induced-identity-action-and-path-integrity-preflight.md index 64be36a5..1238e16e 100644 --- a/docs/briefs/ADR-0246-induced-identity-action-and-path-integrity-preflight.md +++ b/docs/briefs/ADR-0246-induced-identity-action-and-path-integrity-preflight.md @@ -54,9 +54,10 @@ structure is stabilized would instrument lawfulness on a frame the dynamics igno |------|-------|--------| | **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** | 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 | +| **§3.4/§3.5 path ledger** | 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 | **committed** `feat/adr-0246-path-ledger` (RED→GREEN, off-serving), stacked on primitives — awaiting review | +| **§6.1/§6.2 eval matrix (scaffold)** (this unit) | runnable synthetic geometric + path/holonomy suites (`evals/adr_0246_geometric_suite/`), every §6.1/§6.2 case pinned; malformed-F fail-closed (`MalformedVersorError`) | **draft scaffold** `feat/adr-0246-slice1-scaffold` (14/14 eval cases + 114 identity-surface tests + smoke green) — awaiting Opus/Shay audit (`docs/handoff/adr-0246-slice1-scaffold-notes.md`) | +| §3.7 gate admit surface | wire d_orth/d_stab/typed channels into `IdentityScore` (flag default-off); calibrate ε_turn/ε_session | after audit | +| §6.3 + §11 feasibility | 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 | Nothing in the reordering relaxes a §7 non-goal: no `C_id` corrector, no `H_id` diff --git a/docs/handoff/adr-0246-slice1-scaffold-notes.md b/docs/handoff/adr-0246-slice1-scaffold-notes.md new file mode 100644 index 00000000..fa08c5e4 --- /dev/null +++ b/docs/handoff/adr-0246-slice1-scaffold-notes.md @@ -0,0 +1,132 @@ +# ADR-0246 Slice-1 Scaffold — Handoff Notes (Fable 5 bounded autonomous draft) + +**Status:** DRAFT SCAFFOLD ONLY — not a PR, not merged, no status flip, no `main` +push. For review by Opus 4.8 + Joshua Shay before anything proceeds toward `main`. +**Branch:** `feat/adr-0246-slice1-scaffold` (unmerged). +**D4 gate (directive stop-condition #3):** PASSED — D4 is closed. Both acceptance +packets are on `main` (`docs/audit/adr-024{4,5}-acceptance-packet-2026-07-17.md`); +ADR-0244 status line is `Accepted — ratified by Joshua Shay 2026-07-17`; `main @ +04d67ca5`. Proceeded. +**Stop condition reached:** #1 — all four build steps complete and the full +§6.1 + §6.2 matrix passes locally (smoke + new tests + eval harness), with results +written to `docs/audit/artifacts/adr-0246-slice1-scaffold-runlog.txt` (not asserted +from memory). No §3/§7 non-goal had to be violated; no calibration numbers were +invented into the modules (see Placeholders). + +--- + +## 1. What was built (directive steps 1–4) + +| Step | Deliverable | Where | +|------|-------------|-------| +| 1 | `induced_action(F)`, `d_orth`, typed residual energy (§3.1/3.2/3.6), pure f64 | `core/physics/identity_manifold.py` | +| 2 | `H_id={I}` policy (`IdentityStabilizer`), `d_stab`, lawful-only path composition + hard-break ledger (§3.3–§3.5) | `core/physics/identity_action.py` | +| 3 | Runnable §6.1 synthetic geometric suite + §6.2 path/holonomy suite | `evals/adr_0246_geometric_suite/` | +| 4 | Unit pins for every synthetic case (fail loudly on the exact expected) | `tests/test_adr_0246_{induced_action,path_ledger,geometric_suite}.py` | + +### Reuse note (transparent for audit) +Steps 1–2 are the **already-verified** work from earlier this session, reused +rather than re-derived (RED-first TDD, off-serving): +- `feat/adr-0246-induced-action-primitives` — §3 primitives (commit `4941cf18`) +- `feat/adr-0246-path-ledger` — §3.4/3.5 ledger (commit `6efe4ad8`, stacked) + +`feat/adr-0246-slice1-scaffold` is stacked on top of those, so its history carries +both commits (all descend from `main @ 04d67ca5`). The two earlier branches are +therefore **subsumed** by this scaffold and can be pruned if the reviewers prefer +the single-branch deliverable. This scaffold additionally adds: the malformed-F +guard (`MalformedVersorError`), the §6.1/§6.2 eval harness, the extra §6.1 pins, +the run log, and these notes. + +--- + +## 2. Hard-constraint compliance (directive "Hard constraints while building") + +- [x] `H_id = {I}` only — `IdentityStabilizer.singleton`; never enlarged; never + soft-projects an unlawful `A` onto `I` (refused turns are break markers). + Pinned: `test_refused_turn_is_break_and_excluded`. +- [x] Path composed from lawful/certified actions only — never raw `A_t`. + `advance_identity_path` composes only turns with `d_stab ≤ ε_turn`; refused/ + ill-conditioned turns get a `path_break` marker (never an identity stand-in). + Forensic contrast pinned: `test_raw_product_differs_from_lawful`. +- [x] No nonzero geometric `C_id` / conjugate corrector — admit-or-abstain only. + Nothing in the modules rewrites `F` or `A`. +- [x] `chat/runtime.py`, flag defaults, and D4 gate wiring **untouched**. Verified: + `test_gate_flag_and_bound_untouched` (flag default-off, `_WAVE_LEAKAGE_BOUND` + unchanged); A-04 quarantine pinned by `test_*_is_pure_offserving` / + `test_suite_is_offserving`. +- [x] No discrimination report, ADR-0246 body, or acceptance-packet language + written. No "semantic inalienability" / marketing claim drafted — see §5 TODO. +- [x] `docs/handoff/ADR-0244-D4-IMPLEMENTATION-PLAN.md` **not modified**. + +--- + +## 3. Placeholder values used (directive: list every one + why) + +| Placeholder | Value | Where | Why it is a placeholder | +|-------------|-------|-------|--------------------------| +| `epsilon_turn` | 0.1 | `evals/adr_0246_geometric_suite` `PLACEHOLDER_EPSILON_TURN`; test fixtures | **UNCERTIFIED.** D4 Phase 3 certified only `γ_id = 0.2126624458513829`. The two-level path budget (§3.4) is not yet calibrated. Value chosen only to exercise the mechanism (small single-turn drift admits; a 90° rotation refuses). NOT baked into any serve/module default — `PathBudget` is always caller-supplied. | +| `epsilon_session` | 0.3 | same | **UNCERTIFIED.** Same status; chosen so ~7 steps of a 0.05-rad rotation accumulate past it, demonstrating the accumulation guard. Not a policy value. | +| `_NONZERO` = 0.05 | 0.05 | eval harness | Not a calibration number — a "clearly nonzero" marker for the ">0" rows of the §6.1 table (d_stab, leakage). | +| test construction angles (0.02, 0.05, 1.0, 1.5, π/2, π) | — | tests/evals | Case constructions, not policy. Chosen to realize the exact §6.1/§6.2 geometric signatures. | + +**Explicitly NOT invented:** `γ_id` (already certified, unchanged), `τ_max`, +`s_min`. The per-turn admit surface that would consume these (§3.7) is **not built +in this scaffold** — that is the gate-wiring unit, deliberately out of scope, so no +placeholder was needed for them. + +--- + +## 4. Uncertainties in §5–§7 (directive: list anything you were unsure how to satisfy) + +1. **§3.2 general-pack `‖·‖_G` convention.** The brief fixes `d_stab = ‖A−H‖_G` + but leaves the general-pack weighted-norm convention to "ADR-0246 proper." I + implemented `‖M‖_G = ‖G^{1/2} M G^{-1/2}‖_F` (metric-consistent; reduces exactly + to Frobenius at `G=I`, which is the only shipped pack). **Needs review/ratification.** +2. **§3.6 `spatial_foreign` channel.** For the default pack (support = e1/e2/e3) it + is structurally ~0 (projection removes in-span components). Implemented generally + (residual energy on grade-1 spatial slots outside the axis support) but it cannot + fire until a non-default pack exists. Untested against a real non-default pack. +3. **§4.1 `IdentityActionRecord` full telemetry** (per-turn `field_digest`, + `record_digest`, gate/policy versions, admitted/refusal_reason) is **not built**; + only the §4.2 `IdentityPathLedger` (with `ledger_digest`, `chain_id`) is. The + per-turn record belongs to the gate-surface/telemetry unit (§3.7), out of scope + here. `advance_identity_path` returns a lightweight per-turn dict, not the full record. +4. **§6.1 "Malformed F → never silent legacy when wave field was supplied."** The + "silent legacy" clause is about the D4 gate's dual-mode fallback in + `identity.py` (untouched here, owned by D4). This scaffold adds a fail-closed + `MalformedVersorError` at the *pure primitive* boundary (`induced_action` / + `typed_residual_energy`). Whether the gate should route malformed-F to this same + typed error is a gate-wiring decision, deferred. +5. **Path budget semantics under a hard break.** I treated a scope change as: start + a fresh chain, and the triggering turn is turn 1 of the new chain (its own action + composes into the fresh `I`). The brief (§3.5) specifies a new `chain_id` and + that the old path is not continued, but does not pin whether the boundary turn + belongs to the old or new chain. Chose new-chain. **Confirm.** + +--- + +## 5. Explicitly deferred to Opus/human review (NOT written here) + +> **TODO: Opus/human review** — the following were intentionally left undone per +> the directive; do not treat their absence as an oversight: +- The **discrimination report** (§6.3): benign/adversarial rates, false-refusal, + ablations, CI-bounded separation. Requires the gate-surface + live cohorts. +- The **ADR-0246 body** and any **acceptance-packet** language. +- Any **claim about what the axes mean** ("semantic inalienability", grounding). + The §11 grounding-feasibility study (fixed cohort splits, synthetic recovery + controls, generator analysis, precision pairs, adversarial discrimination) is the + first consumer of these primitives and is **not** part of this scaffold. +- The **gate admit-surface wiring** into `IdentityScore` (§3.7) and the calibration + of `ε_turn`/`ε_session`. + +--- + +## 6. Verification (see run log for actual output) + +`docs/audit/artifacts/adr-0246-slice1-scaffold-runlog.txt`: +- `python -m evals.adr_0246_geometric_suite` → 14/14 cases passed, `all_passed=True`. +- pytest ADR-0246 suites + adjacent D4 identity surfaces → 114 passed. +- `uv run core test --suite smoke -q` → (appended to the run log). + +No PR. No merge. No status flip. Report back to Shay/Opus for the audit that +decides what, if anything, proceeds toward `main`. diff --git a/evals/adr_0246_geometric_suite/__init__.py b/evals/adr_0246_geometric_suite/__init__.py new file mode 100644 index 00000000..2d9f57e4 --- /dev/null +++ b/evals/adr_0246_geometric_suite/__init__.py @@ -0,0 +1,301 @@ +"""ADR-0246 §6.1/§6.2 synthetic geometric + path/holonomy eval suite (scaffold). + +A runnable, deterministic harness that constructs every case in the preflight +§6.1 synthetic geometric table and §6.2 path/holonomy table, runs the pure +ADR-0246 primitives (induced action ``A(F)``, ``d_orth``, ``d_stab`` vs the locked +singleton ``H_id={I}``, typed residual channels, lawful-only path ledger), and +checks each against its expected geometric signature. Every case reports +``{name, checks, passed}`` and the suite reports an overall ``passed``. + +Scope (bounded scaffold draft — NOT an accepted ADR): + * Off-serving: imports only ``algebra`` + ``core.physics.identity_{manifold,action}``; + never ``chat.runtime`` (A-04 quarantine). + * ``H_id={I}`` only; refused turns are break markers, never soft-projected ``I``; + the path composes lawful actions only — never the raw product. + * No ``C_id`` corrector; admit-or-abstain only. + * The path-suite ε values are UNCERTIFIED PLACEHOLDERS (see + ``PLACEHOLDER_EPSILON_*``): D4 Phase 3 certified only ``γ_id``; ε_turn/ε_session + are not yet calibrated. They exist here solely to exercise the mechanism and + are flagged in the run log. Do NOT read them as policy. + +No discrimination report, no claims about what the axes *mean* — that is +explicitly deferred to Opus/human review (see the slice-1 scaffold notes). +""" + +from __future__ import annotations + +from typing import Any, Callable + +import numpy as np + +from algebra.cl41 import N_COMPONENTS +from core.physics.identity_manifold import ( + IdentityManifoldGeometry, + ManifoldConditioningError, + MalformedVersorError, +) +from core.physics.identity_action import ( + IdentityChainScope, + PathBudget, + advance_identity_path, + raw_path_product, + stabilizer_defect_for_versor, +) + +# grade-2 bivector plane indices (grade-2 block starts at index 6) +_E12, _E13, _E14, _E15, _E23, _E24, _E25 = 6, 7, 8, 9, 10, 11, 12 + +# --- UNCERTIFIED PLACEHOLDERS (flagged; not policy) --------------------------- +# D4 Phase 3 certified γ_id only. ε_turn / ε_session are NOT calibrated; these +# illustrative values merely exercise the two-level path budget mechanism. +PLACEHOLDER_EPSILON_TURN: float = 0.1 +PLACEHOLDER_EPSILON_SESSION: float = 0.3 +# "clearly nonzero" marker for the >0 rows in the §6.1 table (not a threshold). +_NONZERO = 0.05 +_ZERO = 1e-9 + + +def default_geometry() -> IdentityManifoldGeometry: + """The shipped default declared frame span(e1,e2,e3), Gram = I3.""" + return IdentityManifoldGeometry.from_directions( + ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)) + ) + + +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 boost(biv: int, theta: float) -> np.ndarray: + r = np.zeros(N_COMPONENTS, dtype=np.float64) + r[0] = np.cosh(theta / 2.0) + r[biv] = np.sinh(theta / 2.0) + return r + + +def identity_versor() -> np.ndarray: + v = np.zeros(N_COMPONENTS, dtype=np.float64) + v[0] = 1.0 + return v + + +def _case(name: str, checks: dict[str, bool]) -> dict[str, Any]: + return {"name": name, "checks": checks, "passed": all(checks.values())} + + +def _expect_raises(name: str, fn: Callable[[], Any], exc: type[BaseException]) -> dict[str, Any]: + raised = False + try: + fn() + except exc: + raised = True + except Exception: # wrong exception type is a failure + raised = False + return _case(name, {f"raises_{exc.__name__}": raised}) + + +# --- §6.1 synthetic geometric suite ------------------------------------------- + + +def run_geometric_suite(geometry: IdentityManifoldGeometry | None = None) -> list[dict[str, Any]]: + geometry = geometry or default_geometry() + cases: list[dict[str, Any]] = [] + + # Identity versor → A≈I, ℓ≈0, s≈+1, d_orth≈0, d_stab≈0 + v = identity_versor() + a = geometry.induced_action(v) + leak, self_align = geometry.axis_response(v) + cases.append(_case("identity_versor", { + "A_is_identity": bool(np.allclose(a, np.eye(3), atol=1e-12)), + "leakage_zero": max(leak) < _ZERO, + "self_align_plus_one": min(self_align) > 1.0 - _ZERO, + "d_orth_zero": geometry.orthogonality_defect(v) < _ZERO, + "d_stab_zero": stabilizer_defect_for_versor(geometry, v) < _ZERO, + })) + + # In-plane π inversion of e1/e2 → ℓ≈0, s(e1)≈-1, s(e3)≈+1, d_stab>0 + v = rotor(_E12, np.pi) + leak, self_align = geometry.axis_response(v) + cases.append(_case("inplane_pi_inversion_e12", { + "leakage_zero": max(leak) < _ZERO, + "self_align_e1_minus_one": self_align[0] < -1.0 + _ZERO, + "self_align_e2_minus_one": self_align[1] < -1.0 + _ZERO, + "self_align_e3_plus_one": self_align[2] > 1.0 - _ZERO, + "d_stab_positive": stabilizer_defect_for_versor(geometry, v) > _NONZERO, + })) + + # In-plane 90° permutation e1→e2 → ℓ≈0, s(e1)≈0, d_stab>0 + v = rotor(_E12, np.pi / 2.0) + leak, self_align = geometry.axis_response(v) + cases.append(_case("inplane_90deg_permutation_e12", { + "leakage_zero": max(leak) < _ZERO, + "self_align_e1_zero": abs(self_align[0]) < _ZERO, + "self_align_e2_zero": abs(self_align[1]) < _ZERO, + "d_stab_positive": stabilizer_defect_for_versor(geometry, v) > _NONZERO, + })) + + # Mild in-plane drift step → small d_stab (per-turn passes ε_turn placeholder) + v = rotor(_E12, 0.02) + d_stab = stabilizer_defect_for_versor(geometry, v) + leak, _ = geometry.axis_response(v) + cases.append(_case("mild_inplane_drift_e12_0.02", { + "leakage_zero": max(leak) < _ZERO, + "d_stab_small_but_positive": _ZERO < d_stab < PLACEHOLDER_EPSILON_TURN, + })) + + # Alien tilt e14 → ℓ>0, null_or_conformal channel fires, boost channel ≈0 + v = rotor(_E14, 1.5) + ch = geometry.typed_residual_energy(v) + cases.append(_case("alien_tilt_e14_1.5", { + "leakage_positive": geometry.leakage_rms(v) > _NONZERO, + "null_or_conformal_fires": ch["null_or_conformal"] > _NONZERO, + "boost_like_zero": ch["boost_like"] < _ZERO, + "unclassified_clean": ch["unclassified"] < _ZERO, + })) + + # Boost component (e5) → ℓ,s normalized in range, boost channel fires, d_orth>0 + v = boost(_E15, 1.0) + ch = geometry.typed_residual_energy(v) + leak, self_align = geometry.axis_response(v) + cases.append(_case("boost_e15_1.0", { + "leakage_in_unit_range": 0.0 <= max(leak) <= 1.0, + "self_align_in_range": all(-1.0 - _ZERO <= s <= 1.0 + _ZERO for s in self_align), + "boost_like_fires": ch["boost_like"] > _NONZERO, + "null_or_conformal_zero": ch["null_or_conformal"] < _ZERO, + "d_orth_positive": geometry.orthogonality_defect(v) > _NONZERO, + })) + + # Near-singular Gram (near-parallel axes) → ManifoldConditioningError at build + cases.append(_expect_raises( + "near_singular_gram", + lambda: IdentityManifoldGeometry.from_directions( + ((1.0, 0.0, 0.0), (1.0, 1e-9, 0.0), (0.0, 0.0, 1.0)) + ), + ManifoldConditioningError, + )) + + # Malformed F → MalformedVersorError (NaN and wrong-shape) + nan_v = identity_versor() + nan_v[0] = np.nan + cases.append(_expect_raises( + "malformed_f_nan", lambda: geometry.induced_action(nan_v), MalformedVersorError + )) + cases.append(_expect_raises( + "malformed_f_wrong_shape", + lambda: geometry.induced_action(np.ones(7, dtype=np.float64)), + MalformedVersorError, + )) + return cases + + +# --- §6.2 path / holonomy suite ----------------------------------------------- + + +def _scope(pack: str = "packA") -> IdentityChainScope: + return IdentityChainScope( + pack_content_digest=pack, + geometry_version="geomV1", + policy_version="polV1(PLACEHOLDER_epsilons)", + session_id="sess1", + biography_epoch=None, + ) + + +def run_path_suite(geometry: IdentityManifoldGeometry | None = None) -> list[dict[str, Any]]: + geometry = geometry or default_geometry() + budget = PathBudget( + epsilon_turn=PLACEHOLDER_EPSILON_TURN, + epsilon_session=PLACEHOLDER_EPSILON_SESSION, + ) + gram = geometry.gram + ident = geometry.induced_action(identity_versor()) + cases: list[dict[str, Any]] = [] + + # Sequence of lawful near-I turns → A_path near I; no false path refusal + ledger = None + for _ in range(20): + ledger, _ = advance_identity_path(ledger, _scope(), ident, gram, budget) + cases.append(_case("lawful_near_identity_sequence", { + "path_near_identity": bool(np.allclose(ledger.a_path_lawful, np.eye(3), atol=1e-12)), + "session_admit": ledger.session_admit, + "no_breaks": ledger.break_count == 0, + "composed_20": ledger.composed_turn_count == 20, + })) + + # Small in-plane rotations each < ε_turn → path eventually breaches ε_session + small = geometry.induced_action(rotor(_E12, 0.05)) + ledger = None + all_turns_lawful = True + for _ in range(40): + ledger, rec = advance_identity_path(ledger, _scope(), small, gram, budget) + all_turns_lawful = all_turns_lawful and rec["lawful"] + if not ledger.session_admit: + break + cases.append(_case("small_rotations_accumulate_to_session_refusal", { + "each_turn_lawful": all_turns_lawful, + "session_refused": not ledger.session_admit, + "path_d_stab_exceeds_session": ledger.d_stab_path > PLACEHOLDER_EPSILON_SESSION, + })) + + # Interleaved refuse + admit → refused turns break, excluded, raw recorded + big = geometry.induced_action(rotor(_E12, np.pi / 2.0)) + seq = [ident, big, ident, big, ident] + ledger = None + breaks_pattern = [] + for a in seq: + ledger, rec = advance_identity_path(ledger, _scope(), a, gram, budget) + breaks_pattern.append(rec["path_break"]) + cases.append(_case("interleaved_refuse_admit", { + "composed_3": ledger.composed_turn_count == 3, + "breaks_2": ledger.break_count == 2, + "break_pattern": breaks_pattern == [False, True, False, True, False], + })) + + # Pack digest change → hard break, new chain_id, old path not continued + ledger, _ = advance_identity_path(None, _scope("packA"), ident, gram, budget) + ledger, _ = advance_identity_path(ledger, _scope("packA"), small, gram, budget) + id_a, drifted = ledger.chain_id, ledger.a_path_lawful.copy() + ledger, rec = advance_identity_path(ledger, _scope("packB"), ident, gram, budget) + cases.append(_case("hard_break_on_pack_change", { + "hard_break": rec["hard_break"], + "new_chain_id": ledger.chain_id != id_a, + "old_path_not_continued": not np.allclose(ledger.a_path_lawful, drifted), + "fresh_chain": ledger.composed_turn_count == 1 and ledger.break_count == 0, + })) + + # Raw product ≠ lawful product when a refused turn is present (forensic) + seq = [ident, big, ident] + ledger = None + for a in seq: + ledger, _ = advance_identity_path(ledger, _scope(), a, gram, budget) + raw = raw_path_product(seq) + cases.append(_case("raw_product_differs_from_lawful", { + "raw_neq_lawful": not np.allclose(raw, ledger.a_path_lawful), + "lawful_excludes_refused": bool(np.allclose(ledger.a_path_lawful, np.eye(3), atol=1e-12)), + })) + return cases + + +def build_suite_report() -> dict[str, Any]: + geometry = default_geometry() + geometric = run_geometric_suite(geometry) + path = run_path_suite(geometry) + all_cases = geometric + path + return { + "schema_version": "adr_0246_geometric_suite_v1", + "declared_frame": ["truthfulness=e1", "coherence=e2", "reverence=e3"], + "stabilizer": "H_id={I} (locked)", + "placeholders": { + "epsilon_turn": PLACEHOLDER_EPSILON_TURN, + "epsilon_session": PLACEHOLDER_EPSILON_SESSION, + "note": "UNCERTIFIED — D4 Phase 3 certified only gamma_id; ε not calibrated", + }, + "geometric_suite": geometric, + "path_suite": path, + "case_count": len(all_cases), + "passed_count": sum(1 for c in all_cases if c["passed"]), + "all_passed": all(c["passed"] for c in all_cases), + } diff --git a/evals/adr_0246_geometric_suite/__main__.py b/evals/adr_0246_geometric_suite/__main__.py new file mode 100644 index 00000000..45b1f608 --- /dev/null +++ b/evals/adr_0246_geometric_suite/__main__.py @@ -0,0 +1,42 @@ +"""Run the ADR-0246 §6.1/§6.2 geometric + path eval suite and emit the report. + +Usage: uv run python -m evals.adr_0246_geometric_suite [out.json] + +Deterministic, off-serving. Prints a per-case pass/fail summary and the overall +verdict; optionally writes the structured JSON report. Exit code 0 iff every case +passed (so it can gate a run log), 1 otherwise. +""" + +from __future__ import annotations + +import json +import sys + +from evals.adr_0246_geometric_suite import build_suite_report + + +def main() -> int: + report = build_suite_report() + for suite in ("geometric_suite", "path_suite"): + print(f"\n[{suite}]") + for case in report[suite]: + mark = "PASS" if case["passed"] else "FAIL" + print(f" {mark} {case['name']}") + if not case["passed"]: + for check, ok in case["checks"].items(): + if not ok: + print(f" ✗ {check}") + print( + f"\n{report['passed_count']}/{report['case_count']} cases passed; " + f"all_passed={report['all_passed']}" + ) + print(f"placeholders (uncertified): {report['placeholders']}") + if len(sys.argv) > 1: + with open(sys.argv[1], "w", encoding="utf-8") as fh: + fh.write(json.dumps(report, indent=2, sort_keys=True) + "\n") + print(f"report written to {sys.argv[1]}") + return 0 if report["all_passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_adr_0246_geometric_suite.py b/tests/test_adr_0246_geometric_suite.py new file mode 100644 index 00000000..70c421c9 --- /dev/null +++ b/tests/test_adr_0246_geometric_suite.py @@ -0,0 +1,98 @@ +"""ADR-0246 §6.1/§6.2 eval-suite pins — every synthetic case fails loudly. + +Pins the runnable ``evals.adr_0246_geometric_suite`` harness AND the specific +§6.1 rows the preflight table calls out (π-inversion ``s≈-1``, 90° permutation +``s≈0``, near-singular Gram → ``ManifoldConditioningError``, malformed F → +``MalformedVersorError``), so a regression names the exact broken case. +""" + +from __future__ import annotations + +import numpy as np +import pytest + +from algebra.cl41 import N_COMPONENTS +from core.physics.identity_manifold import ( + IdentityManifoldGeometry, + ManifoldConditioningError, + MalformedVersorError, +) +from evals.adr_0246_geometric_suite import ( + build_suite_report, + default_geometry, + identity_versor, + rotor, + run_geometric_suite, + run_path_suite, +) + +_E12 = 6 + + +@pytest.fixture(scope="module") +def geometry() -> IdentityManifoldGeometry: + return default_geometry() + + +def test_full_suite_all_cases_pass(): + report = build_suite_report() + # every case must pass; the assertion message names any that did not + failed = [c["name"] for c in report["geometric_suite"] + report["path_suite"] if not c["passed"]] + assert failed == [], f"failing cases: {failed}" + assert report["all_passed"] is True + assert report["case_count"] == report["passed_count"] == 14 + + +@pytest.mark.parametrize("case", [c["name"] for c in run_geometric_suite()]) +def test_each_geometric_case_passes(geometry, case): + result = {c["name"]: c for c in run_geometric_suite(geometry)}[case] + assert result["passed"], result["checks"] + + +@pytest.mark.parametrize("case", [c["name"] for c in run_path_suite()]) +def test_each_path_case_passes(geometry, case): + result = {c["name"]: c for c in run_path_suite(geometry)}[case] + assert result["passed"], result["checks"] + + +# --- explicit §6.1 pins (directive step 4: fail loudly on the exact expected) -- + + +def test_pi_inversion_self_align_is_minus_one(geometry): + _, self_align = geometry.axis_response(rotor(_E12, np.pi)) + assert self_align[0] == pytest.approx(-1.0, abs=1e-9) # e1 inverted + assert self_align[1] == pytest.approx(-1.0, abs=1e-9) # e2 inverted + assert self_align[2] == pytest.approx(1.0, abs=1e-9) # e3 fixed + + +def test_90deg_permutation_self_align_is_zero(geometry): + _, self_align = geometry.axis_response(rotor(_E12, np.pi / 2.0)) + assert self_align[0] == pytest.approx(0.0, abs=1e-9) # e1 → e2, orthogonal + assert self_align[1] == pytest.approx(0.0, abs=1e-9) + + +def test_near_singular_gram_fails_closed(): + with pytest.raises(ManifoldConditioningError): + IdentityManifoldGeometry.from_directions( + ((1.0, 0.0, 0.0), (1.0, 1e-9, 0.0), (0.0, 0.0, 1.0)) + ) + + +def test_malformed_versor_raises_typed_error(geometry): + nan_v = identity_versor() + nan_v[5] = np.inf + with pytest.raises(MalformedVersorError): + geometry.induced_action(nan_v) + with pytest.raises(MalformedVersorError): + geometry.typed_residual_energy(np.ones(N_COMPONENTS + 3, dtype=np.float64)) + + +def test_suite_is_offserving(): + import evals.adr_0246_geometric_suite as suite + + assert suite.__file__ is not None + with open(suite.__file__, encoding="utf-8") as fh: + src = fh.read() + # no actual import of serve modules (the A-04 note in the docstring names + # chat.runtime as forbidden, so match import statements, not the substring) + assert "import chat" not in src and "from chat" not in src