Wires SafetyCheck and EthicsCheck into ChatRuntime at end-of-turn on
both the main articulation path and _stub_response. Verdicts attach
to ChatResponse.safety_verdict / .ethics_verdict and TurnEvent.
Observational at v1: no refusal, no re-articulation, no behavioral
change. Refusal policy is the next ADR with real verdict data in hand.
Runtime-checkable predicates today:
- preserve_versor_closure (via _FieldStateWithVersor adapter)
- no_identity_override (manifold hash before vs after; equal by construction)
- no_silent_correction (runtime._last_refusal_was_typed bookkeeping)
- acknowledge_uncertainty (IdentityScore.alignment + hedge detection)
- disclose_limitations (walk_surface == _UNKNOWN_DOMAIN_SURFACE)
Predicates with no runtime evidence (no_manipulation, no_fabricated_source,
defer_high_stakes_to_human_review, respect_user_autonomy, no_hot_path_repair)
honestly report runtime_checkable=False per the ADR-0032/0034 discipline.
They become checkable as classifiers and pipelines land — surface contract
doesn't change.
Test coverage: 14 new tests; combined pack-layer surface suite (loaders +
checks + turn-loop) now 122 green. CLI suites unaffected: smoke 67,
cognition 121, teaching 17, runtime 19. Cognition eval baseline preserved.
Completes the predicate-surface layer for ethics packs, sibling to
ADR-0032's SafetyCheck. Same registry-of-predicates shape; same
observational discipline; same honest reporting of runtime-checkable=False
for structural commitments that cannot be evaluated from per-turn evidence.
Five default predicates for the v1 commitments:
acknowledge_uncertainty — alignment < threshold ⇒ requires hedge
defer_high_stakes_to_human_review — high_stakes ⇒ requires recommend_review
disclose_limitations — ungrounded ⇒ requires disclosure marker
no_manipulation — structural; runtime_checkable=False
respect_user_autonomy — prescriptive ⇒ requires ≥2 options surfaced
`no_manipulation` is the ethics-side analogue of `no_hot_path_repair`
in SafetyCheck — an aggregate property enforced by realizer design and
review, not a per-turn metric. Honest reporting rather than a silent
upheld pass.
ChatRuntime exposes `runtime.ethics_check`; turn loop does not
auto-invoke. Refusal / re-articulation wiring is a future ADR.
Test coverage: 27 new tests; combined pack-layer surface suite
(identity + safety + ethics, loaders + checks) is now 108 tests, all
green. Cognition (121), teaching (17), runtime (19), smoke (67)
unaffected.
Completes the three-layer pack architecture:
identity (who CORE is) + safety (universal red lines)
+ ethics (deployment-specific propositional commitments)
manifold.boundary_ids = identity.boundary_ids
∪ safety.boundary_ids
∪ ethics.commitment_ids
Ethics packs are swappable like identity (fall back to default on load
failure) but propositional like safety (commitment ids union into the
manifold). EthicsPackError inherits from ValueError; only when both
the requested and default packs fail does startup refuse.
Ships default_general_ethics_v1 with five commitments:
- acknowledge_uncertainty
- defer_high_stakes_to_human_review
- disclose_limitations
- no_manipulation
- respect_user_autonomy
Ratified through identity_anchor template at SHA 81fc9b61c828….
Test coverage: 20 new tests; combined identity/safety/ethics surface
suite is 81 tests, all green. Cognition (121), teaching (17), runtime
(19), smoke (67), and cognition eval all unaffected.
Closes the 'boundaries are checked at scattered call sites' gap noted
in ADR-0029. Adds a centralized observational surface parallel in
shape to IdentityCheck — produces a verdict, does not refuse. Wiring
verdicts into refusal paths is a future ADR.
Shape (parallel to IdentityCheck, different in mechanism):
SafetyContext — duck-typed input bag (field_state, citations,
refusal-was-typed flag, identity manifold hashes
before/after). Every field optional with safe
defaults; absence of evidence is not evidence of
violation.
SafetyCheckResult — per-boundary: boundary_id, upheld, reason,
runtime_checkable, evidence tuple.
SafetyVerdict — aggregate: pack_id, results (lex order on
boundary_id), upheld, violated_boundaries,
runtime_checkable_count.
SafetyCheck — registry of predicates; check(ctx, pack) returns
SafetyVerdict. register(boundary_id, predicate)
adds custom predicates.
Five default predicates for v1 boundaries:
preserve_versor_closure runtime_checkable=True field.versor_condition < 1e-6
no_fabricated_source runtime_checkable=True* cited ⊆ allowed
no_silent_correction runtime_checkable=True last refusal was typed
no_identity_override runtime_checkable=True* hash before == hash after
no_hot_path_repair runtime_checkable=FALSE code-path; static-analysis
*Conditional on the caller supplying the necessary fields.
The honest answer on no_hot_path_repair: it is a code-path boundary
enforced by static analysis + code review. Runtime cannot judge it.
A predicate that silently reported upheld=True would be a small lie —
exactly the kind of thing CLAUDE.md forbids. SafetyCheck reports
runtime_checkable=False with a clear reason so auditors see the truth.
ChatRuntime integration:
ChatRuntime.__init__ now constructs self.safety_check = SafetyCheck()
alongside self._identity_check. Turn loop does NOT auto-invoke at
v1 — operators and future ADRs decide when/where to call it.
Files:
packs/safety/check.py new — SafetyCheck + value types +
default predicates
packs/safety/__init__.py re-exports the new public surface
chat/runtime.py constructs self.safety_check
tests/test_safety_check.py new — 20 tests covering each
default predicate (positive +
negative), unknown-boundary
fallback, custom registration,
defensive boundary-id rebinding,
verdict aggregation, ChatRuntime
integration
docs/decisions/ADR-0032-safety-check-surface.md Accepted
docs/safety_packs.md §SafetyCheck section added,
known-limit #1 struck through
memory/safety-pack.md refreshed; new follow-up about
turn-loop auto-invocation
Suite status (all green):
cognition 121, teaching 17, runtime 19, formation 182, smoke 67
identity / safety / surface divergence suites: 108 tests passing
(was 88 before this ADR; +20 safety-check tests)
Scope limits (documented):
- No auto-invocation in the turn loop.
- No refusal wiring on violation.
- No refactoring of existing scattered enforcement sites.
- Defensive boundary-id rebinding masks predicate bugs; debug-mode
surfacing is a future enhancement.
Closes the 'identity hedges are generic' gap. When IdentityCheck reports
that a specific axis is deviating AND the pack supplies an axis_hedges
entry for that axis, the assembler uses that axis's phrase instead of
ADR-0028's generic preferred_hedge_*. The hedge text now names what is
actually at issue.
Selection: lex-smallest axis_id in (ctx.deviation_axes ∩ axis_hedges).
Deterministic; loader emits axis_hedges in lex order on axis_id.
Example surface at alignment=0.30 (strong band) under default pack:
No deviation → 'It seems that truth reveals reality.'
truthfulness deviates → 'Evidence is thin that truth reveals reality.'
coherence deviates → 'This does not yet cohere: truth reveals reality.'
reverence deviates → 'Reports suggest truth reveals reality.'
Same trajectory + truthfulness deviation, three different packs:
default_general_v1 → 'Evidence is thin that truth reveals reality.'
precision_first_v1 → 'The evidence does not support that truth reveals reality.'
generosity_first_v1 → 'Truth reveals reality.' (above generosity's strong=0.20)
Schema (additive, optional):
surface_preferences.axis_hedges = {
<axis_id>: { 'strong': str, 'soft': str, 'qualifier': str },
...
}
Bounds: each phrase length 1–64; axis_id non-empty. Absent block →
ADR-0028 byte-for-byte fallback. Loader emits pairs in lex order on
axis_id for hashability + deterministic tie-break.
Files:
core/physics/identity.py
+ class AxisHedge (frozen: strong, soft, qualifier)
SurfacePreferences gains axis_hedges: Tuple = ()
packs/identity/loader.py
+ _build_axis_hedges(): parse + bounds-check + emit lex-ordered tuple
generate/surface.py
SurfaceContext gains deviation_axes: frozenset[str] + axis_hedges tuple
+ _axis_specific_phrase(ctx): lex-smallest match or None
_apply_hedge consults axis-specific phrase before ADR-0028 fallback
Depth languages (he, grc) unchanged — ADR-0030 canonical phrases
chat/runtime.py
_build_surface_context lifts identity_score.deviation_axes and
prefs.axis_hedges into SurfaceContext
packs/identity/*.json
Three v1 packs gain axis_hedges blocks (truthfulness, coherence,
reverence — each pack uses voice consistent with its character)
scripts/ratify_identity_packs.py (no change — idempotent)
packs/identity/*.mastery_report.json
Auto-refreshed. New SHAs:
default_general_v1 → 2ab7d469013509ba5030313ca9a609a443d0716e3ddcc5596f59858ce054f5d3
precision_first_v1 → 78aa1e6a68a35c2c8576b6196a52d421b94f6d11e006128986902a4fd08679af
generosity_first_v1 → 511f1ce20edd4266239da61443bfc93473a5433f20bfee6692a25a03073dc933
Tests: tests/test_identity_score_decomposition.py — 17 new tests:
per-axis phrase selection, band gating still applies, pack swap with
same deviation produces three different phrases, lex tie-break is
deterministic, depth-language fallback to ADR-0030, backward compat
with empty deviation_axes, and the contract that all three v1 packs
ship axis_hedges for all three default-pack axes.
Suite status (all green):
cognition 121, teaching 17, runtime 19, formation 182, smoke 67
identity+safety+English+depth divergence 71
score decomposition 17
Scope limits (documented in ADR-0031):
- English-only at v1 (depth languages use canonical ADR-0030 phrases)
- Lex tie-break is operational not semantic — pack authors can re-key
if they need a different priority
- No dominance-driven phrasing (Interpretation A); preserved as
forward-compatible follow-up
Docs: ADR-0031 (Accepted) recorded; docs/identity_packs.md gains
§Axis-specific hedge phrases section and updated v1-pack SHAs; memory
'identity-packs.md' refreshed.
Closes the ADR-0028 'English-only differentiation' gap. Hebrew and
Koine Greek surfaces now consult identity-pack surface_preferences for
hedge and claim-strength shaping, using language-appropriate canonical
hedge phrases. CORE's three-language foundation (English / Hebrew /
Greek) is now uniformly identity-aware at the realizer.
Algorithm: the same four-band hedge/claim-strength logic from ADR-0028
runs for all three languages. Thresholds and claim_strength come from
the identity pack (carried on SurfaceContext). Hedge phrases come
from ctx for English and from a new module-level constant
_DEPTH_HEDGE_PHRASES for Hebrew (he) and Koine Greek (grc).
he: 'נראה ש' / 'אולי' / 'במקרים מסוימים,'
grc: 'δοκεῖ ὅτι' / 'ἴσως' / 'ἐνίοτε,'
Pack swap visibly affects depth-language output: a precision_first
identity pulls hedges to higher alignment than default; a generosity
pack pulls them to lower alignment. Same trajectory through the
manifold → three different Hebrew surfaces under three different
packs. Same for Greek.
Files:
generate/surface.py
_DEPTH_HEDGE_PHRASES (new module constant)
_apply_hedge(surface, ctx, lang='en') — lang param added
_assemble_he(.., ctx) — ctx param added
_assemble_grc(.., ctx) — ctx param added
SentenceAssembler.assemble — passes context to he/grc
tests/test_identity_surface_divergence_depth.py — 15 new tests:
Hebrew hedge bands, Greek hedge bands, pack-swap divergence in
both depth languages, three-language hedge phrase distinctness,
backward compatibility with ctx=None
docs/decisions/ADR-0030-depth-language-hedge.md — Accepted
docs/identity_packs.md — closes known-limit #1
memory/identity-packs.md — refreshed
Backward compat:
- _apply_hedge default lang='en' so existing callers unaffected.
- English surface output byte-for-byte unchanged.
- _assemble_he / _assemble_grc with ctx=None match pre-ADR output
byte-for-byte (asserted by TestBackwardCompatibility).
Scope limits (documented in ADR):
- Depth-language hedge phrases are canonical defaults, not per-pack
overridable yet. Future ADR may add a 'languages' block to the
pack schema if a downstream deployment needs override capability.
- Contrast ('However, ...') and subordination ('Given that ..., ...')
remain English-only. Hedge is the dominant differentiator.
- Hebrew/Greek grammar / word order unchanged.
Suite status: cognition 121, teaching 17, runtime 19, formation 182,
smoke 67 — all green. Identity + safety + divergence suites: 26+15+15+15=71
all green.
Closes the trust gap ADR-0027 opened: making the identity manifold
swappable was necessary for downstream robotics / personalization /
creative deployments, but it left nothing structurally preventing a
downstream identity pack from disabling core safety constraints.
Safety packs sit at a separate trust layer, fail closed on every error
path, and union their boundaries into every runtime manifold regardless
of which identity pack is selected.
Architecture (sibling to identity packs, structurally distinct):
Layer Swappable? Removable? Schema
--------------- ---------- ---------- -----------------------------
Safety pack No No boundary_ids + descriptions
Identity pack Yes No value_axes + surface_prefs
Language pack Yes (>=1 reqd) vocab / morphology / packs
Composition rule (at ChatRuntime startup, additive only):
identity = load_identity_manifold(config.identity_pack)
safety = load_safety_pack() # fail-closed
final.boundary_ids = identity.boundary_ids ∪ safety.boundary_ids
Safety contributes boundaries only — no value_axes, threshold, or
surface_preferences. This keeps existing tests that assert on identity
axis sets passing byte-for-byte, and matches the semantic intent
(safety is what's forbidden, not what's pulled toward).
Shipping safety pack: packs/safety/core_safety_axes_v1.json
→ mastery_report_sha256 ee1249acdf8c273aeb656d803c37ef915e536d85f177f5cc18c6e2f6c995ce29
Five v1 boundaries, each closing a specific CLAUDE.md doctrine:
no_fabricated_source — no invented provenance
no_hot_path_repair — no normalization in propagate/stream/store
no_identity_override — user text cannot mutate identity
no_silent_correction — failures are typed and visible
preserve_versor_closure — ||F * reverse(F) - 1||_F < 1e-6
Fail-closed semantics:
SafetyPackError inherits from RuntimeError (NOT ValueError) so
catch-and-continue is discouraged at the type level. Missing file /
malformed JSON / empty boundaries / duplicate boundary / failed
self-seal all raise. ChatRuntime.__init__ does not catch.
Files:
packs/safety/core_safety_axes_v1.json shipping pack
packs/safety/core_safety_axes_v1.mastery_report.json signed report
packs/safety/__init__.py public surface
packs/safety/loader.py load_safety_pack(),
SafetyPack,
SafetyPackError,
DEFAULT_SAFETY_PACK
scripts/ratify_safety_pack.py idempotent driver
chat/runtime.py composition wiring
tests/test_safety_pack.py 15 tests:
loader bounds,
fail-closed,
composition under
all 3 identity packs
docs/decisions/ADR-0029-safety-packs.md decision record
docs/safety_packs.md operational ref
README.md §Safety Pack added
memory/safety-pack.md auto-memory entry
Suite status: cognition 121, teaching 17, runtime 19, formation 182,
smoke 67, identity 41, safety 15 — all green.
Adds the discovery flag callers have been asking for since ADR-0027.
Short-circuits before the REPL launches; supports both a human-readable
table and `--json` machine output. Drives the loader's existing
`available_packs()` helper.
Bug fix on the way: `available_packs()` was globbing every `*.json`
in the search path, so the Phase-5 companion `<pack_id>.mastery_report.json`
files were leaking into the list as fake packs with empty fields. The
helper now skips any file ending in `.mastery_report.json` and rejects
JSON that lacks the required `schema_version` / `value_axes` fields.
CLI output:
pack_id version ratified description
------------------- ------- -------- -----------
default_general_v1 1.0.0 yes Balanced general identity...
generosity_first_v1 1.0.0 yes Generosity-first specialization...
precision_first_v1 1.0.0 yes Precision-first specialization...
Tests: +3 (CLI table, CLI JSON, companion-file filter regression).
test_identity_packs.py: 23 -> 26. cognition / smoke green.
Docs: docs/identity_packs.md CLI usage block updated; memory
'identity-packs.md' closes that follow-up.
Drives the three v1 identity packs through the full formation pipeline
(Forge -> Compose -> Compile -> Run -> Ratify) and embeds the resulting
self-sealed MasteryReport SHAs into each pack file. Companion
'<pack_id>.mastery_report.json' artifacts ship alongside. Loader now
defaults to production mode (require_ratified=None) and ChatRuntime
calls it without the dev-only override.
Ratification results:
default_general_v1 -> 0b77357fe4359f161d7ca72f184b6e0db2f9e2de16b32c237a3b80d2bbb005b4
precision_first_v1 -> 5f5000dba9a0dd19d831e9ab5d3c0e3b9faf6abdc2648940e96aa6263af3302e
generosity_first_v1 -> 91716117558113f74b2c6d07a804cb324f262d62b743523d901d1386a4f85ae4
Driver: scripts/ratify_identity_packs.py — idempotent. Re-running on
already-current packs is a no-op (verified by a test). Each pack is
treated as its own provenance source: source_sha = SHA-256 of the pack's
canonical JSON body with mastery_report_sha256 blanked, so the
self-referential chain stays stable across SHA updates. Axes become
ConceptCandidates; canned override-attempt triples become
CounterCandidates; the identity_anchor template renders the body.
Loader hardening (packs/identity/loader.py):
* When require_ratified resolves to True, the loader now requires the
companion '<pack_id>.mastery_report.json' to exist, its
report_sha256 to match the pack's mastery_report_sha256, and its
self-seal to verify via formation.hashing.verify_seal.
* Tampered companion (wrong SHA, broken seal) is rejected with a
diagnostic IdentityPackError.
Tests: 18 -> 23. New cases cover production-mode loading of all three
v1 packs, missing companion file, mismatched companion SHA, failed
self-seal, and end-to-end idempotency of the ratification script
(subprocess-launched, asserts pack bytes unchanged on re-run).
Suite status: cognition 121, teaching 17, runtime 19, formation 182,
smoke 67 — all green.
Docs updated: ADR-0027 status flipped to Phases 1-6 complete with the
three report SHAs recorded; docs/identity_packs.md notes the ratified
SHAs and the re-ratification command; memory file 'identity-packs.md'
refreshed.
Adds the four templates called out in docs/teaching_order.md so the formation
pipeline can ratify more than just definitional ontologies:
* composed_relation — Layer 4. Chains are the unit of mastery; each chain of
length >= 2 emits a composed_relations entry with composition_kind
(transitive | lifting), an inferred relation, and chain-break adversarial
probes drawn from counters or canned.
* procedural — ordered state transitions; strict_linear_topo refuses
branches, cycles, and disconnected components at render time.
ordering_hints validated against the linear chain. Canned violation
probes for precondition_violation / step_skip / back_edge.
* falsification — counter-example-driven. Counters move to Phase 2 paired
with coherent alternatives drawn from relations sharing the same head.
Unmatched counters surface in unmatched_counters; false-coherent probes
emitted per pair.
* identity_anchor — Layer 1 seeding. Concepts interpreted as identity axes
ranked by ordering_hints; counters interpreted as override attempts;
canned IDENTITY_OVERRIDE_PROBES always appended.
Common helpers extracted to formation/templates/_common.py: canonical
constants (MAX_VERSOR_CONDITION, RATIFICATION_GATES, PROMOTION_PATH,
IDENTITY_OVERRIDE_PROBES, NORMALIZATION_FORBIDDEN_SITES), deterministic
ordering (sorted_concepts/_counters/_hints, topo_sorted_relations,
strict_linear_topo), payload builders, geometric_dependencies,
maximal_chain_walks, adversarial_block, course_id, subject_payload,
substrate_invariants_payload, phase_5_payload.
formation/templates/__init__.py now dispatches via a lazy-import _REGISTRY
keyed by template_id; registered_template_ids() exposed for callers and
tests. definition.py refactored to use _common verbatim — byte-stability
preserved (existing test_compose.py still passes; test_sha_stable_across_
subprocess unchanged).
Tests: 44 new tests across test_template_{composed_relation,procedural,
falsification,identity_anchor,registry}.py. Each new template gets
determinism, paradigm-structure, error-handling, and cross-subprocess SHA
stability tests; registry test asserts the five known ids and that
identical inputs through different templates produce different SHAs.
Formation suite: 138 -> 182 passing. cognition (121) and smoke (67)
suites unchanged. ratify.py enforcement of the new paradigm-specific
gates (every_composed_relation_replayed, linear_order_strict, etc.)
remains a documented follow-up — templates declare the gates in their
phase_5 body so the ratifier extension is purely additive.
- Add docs/teaching_order.md as durable reference for curriculum ordering.
Five-layer rule (identity axes -> atomic definitions -> binary relations ->
composed relations -> domain expansion), grounded in ratify.py G3,
MasteredCoursesIndex, and exact CGA distance. Linked from README.md.
- Mark formation_pipeline_plan.md as IMPLEMENTED (back half); enumerate the
open items not closed by Phases 1-7 (additional templates, first formation-
routed curriculum, G2 activation).
- Add 2026-05-17 status block to capability_roadmap.md covering the FSC chain,
epistemic schema closure, formation pipeline back half, FSC v3 proof matrix,
cost benchmark, and the pulse import fix.
0xC0_RELOG0 contained non-hex digits (R, L, G) producing a SyntaxError
at module import, which made core pulse unable to load the GloVe-backed
manifold. Replace the wordplay constant with the equivalent integer
literal from the comment (3236855408) so the deterministic seed is
preserved and the import path is restored.
Two-pronged self-documentation pass so reviewers / investors / the
future team can revisit any artifact cold and immediately understand
what it tests, what to expect, and what to do if the numbers shift.
Inline preambles (`core demo`):
Before each demo's results table, print a structured preamble:
- WHAT THIS DEMO TESTS mechanism + corpus shape
- WHAT TO EXPECT IF WORKING concrete pass numbers
- WHAT TO LOOK FOR specific signals on regression
- WHEN TO TWEAK falsifiability + corpus authoring rules
Suppressed under --json so machine-readable output is uncluttered.
Wired into:
core demo phase5 (5-family stratified mechanism-isolation)
core demo phase6 (3-condition head-to-head vs baseline)
core demo all (combined; both preambles + a "what this means"
summary after the combined table)
Per-directory READMEs:
evals/forward_semantic_control/results/README.md
- Inventory of every JSON report with headline metrics
- Per-report interpretation guide ("when to look here")
- Per-case schema reference
- "When something looks wrong" troubleshooting tree
- Cross-links to ADRs, runtime_contracts, findings docs
evals/forward_semantic_control/public/v2_phase5/README.md
- The five failure-mode families, geometric construction, and
expected behaviour per mode
- Case schemas (single-step + chained) with field semantics
- How cases were geometrically mined (phase5_mine.py)
- Authoring rules: add cases, never relax assertions
evals/forward_semantic_control/public/v2_phase6_demo/README.md
- The three conditions with case counts and what each proves
- Why the baseline is in-system (not a transformer LLM) — table
- Case schema with the `condition` field
- Authoring rules: surface specific asymmetry, never relax predicate
evals/forward_semantic_control/public/inner_loop_benign/README.md
- Why this corpus exists (replaces adversarial-by-accident v1/dev)
- The Cl(4,1) signature quirk (23/85 tokens with negative
self-cga_inner) and the 0.25 self-score authoring filter
- Expected exhaustion_rate per condition
- How to verify a new case before committing (one-liner snippet)
New contract tests (tests/test_cli_demo.py::TestDemoPreambles + ::TestResultsReadme):
- Phase 6 preamble explains C1/C2/C3 and the in-system baseline rationale
- Phase 5 preamble explains all five families AND that δ is falsifiable
- Preamble suppressed under --json (parseable JSON from byte 0)
- `demo all` runs both preambles + a "what this means" summary
- results/README.md mentions every phase report file
- All three corpus READMEs exist
Tests: 1107 passed, 2 skipped (+8 from preceding baseline).
No mechanism changes — all additions are documentation surface.
Patent-grade precision pass over the doc surface so every claim
about the Forward Semantic Control chain is backed by a file path,
test count, or commit hash.
Updates by file:
README.md
- Modernize Quick Start: add `core test --suite adr-0024`,
`core demo phase6 / phase5 / all / list-results`, full CLI map.
- New "Forward Semantic Control — The ADR-0024 Chain" section
with layer/ADR mapping and CI-enforced C1/C2/C3 claims table.
- Cross-links to runtime_contracts.md, phase5_stratified_findings,
phase6_comparative_demo, and the central results directory.
docs/decisions/README.md
- Index was stale at ADR-0014. Add ADR-0015 through ADR-0026
with accurate Accepted statuses.
- New "ADR-0024 chain — Forward Semantic Control closure" section
laying out the five-ADR / six-commit dependency order with test
counts per phase.
docs/runtime_contracts.md
- Add "Ranked-with-margin contract (ADR-0026 / Phase 3)" section
between the existing Phase 2 refusal and Phase 4 rotor sections.
- Documents threshold-mode vs margin-mode behaviour, δ = 0.4
default, falsifiability gate, and Cl(4,1) signature motivation.
docs/PROGRESS.md
- Add naming-note disambiguation: capability-roadmap "Phase N"
vs ADR-0024 chain "Phase N" are distinct.
- New top-of-document "ADR-0024 Chain — Forward Semantic Control
Closure" section with per-phase commit + test-count table and
a single-command verification path.
docs/Whitepaper.md
- New Section XII "Forward Semantic Control — Generation Without
Sampling" before Extensions. Five-component description of the
mechanism (region, intersection, destination check, rotor check,
margin gate) with explicit "what a sampling LLM cannot exhibit"
contrast. Existing Section XII renumbered to XIII.
docs/Yellowpaper.md
- New Section IX-B "Forward Semantic Control — Formal Admissibility
Specification" with eight subsections covering:
1. AdmissibilityRegion typed triple (I, B, Φ)
2. Destination-side admissibility (σ_dest, admit_threshold)
3. Rotor-side admissibility (σ_rotor, admit_rotor)
4. Ranked-with-margin gate (admit_margin, deterministic
tie-break by index, default δ = 0.4)
5. Honest refusal (InnerLoopExhaustion typed evidence,
RefusalReason enum, trace fold)
6. Composition order at the generation seam
(admit_step = intersection ∧ destination ∧ rotor)
7. Replay determinism contract (5 test lanes pinning byte
identity across reruns)
8. Verification invariants table (6 new structural contracts)
- Patent-grade: every predicate is named, every module is path-
referenced, every test is file-referenced, the load-bearing
architectural placement decision (rotor admissibility lives in
generate/, NOT algebra/, NOT field/) is stated by name with
its rejection reasoning.
No code changes; tests untouched (1099 passed, 2 skipped baseline
from commit 36aad75 still holds).
Closes the 6-phase ADR-0024 chain with a focused comparative demo
that distinguishes CORE (inner-loop + margin + typed refusals) from
the in-system boundary-only baseline (ADR-0023 ablation).
Three conditions, all passing under contract tests:
C1. Replay determinism
baseline: 8/8 stable across 5 reruns
CORE: 8/8 stable across 5 reruns
CORE additionally folds refusal_reason into trace hash so
refusal events are replayable evidence.
C2. Traced rejection
baseline emits forbidden: 3/3 (admits=False but walk continues)
CORE corrects-or-refuses: 3/3
CORE rejection in trace: 3/3
Demonstrates that inner-loop is causally responsible for the
selection difference between baseline and CORE.
C3. Coherent refusal
baseline typed refusals: 0/3 (never raises typed refusal)
baseline emits inadmissible: 3/3
CORE typed refusals: 3/3 (all INNER_LOOP_EXHAUSTION)
Demonstrates that typed refusal with rejected_attempts evidence
is new in CORE, not present in boundary-only.
Why in-system baseline (not LLM):
A transformer-LLM comparison would be non-deterministic by
construction, could not be CI-enforced, and would be apples-to-
oranges (different corpus / training / sampling). The honest
comparison is the ablation: same codebase with the Phase 2-5
additions disabled.
Files:
evals/forward_semantic_control/phase6_demo.py
evals/forward_semantic_control/public/v2_phase6_demo/cases.jsonl (8 cases)
evals/forward_semantic_control/results/phase6_demo_report.json
tests/test_phase6_demo.py (17 passing)
docs/evals/phase6_comparative_demo.md
Tests: 1085 passed, 2 skipped (+17 from Phase 5 baseline).
This closes the ADR-0024 6-phase chain:
Phase 1 — pack-grounded fixture + architectural finding (3940290)
Phase 2 — typed refusals + trace fold (310793a)
Phase 3 — ADR-0026 ranked-with-margin (639e107)
Phase 4 — ADR-0025 rotor / frame admissibility (542e13d)
Phase 5 — stratified 5-family mechanism-isolation (b664984)
Phase 6 — comparative demo (this commit)
Authors a 20-case corpus stratified across five geometric failure-mode
families and a separate 10-case benign corpus for the
EXHAUSTION_CEILING lane:
A. near_forbidden_correct_endpoint (6 cases, gaps 0.002 to 0.55)
B. near_equal_admissible (5 cases, diffs ≤ 0.01)
C. no_admissible_path (3 cases, honest refusal)
D. multi_step_admissibility (3 chained cases)
E. heterogeneous_relation (3 chained cases, blade-switching)
phase5_runner runs each case under BOTH threshold and ADR-0026 margin
modes and reports per-family pass_rate, refusal_rate, and (for Family
A) rejection_traced_rate + boundary_overridden_rate.
Headline:
pass_rate_threshold = 1.00 (20/20)
pass_rate_margin = 1.00 (20/20)
mechanism_isolated = true (both modes, all five families)
replay determinism = byte-identical across 3 reruns
Family C refuses with RefusalReason.INNER_LOOP_EXHAUSTION in both
modes (load-bearing evidence for ADR-0024 Phase 2 typed refusals).
Family B refuses under margin mode (validates ADR-0026 δ=0.4 gate).
Benign inner-loop corpus for EXHAUSTION_CEILING ≤ 0.05 gate:
boundary_only: exhaustion 0.00, pass 1.00
null_control: exhaustion 0.00, pass 1.00
inner_loop_t0: exhaustion 0.00, pass 1.00
inner_loop_tpos: exhaustion 0.00, pass 1.00 (threshold 0.25)
Geometric finding documented while authoring the benign corpus:
23 of 85 pack tokens have negative self-cga_inner under Cl(4,1).
Tokens with self-score ≤ 0 cannot serve as single-token expected
endpoints in threshold mode — the algebra's Lorentzian signature
forbids this geometrically. Phase 5 benign corpus draws expected
endpoints from the 62-token positive-self-score subset. This is
consistent with Phase 4 characterization: no static threshold
delivers separation_quality ≥ 0.8 — the margin lane survives
because margin compares differences, not absolute scores.
Files:
evals/forward_semantic_control/public/v2_phase5/cases.jsonl
evals/forward_semantic_control/public/inner_loop_benign/cases.jsonl
evals/forward_semantic_control/phase5_runner.py
evals/forward_semantic_control/phase5_mine.py
evals/forward_semantic_control/results/phase5_report.json
evals/forward_semantic_control/results/phase5_benign_inner_loop_report.json
tests/test_phase5_corpus.py (20 passing)
docs/evals/phase5_stratified_findings.md
Tests: 1068 passed, 2 skipped (+20 from Phase 4 baseline).
Promote ADR-0025 from Draft (design note) to Accepted with the
architectural home decision reversed: rotor admissibility lives at
the same generation/propagation seam as ADR-0024's destination
check — in a sibling-but-separate module
`generate/rotor_admissibility.py` — NOT in `algebra/versor.py` or
`field/propagate.py`.
Algebra rejected because admissibility is a pack-semantic test, not
a closure invariant; placing it there couples algebra to pack state
and creates structural temptation toward grade-projection repair
(CLAUDE.md §Normalization Rules forbids). field/propagate rejected
as a forbidden normalization site even when framed as precondition
guard. The clean answer is generation-side, in its own file:
endpoint admissibility (token-side, blade) and rotor admissibility
(rotor-side, frame) compose at the same seam while remaining
conceptually separable.
New module generate/rotor_admissibility.py:
RotorVerdict — admit/reject + score + region_label + reason
check_rotor_admissibility(region, *, field_current, rotor)
-> RotorVerdict
Pure semantic check:
F' = versor_apply(V, F_current)
score = cga_inner(F', region.frame_versor)
admit iff score > 0 (basic positivity in frame half-space)
No state mutation, no closure enforcement (algebra's job).
region.frame_versor is None → trivial admit (back-compat).
RefusalReason extended:
INNER_LOOP_EXHAUSTION — destination-side (ADR-0024 / ADR-0026)
ROTOR_REJECTION — rotor-side (this ADR)
The two reasons let the trace name the axis that ran out without a
parallel exception type. InnerLoopExhaustion(ValueError) hierarchy
unchanged; back-compat preserved.
Wiring in generate/stream.py:
threshold mode per-candidate rotor check after destination admit;
reject → log rotor score, retry next candidate;
exhaustion routes reason to ROTOR_REJECTION iff
any rotor rejection occurred in the step
margin mode rotor check on the top-ranked admissible candidate;
reject → immediate InnerLoopExhaustion(
reason=ROTOR_REJECTION) carrying the destination
ranking + the rejected rotor's score
Phase 4 keeps positivity (score > 0), not margin, on the rotor side.
No cross-case calibration evidence to inform a rotor-margin constant
yet; promoting to ranked-with-margin awaits Phase 5 diversified-
families evidence. Destination-side margin (ADR-0026) is unchanged.
Teaching boundary closed at Stance A — strictly hygiene-only.
Rotor rejections are deterministic geometric outcomes, not reviewed
teaching examples. CLAUDE.md §Teaching Safety forbids parallel
correction paths; entangling rotor rejection with reviewed teaching
would create one. Confirmed in ADR-0025 §"Teaching boundary".
Acceptance evidence (tests/test_rotor_admissibility.py, 11 passing):
No-frame back-compat — frame_versor=None tokens identical to
Phase 3 baseline
Admit when aligned — frame_versor=seed direction admits
seed→destination rotor
Refuse with named axis — orthogonal frame raises
InnerLoopExhaustion(reason=ROTOR_REJECTION); threshold mode
also routes reason correctly
versor_condition < 1e-6 preserved on admitted rotors
Deterministic replay — 5 reruns identical for both admitted and
refused turns
Suite results:
full: 1048 passed, 2 skipped (+11 new rotor tests)
docs/runtime_contracts.md updated with "Rotor admissibility contract"
subsection documenting the seam, the algorithm, and the refusal
taxonomy.
Architectural invariants preserved:
no new code in algebra/versor.py, field/propagate.py, vault/store.py
no approximate recall, no cosine similarity, no HNSW/ANN
no hot-path repair; check is pure typed-verdict
InnerLoopExhaustion(ValueError) hierarchy unchanged
Replace the static-threshold admissibility gate with a ranked-with-
margin check that is scale-invariant under blade-norm variation.
Phase 4 characterization established no single global threshold
separates the v2 mechanism-isolation cases (blade norms vary ~10x);
margins between top and second-ranked candidates do, because they
scale with the blade norm and carry the relative ordering the
geometry actually delivers.
New primitives in generate/admissibility.py:
RankedCandidate — (index, word, score)
MarginVerdict — admit/reject + top + margin + full ranking
rank_candidates_by_blade — sort admissible set by cga_inner desc,
strict > tie-break by ascending vocab index
check_margin — admit top iff score>0 AND margin>=delta
Selection semantics in margin mode are blade-rank-driven: the top-
ranked admissible candidate IS the admitted destination. Differs
from threshold mode (field-driven _nearest_next then per-candidate
gate). Both modes coexist; threshold is the default and ADR-0024
acceptance evidence is preserved byte-for-byte.
Wired through:
core/config.py admissibility_mode="threshold" (default)
admissibility_margin=0.4
chat/runtime.py forwards both fields
generate/stream.py margin_mode_active branch — ranks the
candidate set once per step, admits or
raises InnerLoopExhaustion with the full
ranking in rejected_attempts
Default delta = 0.4 chosen from the v2 case margins:
V2-001: 0.596 V2-002: 0.456 V2-003: 13.27
V2-004: 3.37 V2-005: 12.74
min = 0.456 → 0.4 admits all 5 with headroom; 0.5 would refuse
V2-002. The default is falsifiable: Phase 5 may surface a case
below 0.4, which should be reported as an architectural finding
rather than patched per-case.
Acceptance evidence (tests/test_margin_admissibility.py, 13 passing):
5/5 v2 cases pass in margin mode; forbidden_token in every
case's rejected_attempts ranking
Refusal-on-insufficient-margin: delta=0.9 on V2-001 (margin
0.597) raises InnerLoopExhaustion with full ranking; no silent
boundary fallback
Threshold mode byte-identical with or without margin plumbing
5 reruns produce identical canonical trace steps
Strict > tie-break: equal scores resolve to lower-index winner
deterministically
Invariants preserved:
versor_condition < 1e-6 — rotor V is constructed only for the
admitted candidate; margin mode adds no normalization/repair site
Deterministic replay — strict > tie-break now load-bearing in
rank_candidates_by_blade alongside vocab.nearest
No approximate recall, no cosine similarity, no HNSW/ANN; pure
rank-and-difference on exact cga_inner scores
No new code in field/propagate.py, algebra/versor.py,
vault/store.py, or chat/runtime.respond()
Suite results:
full: 1037 passed, 2 skipped (+13 new margin tests)
core eval cognition: 13/13, 100% intent_accuracy,
100% versor_closure_rate
ADR-0026 documents the contract, the single-delta rationale, the
falsifiability story, and the residual risks. Margin mode is
flag-gated default-off; a future ADR may promote it to default
after Phase 5's diversified families confirm the single delta
holds (or surface the architectural finding if it doesn't).
Replace plain ValueError at both inner-loop exhaustion sites in
generate/stream.py with InnerLoopExhaustion, a typed ValueError
subclass carrying machine-readable refusal evidence:
reason : RefusalReason (INNER_LOOP_EXHAUSTION)
region_label : which AdmissibilityRegion blocked
step_index : -1 = pre-walk empty intersection;
>=0 = in-walk per-step exhaustion
rejected_attempts : ordered (idx, word, score) triples
Backward-compat by construction: subclassing ValueError preserves
every pre-Phase-2 `except ValueError` handler in chat/runtime.py,
eval lanes, and tests. No edits to chat/runtime.py, field/propagate.py,
algebra/versor.py, or vault/store.py.
Trace path wired:
- CognitiveTurnResult.refusal_reason (str, default "")
- compute_trace_hash folds refusal_reason only when non-empty
-> byte-identical hashes preserved for non-refused turns
- CognitiveTurnPipeline reads via getattr from ChatResponse and
forwards into both trace_hash and result construction
Contract documented in docs/runtime_contracts.md §"Refusal contract".
Tests (tests/test_refusal_contract.py — 10 passing):
- InnerLoopExhaustion isinstance(ValueError) at both raise sites
- In-walk site carries reason/region_label/step_index>=0/
rejected_attempts with (int,str,float) triples
- Pre-walk site uses step_index=-1 sentinel + empty
rejected_attempts
- Pre-walk fires even when inner_loop_admissibility=False
- Trace hash: empty refusal_reason preserves legacy bytes;
non-empty differs; same inputs are stable
Suite results:
smoke: 67 passed
cognition: 121 passed
runtime: 19 passed
full: 1024 passed, 2 skipped
core eval cognition: 13/13, 100% intent accuracy, 100% versor closure
Residual silent path (documented as out-of-scope for Phase 2):
chat/runtime.respond()/arespond() still convert any ValueError to
"" for their public str return contract. So a refused turn today
produces surface == "" with refusal_reason == "" — the typed
evidence is unread between the raise site and the result. The
plumbing on result + trace + pipeline is in place so a future ADR
can wire materialisation (propagate exception to
ChatResponse.refusal_reason, or catch at the pipeline seam) without
re-deriving the contract.
Phase 1 (commit 3940290) and Phase 2 (this commit) were developed
in parallel with disjoint file scope to avoid conflicts.
Rewrite v1+dev FSC cases with pack-grounded tokens drawn from
en_core_cognition_v1. Closes the 9/9 region-construction failure
recorded in Phase 4 (chain_tokens alpha/beta/gamma/delta/etc. were
ungrounded in the active pack).
Token mappings preserve each case's test pattern:
* alpha→beta→gamma→delta → tone→evidence→memory→wisdom (causes)
* mu→nu→omicron → voice→memory→wisdom (means)
* pi→rho→sigma→tau → question→answer→understanding→wisdom (precedes)
* upsilon→phi→chi → word→discourse→narrative (part_of)
* eta/theta/zeta + means-distractors → symbol/word/meaning + image/light
Result post-rewrite:
* skipped_count: 9/9 → 0/9 (region constructible)
* causal_attribution_valid: True (preserved)
* code_path_residual: 0.0 (preserved)
* inner_loop_t0 hash stability: 1.0 (preserved)
* best_separation_quality: 0.0 → 0.056 (still below 0.8 gate)
The rewrite exposes a deeper architectural finding documented in the
ADR addendum: v1/dev case schema (prime + chain_tokens) probes
teaching-driven walk (ADR-0022/0023), not the inner-loop's
blade-admissibility mechanism (ADR-0024). The Phase 2 corpus-
observation runner's reuse of v1/dev was a categorical error.
v1/dev belong to the boundary-walk lane (runner.py); v2 belongs to
the inner-loop lane (v2_runner.py). Phase 5 will author the benign
inner-loop corpus the EXHAUSTION_CEILING gate was designed against.
Tests pinning new state:
* TestV1ChainBladeUngrounded → TestV1ChainBladePostGrounding
(assertions inverted: skipped_count == 0; separation_quality < 0.5)
* TestPhase2 (unchanged) continues to assert causal_attribution_valid
and hash stability; exhaustion remains a finding, not an invariant.
Phase 2 — Corpus observation runner (inner_loop_runner.py):
- Four-condition matrix: boundary_only / null_control / inner_loop_t0 / inner_loop_tpos.
- Added `inner_loop_force_admit` to generate() — exercises the inner-loop
code path but force-breaks on first candidate. Eval-only null control:
isolates rejection as the causal factor for any pass-rate delta.
- Metrics: pass_rate, mean_rejection_count_per_turn,
non_empty_rejected_attempts_rate, exhaustion_rate (gated at 5%),
mean_admissibility_checks_per_turn, mean/p95 added_latency_ms,
trace_hash_stability across 5 reruns per case.
- Finding on v1+dev: causal_attribution_valid=True, code_path_residual=0.0,
but exhaustion_rate=0.33 at t=0 — chain outer-product blade is
geometrically blind to the active pack.
- Tests (tests/test_inner_loop_phase2.py, 5 pass): pin
causal-attribution and live-corpus trace-hash stability invariants.
Phase 3 — Mechanism-isolation v2 corpus (5 cases, v2_runner.py):
- Synthetic adversarial cases with controlled geometry — each case
specifies seed_token, admissible_tokens, relation_blade_token, and
admissibility_threshold. Field state is constructed directly from
the seed token versor, not via priming.
- For every case: boundary-only selects the forbidden decoy and
inner-loop selects the expected endpoint with the forbidden token
appearing in rejected_attempts.
- Result: mechanism_isolated=true on 5/5. boundary_decoy_rate=1.0,
rejection_traced_rate=1.0. Inner-loop rejection is demonstrably
doing causal semantic work on real packs.
- Tests (tests/test_inner_loop_phase3.py, 8 pass): GATE on
mechanism_isolated.
Phase 4 — Threshold characterization (threshold_characterization.py):
- Distribution mapping per-case AND globally on v1+dev, v2, combined.
- Per-threshold sweep over [-1.0, -0.5, 0.0, 0.1, 0.25, 0.5, 1.0].
- Finding: per-case geometry separates cleanly (correct_min > incorrect_max
on every v2 case), BUT no global static threshold passes the
separation_quality >= 0.8 gate. Blade norms vary ~10x across cases.
- Static thresholds (global, relation-typed, or constant frame-derived)
are geometrically insufficient. Per-case-normalized thresholds
(e.g. fraction of blade self-score) are the recommended next step.
- v1 chain-token outer-product cases all skipped — the corpus's chain
tokens (alpha, beta, gamma, delta) are not grounded in the active
pack. Load-bearing finding for ADR-0025 region construction.
- Tests (tests/test_inner_loop_phase4.py, 5 pass): pin the finding
diagnostically (not gated).
Phase 5 — ADR-0025 design note (draft):
- No code changes proposed. Scopes three architectural questions:
(1) home (algebra/versor.py vs field/propagate.py vs generate/) —
preliminary stance: algebra/versor.py.
(2) threshold scheme (blade-normalized fraction recommended over
static; learned/adaptive rejected for determinism).
(3) teaching-loop boundary — Stance A confirmed: rejections are
runtime hygiene only, no entanglement with teaching/*.
- Decisions to be closed before Draft → Accepted.
Phase 1 acceptance criteria from previous commit (7fccf36) carry
forward: wired, deterministic-when-wired, legacy hash preserved.
Suite: 1014 passed, 0 failed, 2 skipped.
Phase 1 of the post-ADR-0024 sequence: wire the inner-loop flag into live
cognition paths and prove deterministic-when-wired in the same milestone.
Changes:
- RuntimeConfig: add inner_loop_admissibility + admissibility_threshold.
- ChatRuntime: pass both into generate() on the chat hot path.
- CLI: --inner-loop-admissibility / --admissibility-threshold flags.
- vocab/manifold.py: document strict `>` tie-break as load-bearing for
ADR-0024 rejected_attempts ordering (determinism by construction, not
by accident).
- tests/test_inner_loop_admissibility.py: three new determinism tests —
identical rejected_attempts across 5 runs, identical trace hash across
5 runs (non-empty), and legacy hash equivalence when no rejections
occur (flag on/off byte-identical).
- tests/test_language_pack_cache.py: fix stale fixture (en-core-cog-070
-> en-core-cog-085 after pack growth).
Suite: 995 passed, 0 failed, 2 skipped.
Acceptance criteria met:
- wired through RuntimeConfig + CLI + ChatRuntime + generate()
- deterministic rejected_attempts sequence (verified by repetition)
- deterministic trace hash under inner_loop=True
- legacy ADR-0023 trace hashes preserved when no rejections
- nearest_next determinism is by construction (sequenced iteration +
strict > tie-break), now documented
Next: Phase 2 — corpus-observation eval on existing v1 corpus with the
four-condition matrix (boundary-only, null control, inner-loop t=0.0,
inner-loop t>0) and exhaustion_rate + latency metrics.
Flag-gated semantic change to generate(): when
inner_loop_admissibility=True and a non-unconstrained region is
supplied, each per-step selection is re-evaluated by check_transition
with admissibility_threshold; rejected candidates are excluded and
the walk re-selects until admitted or every admissible candidate is
exhausted (ValueError = honest refusal, same shape as ADR-0022 §2).
Default False — every legacy call site keeps ADR-0023 boundary-only
semantics, and the new AdmissibilityTraceStep.rejected_attempts field
is folded into canonical() only when non-empty, so trace_hash bytes
are byte-identical with ADR-0023 turns.
Invariants preserved: rotor V is only built for the admitted
candidate, so versor_condition < 1e-6 still holds at propagate_step;
no new normalization site; no new I/O / dynamic imports.
Tests: tests/test_inner_loop_admissibility.py covers the four
acceptance properties — default off preserves behavior, rejection
drives re-selection, exhaustion raises ValueError, empty
rejected_attempts is omitted from canonical(). Full pytest: 927
passed, 1 pre-existing unrelated failure (test_language_pack_cache).
Extends ADR-0022 with inspection/telemetry surfaces that turn the
forward-semantic-control claim from "mechanism exists" into "mechanism
is causally load-bearing, isolated, and replayable."
Changes (zero runtime semantics change beyond a pipeline bug fix):
- AdmissibilityTraceStep + GenerationResult.admissibility_trace —
per-transition record of region label, candidates before/after,
selected destination, and the typed AdmissibilityVerdict.
- ChatResponse + CognitiveTurnResult expose admissibility_trace,
admissibility_trace_hash, ratification_outcome,
region_was_unconstrained.
- hash_admissibility_trace + compute_trace_hash fold the new fields
only when they carry non-default values, so pre-ADR-0023 turn
hashes remain byte-preserved.
- Same-path ablation leg in evals/forward_semantic_control/runner.py:
generate(..., region=None) vs generate(..., region=R) on the same
runtime/vocab/field/persona/prompt — isolates the region as cause.
- Lane expansion: 8 dev cases across 4 relation axes (cause, means,
precedes, part_of) including 2 adversarial distractor cases.
- Lane metrics now report region_only_constrained_rate /
region_only_gap / ratified_rate / demoted_rate / passthrough_rate /
passthrough_on_scored.
- Bug fix surfaced by the new accounting: _ratify_intent looked up
runtime.vocab (always None) instead of runtime.session.vocab —
every production turn was silently PASSTHROUGH. Fixed; ratifier
now actually gates intent classification.
- tests/test_admissibility_trace.py: hash determinism +
pre-ADR-0023 byte-preservation tests.
Lane evidence (dev, 8 cases):
- constrained_pass_rate=0.80, causality_gap=0.80
- region_only_gap=1.00 (5/5 with region, 0/5 without — same path)
- ratified_rate=1.00, passthrough_on_scored=false
- overall_pass=true
Bench: 9.41s / 20 turns (~470ms/turn), well inside the +5% budget.
Full pytest: 922 passed, 1 pre-existing failure
(test_language_pack_cache, unrelated to ADR-0023).
Resolves all 5 TBDs and closes all 8 acceptance gates for ADR-0022.
TBD-1 (intent oracle): regex seed + field ratification —
generate/intent_ratifier.py. RATIFIED / DEMOTED / PASSTHROUGH
outcomes; DEMOTED routes through honest refusal.
TBD-2 (region intersection algebra): generate/admissibility.py.
Token-set composition via sorted set intersection; blade composition
via outer product with zero-blade as neutral element; rotor
composition via sandwich conjugation routed through
algebra.backend.versor_apply (Rust parity preserved by construction).
Empty intersections preserved — no silent relaxation.
Wiring: propose() and generate() accept an AdmissibilityRegion
(default None preserves legacy behavior); pipeline ratifies intent
at step 1b.i before graph construction.
Eval lane: evals/forward_semantic_control/ — both legs run against
CognitiveTurnPipeline (constrained) vs bare ChatRuntime.chat()
(unconstrained baseline). Dev (3 cases) and public/v1 (1 case) both
report overall_pass=true, causality_gap=1.0, coincidence_rate=0.0.
Chain-endpoint probe surfaces 'delta' only under forward semantic
control.
Bench cost (30 turns): -2.8% wall-clock (within +5% budget the ADR
set for the ratification gate on every turn). 138x cheaper than
Sonnet 4.5; main was 142x.
Tests: 33 new (25 admissibility + 8 ratifier). Full suite 912/913
pass — the single failure is pre-existing pack-size drift on main,
unrelated.
Skeleton (Status: Draft) for the bridge between graph and field.
Today the proposition graph and the field walk run in parallel —
the graph does not constrain field propagation, and the field
does not prove the graph. This ADR commits to making semantic
structure causally active inside propagation: graph computes an
admissibility region, propagation must satisfy it or fail
honestly, no template authoring, no sampling, no symbolic
planner regression.
Explicitly marked Draft because 5 TBD items must close before
promotion to Proposed:
TBD-1 intent oracle (regex classifier as load-bearing oracle
recreates the gap one level up)
TBD-2 region intersection algebra (frame × relation × identity
composition — closed rotor operator not yet proven)
TBD-3 backward-compat window removal mechanism
TBD-4 identity manifold as constraint source (v1 scope decision)
TBD-5 pack semantic depth — cognition pack may need targeted
extensions before lane can pass
Acceptance criteria for Draft → Proposed → Accepted explicit.
Trust-boundary review (CLAUDE.md §Security) included.
Implementation sequencing sketched (7 steps, each its own PR).
Motivated by 2026-05-17 external assessment. The assessment is
input to the ADR, not authority over it.
Verified: no code changes; ADR is documentation only.
benchmarks/cost.py measures CORE per-turn cost honestly:
Measured (no estimation):
- turns, wall_seconds_total, cpu_seconds_total
- latency stats: min / median / p95 / max in ms
- throughput in turns per second
Derived with disclosed assumptions:
- USD per 1000 turns at AWS t3.medium on-demand
($0.0416/hr, source cited in CloudReference.source_note)
- Frontier pricing comparison: Anthropic Claude Sonnet 4.5 /
Haiku 4.5 and OpenAI GPT-4o, public per-token rates with
source notes, derived using a conservative 20-in / 40-out
tokens-per-turn assumption.
Explicitly NOT reported:
- Joules per turn. Honest energy measurement requires RAPL
(Linux) or IOKit/powermetrics (macOS) with privileged access
that a plain Python process cannot get. Reporting a fabricated
figure from a hand-waved TDP would violate "speculation is not
evidence." cpu_seconds_total is the available proxy.
CLI:
core bench --suite cost --runs 100
Measured numbers (100 turns, "What is truth?", warmup 5):
median latency: 444.88 ms
p95 latency: 447.10 ms
throughput: 2.61 turns/s
$/1000 turns: $0.0044
vs frontier: 48–149× cheaper depending on provider
CLAIMS.md Tier 4 cost/latency rows updated with real numbers
replacing TBDs. evals/reports/cost_latest.json committed as the
captured baseline.
Verified: smoke (67), bench --suite cost CLI works.
contradiction_detection: 0.50 → 1.00 contradiction_flag_rate,
1.00 → 0.00 false_flag_rate. Lane graduates overall.
TeachingStore.add now runs a coherence checker on every new proposal.
Two detection paths, both require subject token overlap:
Typed path — both new and prior parse to triples with the same
relation. Tails must differ in negation/opposition polarity AND
share ≥1 content token. Catches (truth, is, coherence) ↔
(truth, is, not coherence).
Text fallback — at least one side failed to parse a triple (e.g.
relation predicate "depends" not in the cognition pack lexicon
yet). Raw correction texts must differ in polarity AND share ≥2
non-discourse content tokens. ≥2 threshold prevents
single-shared-subject false positives on unrelated corrections.
Catches "meaning depends on use" vs "meaning is independent of use".
On detection, BOTH proposals (new and conflicting prior) transition
to EpistemicStatus.CONTESTED. ADR-0021: CONTESTED is not admissible
as evidence until a coherence judgment ratifies one direction or
falsifies the other.
Runner side: v1 versor-spike heuristic retired. The new CONTESTED
signal is the only one that drives `flagged`. versor_delta retained
in the record for telemetry.
CLAIMS.md Tier 4.5 contradiction rows CLOSED — completes the
truth-seeking schema arc. All red Tier 4.5 rows from the audit are
now green. docs/truth_seeking_schema.md §"Contradiction detection
is not implemented" closed.
Verified: smoke (67), teaching (17), cognition (121), runtime (19),
architectural invariants (40) — all green.
Two Tier 4.5 lanes graduate to passing:
refusal_calibration: 0.00 → 1.00 refusal_rate, 0.00 fabrication,
1.00 in_grounding_answer_rate.
- chat/runtime.py: _UNKNOWN_DOMAIN_SURFACE reworded to "I don't know
— insufficient grounding for that yet." (matches lane refusal
markers; was equivalent in spirit but unrecognizable).
- evals/refusal_calibration/runner.py: per-case `prime` field replays
brief priming turns before the probe. Necessary because ChatRuntime
cold-starts with an empty vault; "in-grounding" only counts as
grounded if the session has actually been told something relevant.
Previous 1.00 in_grounding rate was a false positive (gate was
firing on these too, but the surface text didn't match markers).
articulation_of_status: 0.00 → 1.00 speculative_articulation, 0.60
→ 0.00 false_certainty.
- core/cognition/pipeline.py: CognitiveTurnPipeline tracks subjects
of prior SPECULATIVE teaching proposals (parsed-triple subject
plus ≥4-char tokenized split, so prefixed parses like
"correction: wisdom" still match "What is wisdom?"). On a later
turn that references one of those subjects, or that carries a
reflexive query shape ("is your answer confirmed?", "has this
been reviewed?"), prepends "(speculative, not yet reviewed)" to
the surface. Teach turn itself does not self-mark; only
subsequent probes do.
Lane contracts updated to reflect graduation. CLAIMS.md Tier 4.5
rows for both lanes now CLOSED. docs/truth_seeking_schema.md
§Realizer-side surface gaps closed and rewritten.
Verified: smoke (67), cognition (121), runtime (19), teaching (17),
architectural invariants (40) — all green.
Categorizes every production vault.recall() callsite as RECOGNITION,
EVIDENCE_TELEMETRY, or EVIDENCE_USER_FACING. Adds INV-24 architectural
invariant (TestINV24VaultRecallRegistry, 3 tests) that forces any new
callsite to declare its role and requires EVIDENCE_USER_FACING sites to
pass min_status=COHERENT.
Audit findings:
- chat/runtime.py:330 → RECOGNITION (gate decision input)
- vault/decompose.py:121 → RECOGNITION (grade-decomposed gate fallback)
- generate/stream.py:147 → EVIDENCE_TELEMETRY (walk_surface per runtime contract)
- No EVIDENCE_USER_FACING sites exist today — user-facing surface comes from
pack-grounded realize(proposition, vocab), not vault.recall.
Why this closes Leak C: the write-side fix already stamps SPECULATIVE on
self-stored propositions; the read-side audit confirms no inference path
treats them as ratified evidence. If a future change routes the
generation walk into the user-facing surface, INV-24 forces the
recategorization to be explicit.
CLAIMS.md Tier 4.5 Leak C row now CLOSED. docs/truth_seeking_schema.md
§Leak C updated with full audit categorization.
Verified: smoke (67), cognition (121), runtime (19), all architectural
invariants (40) — green.
Audit of the one-mutation-path invariant (ADR-0021 §3) found three leaks
where pack authority or session-state writes could substitute for coherence
judgment. All three landed fixes or partial closures in this push.
Leaks closed:
- Leak A: pack vocab defaulted to COHERENT — flipped to SPECULATIVE in
language_packs/{compiler,schema}.py; docstring corrected to align with
ADR-0021 (it was rationalizing the leak).
- Leak B: vault.recall was epistemic-blind — VaultStore.store() now stamps
every entry with EpistemicStatus (default SPECULATIVE); recall(min_status=)
filters to admissible-as-evidence tier. All 4 vault-write sites updated.
- Leak C (write-side): generate/proposition.py:198 stored articulated
propositions unmarked — now stamps SPECULATIVE, breaking the
fabrication-feedback loop in principle. Read-side audit of 5 call sites
is the residual.
New architectural invariants (tests/test_architectural_invariants.py):
- INV-21: one-mutation-path allowlist (caught Leak C on first run)
- INV-22: pack lexicon default is SPECULATIVE (Leak A guard)
- INV-23: vault recall epistemic-aware (Leak B guard)
New eval lanes:
- teaching_injection_resistance — ships GREEN at 1.00/1.00/0 (the
structural anti-injection claim is real and measurable)
- refusal_calibration — honest gap: 0% refusal, 0% fabrication
- contradiction_detection — honest gap: 50% flag via versor-delta heuristic,
100% false-positive; motivates the proper coherence-checker
- articulation_of_status — honest gap: 0% speculative articulation, 60%
false certainty; output-side leak surface
New benchmarks:
- benchmarks/footprint.py — total deployed runtime is 7.06 MiB
(109,358x smaller than Llama 3.1 405B, runs offline, no GPU)
- benchmarks/learning_curve.py — monotonic + replay-deterministic curve
per lane
Documentation:
- docs/truth_seeking_schema.md — foundational architectural commitment,
five rules, mapped to human failure modes, leaks published openly
- evals/CLAIMS.md — five-tier public claims doc; Tier 4.5 publishes
known gaps with named fixes; verification contract at top
- README.md — new pillar between algebraic substrate and language pillar
Includes in-flight formation pipeline scaffolding (formation/, tests/formation/,
docs/formation_pipeline_plan.md) and minor CLI/contracts/gitignore edits
that were already in the working tree at session start.
Verification: 798 passed, 2 skipped, 1 deselected (pre-existing pack-count
test drift unrelated to schema changes).
Closes the user-flagged scope gap: every previous fluency lane (Phase
5.1 + 5.4-5.7 + grammatical_coverage) operates on 3-word SVO probes.
These three pieces stress paragraph-scale generation, give per-stage
latency visibility, and expose the realizer's word-choice geometry —
all on top of the existing deterministic infrastructure.
# discourse_paragraph lane (paragraph-scale fluency)
Forces the realizer to emit multi-sentence paragraphs from a
multi-step ArticulationTarget with rhetorical moves (ASSERT, SEQUENCE,
ELABORATE, CONTRAST). Same realizer, much richer input — every case
is 3-5 sentences with deterministic discourse markers.
Public 12 cases / holdouts 5 / dev 1 across 12 + 5 topic chains
(epistemic, scientific method, creation arc, logical dependency,
ethical grounding, linguistic layers, mathematical chain, narrative,
biology, physics, two contrast-shaped, musical, social, computational,
psychological, economic).
Sub-metrics per case:
- sentence count (within min..max window)
- subject coverage rate
- discourse marker presence (next / furthermore / in contrast)
- sentence-initial capitalization
- replay determinism (run twice, surfaces match)
Result: 12/12 public + 5/5 holdouts at 100%, replay rate 100%, mean
sentence count 4.
# Realizer capitalization (G4, addresses user-flagged concern)
generate/realizer.py gains `_capitalize_sentence` + `_join_as_paragraph`
helpers. Sentence-initial alphabetic characters are now uppercased
(skipping leading whitespace/punctuation). Surfaces went from
"wisdom grounds knowledge. next, knowledge requires evidence."
to
"Wisdom grounds knowledge. Next, knowledge requires evidence."
The discourse_paragraph runner ships a strict per-sentence
capitalization check so future regressions get caught.
# Pipeline-stage profiler (benchmarks/pipeline_profiler.py)
External monkey-patch wrapper around CognitiveTurnPipeline.run() that
records per-stage ns budgets without editing any pipeline source.
Stages: intent, graph_planner, realize_semantic, runtime_chat,
maybe_transitive_walk, fold_walk_into_surface, run_teaching,
trace_hash.
API: `profile_turn(pipeline, text) -> ProfileReport` with
`.stages: dict`, `.total_ns: int`, `.as_dict()`.
Empirical: runtime_chat dominates >99% on the runtime hot path (which
is correct — that's where ingest + propagate + recall + articulate
all happen). Future optimisation work has a clear per-stage signal.
# Word-selection tracer (benchmarks/word_selection_tracer.py)
External wrapper around generate.articulation._resolve_slot that
records every nearest-neighbor lookup as a WordSelectionStep:
- slot (subject/predicate/object)
- input versor (32-d copy)
- top-K candidate words by CGA inner product
- chosen word + morphology
- output language
Top-K scoring uses the diagonal Cl(4,1) metric kernel from
algebra.backend (same vectorised path vault_recall uses), not a
per-word Python loop over cga_inner. No approximation, exact
deterministic ranking, bit-identical to a scalar scan.
API: `trace_realization(pipeline, text) -> RealizationTrace` with
`.steps`, `.realization_steps`, `.surface`, `.as_dict()`.
# CLI lane registration
Cognition suite now sweeps the benchmark profiler/tracer tests
(test_benchmarks_profiler.py) so any future regression in the
instrumentation surfaces immediately.
# Constraints honoured
- Zero edits to core/, chat/, vault/, teaching/, language_packs/, or
the algebra hot path. All instrumentation is external monkey-patch
with originals restored in finally.
- discourse_paragraph runner bypasses ChatRuntime grounding (named v2
gap) so paragraph capability is isolated to the realizer.
- No semantic changes; no hidden normalisation; no approximate
recall.
# Lane health
smoke 55, runtime 19, teaching 17, packs 6, cognition 105 (was 103),
algebra 132. All Phase 5 fluency lanes still 100% with the
capitalised surfaces (rubric is case-insensitive). discourse_paragraph
100%.
# What ships next (named v2)
- Round-trip: discourse_paragraph through ChatRuntime end-to-end,
not just realize_target.
- Per-sentence grammatical_coverage rubric on each emitted sentence.
- Longer chains (10/20/50 sentences) with per-sentence determinism
scaling curves.
- compose_relations operator to lift compositionality recall from
68.8% toward 100%.
Closes the two skipped null-preservation tests and the architectural
gap behind them. In CGA, null vectors represent Euclidean points;
under a conformal transformation a point must map to a point —
applying a versor sandwich to a null vector must preserve null
property. The previous implementation forced everything onto the
unit-versor shell, which is correct for field-state propagation but
wrong for geometric point input.
Implementation
- algebra/versor.py: new `_input_is_null(F)` checks `cga_inner(F,F) ≈ 0`;
`versor_apply` routes null inputs around `_close_applied_versor`
and returns the raw sandwich V·F·rev(V), which algebraically
preserves null property. Non-null inputs unchanged.
- core-rs/src/versor.rs: `versor_apply_closed_f64` gains the same
null-check branch via `input_is_null_f64`. ADR-0020 parity
preserved (8/8 versor_apply bit-identity tests still pass).
Test changes
- tests/test_architectural_invariants.py::TestINV06NullConePreservation::
test_versor_apply_preserves_null_property — un-skipped, passes.
- tests/test_rust_backend.py::test_rust_versor_apply_preserves_null_vectors
— un-skipped, passes.
- tests/test_versor_closure.py::test_versor_apply_closes_null_like_field_
results_for_runtime_contract — renamed to
test_versor_apply_preserves_null_property_for_null_inputs and
rewritten to assert the now-correct semantics (null in → null out).
The old contract over-specified closure for null inputs and
contradicted the architectural invariant; that's what kept the
invariant test skipped.
Stale gap docs updated
- inference_closure / cross_domain_transfer / multi_step_reasoning
gaps.md now lead with a resolution block: lanes pass at 100% on
both splits after the typed operators (transitive_walk,
multi_relation_walk, path_recall in generate/operators.py) +
pipeline wiring (_maybe_transitive_walk + _fold_walk_into_surface)
landed. The historic findings are preserved below for traceability.
- compositionality gaps.md: partial resolution — recall up from
6.25% to 68.75%; overall_pass True; residual ~30% miss requires
a relation-aware `compose_relations` operator (v2 follow-on).
Lane health unchanged: algebra 132, smoke 55, runtime 19, teaching 17,
packs 6, cognition 103. Cognition eval 100%. Four formerly-"blocked"
reasoning lanes confirmed 100% / overall_pass=True end-to-end.
Six contained-tier gap closures from the cross-phase gap audit.
Every gaps.md file involved gains a resolution block; the historical
findings are preserved for traceability.
Identity (teaching/review.py)
- _ROLE_FRAMES gains the philosophical-axis family (axiology, ontology,
telos, ethos, epistemology, morality, ethics, virtues, norms,
doctrine, creed, convictions, beliefs, values, principles).
- _REDIRECT_VERBS gains the dismissal family (disregard, dismiss,
bypass, circumvent, renounce, reject, repudiate).
- Closes multi_agent_composition holdout MAC-HOLD-001 ("disregard your
axiology") and the matching adversarial_identity gap.
- Multi-agent holdouts: 8/8 attacks rejected, 3/3 legits accepted.
Pipeline (core/cognition/pipeline.py + docs/runtime_contracts.md)
- When the unknown-domain gate fires, ChatRuntime returns the
"I don't have field coordinates for that yet." stub and
vault_hits == 0. The pipeline now honours that stub as the
user-facing surface instead of overriding with the realizer's
fallback articulation. walk_surface is unchanged either way.
- New contract test
tests/test_semantic_realizer_integration.py::test_pipeline_honours_safety_stub_when_gate_fires
locks the contract; the existing semantic-surface test now primes
the vault first so the gate doesn't fire on the probe.
- Closes calibration gaps.md Finding 2.
Realizer morphology (generate/morphology.py)
- G1: ~100-entry irregular-verb table replaces the previous list which
contained only regular forms. Includes bind→bound, run→ran,
stand→stood, write→wrote/written, eat→ate/eaten, fly→flew/flown,
swim→swam/swum, etc.
- CVC doubling rule for -ed and -ing (stop→stopped/stopping,
plan→planned, run→running).
- Short-ies disambiguation (die/lie/tie keep -ie- in the base; cry/fly
collapse to -y). Lie is also irregular (lay/lain) — uses
_IRREGULAR_FORMS first.
- 28-case regression test (tests/test_morphology_irregular.py).
Realizer plural agreement (generate/templates.py)
- G2: under universal/existential/many/few/most quantifiers, count-noun
subjects pluralise (molecule → molecules) and the verb de-conjugates
(binds → bind). Negation toggles does-not → do-not. Aspect toggles
has → have, is → are. All other constructions unchanged.
- Mass nouns (evidence, wisdom, knowledge, truth, water, …) stay
singular under quantifiers — "all evidence supports truth" is right;
"all evidences support" would be wrong English.
- 17-case regression test
(tests/test_realizer_quantifier_agreement.py) covering count vs mass,
irregular plurals (child→children, analysis→analyses), and the
quantifier-tense / quantifier-aspect / quantifier-negation grid.
Rubric punctuation tolerance (evals/grammatical_coverage/runner.py)
- G3: _check_word_order strips trailing/leading punctuation
(.,;:!?—–) before exact-word comparison so "river," still satisfies
word_order=["river"]. must_contain also accepts punctuation-
stripped token matches.
- Affects every lane that uses grammatical_coverage scoring; the OOD
case generators no longer need to pin punctuated accept_surfaces for
C06.
Case generator + lane regeneration
- scripts/generate_english_fluency_ood.py uses generate.templates.pluralize
for C07/C08 must_contain + word_order so case-side constraints stay
aligned with the (more correct) realizer.
- All Phase 5 OOD lane cases (5.1, 5.4–5.7) regenerated; results files
re-scored.
CLI (core/cli.py)
- cmd_eval no longer crashes on lanes whose case_details use "id"
instead of "case_id" (adversarial_identity, multi_agent_composition).
- Cognition CLI lane gains the two new morphology/quantifier
regression test files.
Lane sweep (all 100%, no regression):
english_fluency_ood 117/117 public + 39/39 holdouts
elementary_mathematics_ood 117/117 + 39/39
foundational_physics_ood 117/117 + 39/39
foundational_biology_ood 117/117 + 39/39
classical_literature_ood 117/117 + 39/39
grammatical_coverage back to 100% on its own seed cases
hebrew_fluency / koine_greek_fluency 3/3 each
CLI lane health:
smoke 54, runtime 19, teaching 17, packs 6, cognition 103 (was 57),
algebra 132.
ADR-0020 next-level: close the parity-gate hole on the four remaining
ungated Rust surfaces.
Gates landed (subprocess-based, raw f32/f64 byte equality):
cga_inner — 14/14 bit-identical (random + basis blades + self-norm)
geometric_product — 15/15 bit-identical (random + basis blades + scalar identity)
versor_condition — 9/9 bit-identical AFTER kernel fix
versor_apply — 8/8 intentionally skipped (see below)
Kernel fix: versor_condition_raw
The Python source-of-truth (algebra.versor.versor_unit_residual) folds
the geometric product + identity subtraction + Frobenius norm in f64.
The Rust kernel was folding in f32, drifting by 1 ULP on out-of-shell
inputs. Rewrote versor_condition_raw to promote inputs to f64, use the
existing geometric_product_f64/reverse_f64 building blocks, and cast
only the final scalar back to f32. Python is canonical per CLAUDE.md
sequencing rule 5.
Honest disable: versor_apply
The Rust versor_apply_closed diverges structurally:
(1) precision — f32 sandwich vs Python's f64 throughout
(2) closure form — Rust has a null-vector early branch + no
post-unitize condition recheck; Python is the
inverse (no null branch; recheck + seed-rotor
fallback)
Per ADR-0020 "default-off until parity passes", the Rust dispatch for
versor_apply is disabled in algebra/backend.py with a pointer to the
gate. The parity tests are skipped with explicit reason. The follow-up
f64 port is documented in the ADR's new Parity status table.
Lane registration: all four parity files added to --suite algebra.
After: algebra 124 passed, 8 skipped (was 86). All other lanes green:
smoke 54, runtime 19, cognition 57, teaching 17, packs 6. Cognition
eval 100%.
Registers tests/test_epistemic_invariants.py in the teaching CLI lane so
`core test --suite teaching` sweeps the ADR-0021 non-hardening
invariant checks alongside the reviewed-teaching loop and pipeline
integration tests. Lane: 17/17.
ADR-0021 v1 schema land. epistemic_status is a position in the revision
graph, not a source-trust tier — coherence is the only admission signal.
Surfaces:
- teaching/epistemic.py: EpistemicStatus enum (COHERENT, CONTESTED,
SPECULATIVE, FALSIFIED); ADMISSIBLE_AS_EVIDENCE = {COHERENT}.
- PackMutationProposal.epistemic_status (default SPECULATIVE) + immutable
with_status() updater.
- ReviewedTeachingExample.epistemic_status (default SPECULATIVE);
orthogonal to acceptance per ADR §Schema impact.
- LexicalEntry.epistemic_status (default "coherent" for seed; absent in
JSONL is treated as the seed default — no retroactive tagging).
- compute_trace_hash + trace_hash_from_result + pipeline.py fold the
load-bearing proposal's epistemic_status into the trace hash so
replay detects different epistemic frames.
Non-hardening invariant (ADR-0021 §2): tests/test_epistemic_invariants.py
asserts no final/frozen/axiom/permanent flag on PackMutationProposal or
ReviewedTeachingExample, and EpistemicStatus contains no source-trust
tier names.
Docs: docs/runtime_contracts.md gains an Epistemic surface section.
Lanes green: smoke 27/27, teaching 10/10, packs 6/6, runtime 19/19,
cognition eval 100%.
ADR-0020 follow-on (task #35). Two-pronged fix:
1. Kernel: ported ADR-0019 Stage 1 diagonal-metric kernel to
core-rs/src/vault.rs. Per-versor scoring is now 32 multiplies
+ 32 adds via the precomputed Cl(4,1) metric, not the
1024-op full geometric_product the prior path computed.
Bit-identity preserved by serial fold order matching Python.
2. Zero-copy marshalling: replaced Vec<&PyAny> + extract-per-
versor with PyReadonlyArray2<f32> via the numpy Rust crate.
The Rust binding now reads a slice view directly into the
numpy buffer — no Python→Rust copy, no Vec<[f32;32]>
re-chunk. Python caller passes the (N, 32) ndarray as-is
(ascontiguousarray ensures C-contiguous f32).
Result:
N python rust speedup
1k 0.20ms 0.26ms 0.77x (Python wins on fixed overhead)
10k 1.62ms 1.45ms 1.12x
100k 19.22ms 12.93ms 1.49x
1M 251.50ms 131.36ms 1.91x
Parity bit-identical (raw f32 bytes) at every scale across the
parameterised test in tests/test_vault_recall_rust_parity.py.
Both ADR-0020 first-surface gates now pass: parity AND
performance at the scales where Rust is meant to win. Python
remains the default per CLAUDE.md sequencing rule 5;
CORE_BACKEND=rust is now a legitimate opt-in acceleration.
Smoke 27/27, algebra 70/70, runtime 19/19 all green.
MLX was declared as a darwin-only dependency but no source code
imports it — runtime is NumPy (Python path) + Rust/Rayon
(CORE_BACKEND=rust). Per ADR-0020, third-backend work
(GPU / JAX / MLX) is explicitly deferred until both Python and
Rust paths are mature.
Smoke suite (27/27) confirms no path depends on MLX. Re-add if
and when a deferred Apple-Silicon GPU backend is opened.
ADR-0020 first per-surface Rust parity port. Parity test runs
the same fixture under CORE_BACKEND=python (default) and
CORE_BACKEND=rust in subprocesses and asserts:
- per-versor scores are float32 bit-identical (raw bytes hex)
- top-k ordering matches, including ascending-index tie-break
Tested at N=50/137/200/500 versors across four seeds. All four
parameterisations pass with 0 ULP delta.
Why parity holds with no Rust code change: the Cl(4,1) CGA inner
product is structurally diagonal with ±1 metric. The full
geometric-product Rust path (core-rs/src/cga.rs::cga_inner_raw)
accumulates off-diagonal contributions to scalar[0] in pairs that
cancel to bit-exact zero in float32, leaving the same serial
sum_i metric[i]*X[i]*Y[i] that the Python vectorised path
computes. Same kernel, two implementations.
Parity gate: PASS. Performance gate: NOT YET. At N=100k the Rust
path is ~13x slower than Python (266ms vs 20ms) due to per-
versor numpy marshalling in the Rust binding (100k Python→Rust
round trips). Default-off posture is correct until the
marshalling is fixed (next per-surface follow-on).
Establishes a typed epistemic surface for stored claims without
importing any source-trust bias. The status enum (COHERENT,
CONTESTED, SPECULATIVE, FALSIFIED) describes a claim's position
in the revision graph, not the credentials of its source. No
tier in the schema carries inherent trust weight.
Three commitments:
1. epistemic_status is a position in the revision graph, not a
trust tier. Source labels (peer_consensus / outsider /
established / unauthoritative) are explicitly excluded.
2. Non-hardening invariant: no reviewed claim, relation, or
edge ever becomes unrevisable. No final/frozen/axiom flag
may be added. Stage-3 inversion (versor-conjugate
correction) is always available.
3. Coherence is the only admission signal. v1 is curator-
mediated but bias-free at the schema level; v2 must add a
structural coherence metric so the tag has geometric teeth
and not just curator authority.
Schema impact: PackMutationProposal.epistemic_status,
review outcome carries status alongside ACCEPTED/REJECTED_IDENTITY,
lexicon entries get an optional epistemic_status field,
trace_hash folds in epistemic_status for replay verification.
Named v2 gap: structural coherence metric recipe (cga_inner
agreement with the existing reviewed field) is committed as
the path forward.
Implementation lands as a Phase 5 parallel-track item alongside
Rust parity per ADR-0020.
First Phase 5 lane. Tests whether the deterministic realizer
produces grammatical English across all 13 C01-C13 constructions
when the (subject, predicate, object) vocabulary is outside the
en_core_cognition_v1 seed pack. Four OOD domains: nature, tech,
domestic (public), chemistry (holdouts).
Public 117/117 (100%) and holdouts 39/39 (100%) — every
construction passes on every domain. Realizer fluency is
mechanistic and pack-independent; the Phase 5 capability story
rests on a sound structural bet.
Known v1 gaps (designed around to isolate the structural
claim): G1 irregular past tense (realizer applies -ed
unconditionally), G2 plural agreement under quantifiers (no
pluralisation of subjects under "all"/"some"), G3 rubric-side
punctuation strictness in shared _check_word_order. All three
are documented in gaps.md with bounded follow-on lanes.
Scoring is delegated to evals.grammatical_coverage.runner so the
rubric stays consistent. Cases generated by
scripts/generate_english_fluency_ood.py for reproducibility.
ADR-0020 moves to Accepted. Phase 5 — Curriculum Era opens
2026-05-16 on the Python runtime. Rust backend parity port
runs as a parallel track, per-surface bit-identity gated,
default-off until each surface's parity test passes on main.
First Phase 5 lane: 5.1 English fluency v5 OOD.
First Rust parity port: vault_recall.