Verdict: PARTIAL.
Updates the substrate liveness registry with the L1 field substrate audit, including ADR enumeration, module/caller mapping, suite lane evidence, cross-layer contract findings, and downstream notes.
Cleanup: none; no unambiguously dead L1 code was found. Test-only helpers and pulse-only field operators are documented for review instead of deleted.
Verification: python3 -m core.cli test --suite smoke -q; python3 -m core.cli test --suite algebra -q; python3 -m core.cli test --suite pulse -q; python3 scripts/verify_lane_shas.py.
Names the missing prerequisite that recognizer-storage v2 and
substrate-liveness-audit v2 both flagged: the process shape in which
the engine accumulates capability over its lifetime, survives reboot
as recovery, and presents a narrow async HITL ratification entrypoint.
Cross-reference discipline applied up-front (per
feedback-adr-cross-reference-discipline memory entry — fourth
iteration; this time grep BEFORE draft). Existing ADRs identified
as load-bearing: ADR-0040 (telemetry sink, persistent audit trail),
ADR-0041/0042 (operator surface + audit-tour), ADR-0055 (four-tier
memory: T1 session vault → T4 ratified packs; explicitly names
"what survives across all sessions and reboots"), ADR-0056/0080
(contemplation loop), ADR-0057 (proposal review machinery this
scope must build on), ADR-0014 (vault promotion gate, currently
dormant — L2 audit will verify), ADR-0027/0029/0033 (identity/
safety/ethics packs, currently startup-loaded).
Current state honestly mapped: every entry is a one-shot CLI
command via argparse in core/cli.py; ChatRuntime is per-invocation;
no long-lived process exists.
Four sub-questions framed:
1. Process shape — long-lived daemon vs. hybrid (state externalized
+ restored) vs. one-shot CLI with audit-trail-as-lifetime.
2. State partitioning — session-state (ephemeral) / engine-state
(live, persistent across reboot) / substrate-state (cold,
persistent).
3. Reboot recovery — what verifies, what reloads vs. rederives, what
records.
4. HITL async entrypoint — queue shape, backpressure, operator
interaction model.
Cross-references shelved project-engine-identity-candidate (DNA-
analog EngineIdentity) as potential primitive if sub-question 3
demands cross-reboot identity verification. Does NOT un-shelve it;
flags trigger.
Explicit rejections: database persistence (per ADR-0055 north-star);
network primary entrypoint (per user-circumstances memory entry,
always-on-internet unsafe to assume); multi-tenant; re-architecting
ChatRuntime.
Constraints inherited from CLAUDE.md: deterministic replay, no
hidden state, HITL is narrow entrypoint, reboot is recovery not
control flow, append-only artifacts stay append-only, no drift
repair / hot-path normalization.
This is a scope, not a decision. Spike/ADR decides; audit findings
(L4-L9) inform.
First per-layer audit of the substrate-liveness program. Establishes the
registry shape and standard of evidence for subsequent layers.
L0 (algebra primitives) is foundation; verdict CLOSED:
- 4 ADRs in scope (ADR-0001 versor invariant, ADR-0003 coordinate
dissolution, ADR-0004 rotor as operator, ADR-0009 compositional
physics).
- 6 modules (versor, rotor, cga, cl41, holonomy, backend); every
module has at least 2 live-import sites outside the package and
outside tests (38 distinct caller files overall).
- core test --suite algebra exercises every L0 module: 82 passed, 50
skipped (Rust-parity tests, Python-only env — not a closure gap).
- Cross-layer contract pass 1 (mechanical): every exposed symbol has
at least one downstream consumer.
- Cross-layer contract pass 2 (semantic): versor_condition < 1e-6
invariant is measured per turn (core/cognition/trace.py:34), folded
into deterministic trace payload, surfaced to operator via CLI, and
gated at the eval boundary (evals/cognition/runner.py:60). Matches
CLAUDE.md discipline of "measure and surface, don't weaken."
No cleanup performed — no dead code, no redundant modules, no
orphaned tests found at L0. Foundation is honest.
One scope-hypothesis correction: layering table cited "algebra/backend/"
(directory); reality is "algebra/backend.py" (file). Hypothesis drift,
recorded for amendment if it matters elsewhere.
Three forward-pointing notes left for downstream auditors:
- L1 should verify field propagation correctness is tested
independently of L5's downstream versor_condition measurement.
- L2 should verify vault honors exact-CGA-recall end-to-end, not just
at the algebra layer.
- ADR-0020 (Rust parity) is cross-cutting; audit when Rust integration
is live rather than at any single layer.
Format established for subsequent per-layer commits. Audit progress
table in registry root tracks pending layers; resume-after-interruption
is "look at the progress table, start with the first pending layer."
* docs(audit): scope substrate liveness audit (system-of-systems closure)
The recognizer-storage v1→v2 revision surfaced a pattern: CORE
contains ~140 ADRs, many marked Implemented, but several have
spec-in-code that nothing live calls (e.g., VaultPromotionPolicy in
core/physics/learning.py — imported by no module outside its package).
The engine today executes a subset of its own design.
Per the operator's system-of-systems framing (human body / universe /
ecosystem: subsystems achieve closure together; a half-built layer
degrades the whole organism silently): this scope defines a layered
audit that walks from the foundation outward to identify, per ADR
and per module, which subsystems are closed (designed + wired +
exercised + cross-layer consistent), which are partial, and which
are open.
The audit method is mechanical: grep + caller-trace + end-to-end test
verification + cross-layer contract check. Two reviewers running the
audit should produce identical verdicts. No refactoring, no new ADRs,
no subjective judgment — just evidence.
The output is two artifacts: a closure registry (per-layer, per-ADR
verdicts with evidence) and a ratchet plan (wiring sequence in
dependency order). Both append-only / revisable; both committed to
the repo as audit artifacts.
First-pass layering (L0 algebra primitives → L11 forever-running
engine, with L10 runtime model named as the missing prerequisite)
is a hypothesis the audit will refine. Layers L0–L3 are expected to
be closed (foundation); L4–L9 are expected to be partial; L10–L11
are explicitly open and depend on the audit + the runtime-model
scope.
Applies feedback-adr-cross-reference-discipline (the memory entry
this revision flagged): explicit cross-references to ADR-0006/0014/
0055/0056/0057/0142/0143/0144 and the existing scope docs.
This is a scope, not an audit. Audit deliverables (registry, ratchet)
are separate work.
* docs(audit): revise substrate-liveness-audit scope to v2 (self-review fixes)
Self-review surfaced two HIGH, three MEDIUM gaps in v1. Notably,
v1 of the scope that creates cross-reference discipline still
committed the documented mistake — third consecutive iteration of
the same failure mode in one session (recognizer-storage v1
substrate overclaim → recognizer-storage v2 drop-off invention →
audit-scope v1 ADR range mis-grouping). New "Self-review
acknowledgment" section records the pattern's durability and
states the structural mitigation: the audit's mechanical
deliverables make the discipline impossible to skip silently,
which is more rigorous than the memory entry alone.
HIGH-1 — ADR range mis-grouping. v1 layering table listed
"ADR-0055..0064" as L7 (teaching loop); verification showed
ADR-0058-0064 are predominantly L6 (surface composition,
correction telemetry, cross-pack resolution). Fixed L7 to cite
only ADR-0057; added explicit note that ADR-range citations
are starting points and the audit's first act per layer is
re-enumeration.
HIGH-2 — Audit tractability buried in risks. ~140 ADRs requires
structural handling, not just a risk warning. Promoted "per-layer
commits + per-layer handoff to subagents + progress tracking in
registry + optional per-layer file splitting" to a first-class
Step 0 in the audit method. The audit is explicitly framed as the
archetypal parallel-agent handoff candidate.
MEDIUM-1 — Expected-status column anchored the auditor. v1's
table had my predictions ("Closed (foundation)", "Live but
session-bounded"). Removed; replaced with a "Where to look first"
column. Explicit note: "No expected-status column intentionally
— predictions are the failure mode this scope was meant to
prevent."
MEDIUM-2 — "End-to-end test" criterion maps awkwardly onto CORE's
suite-lane organization. Reframed Step 4 to "Identify the
exercising suite lane" with concrete `core test --suite {…}` /
`core eval …` invocations. A module whose only test coverage is
in `tests/` files not reached by any suite lane is a closure gap.
MEDIUM-3 — Cross-layer contract check was hand-wavy. Made
Step 5 explicitly two-pass: mechanical (grep for at least one
consumer per exposed field/method) carries full verdict authority;
judgment-required semantic mismatches are flagged for operator
review rather than verdicted mechanically.
LOW fixes: softened "two reviewers identical" claim; L10/L11
explicitly marked not-audit-targets; per-layer file splitting
flagged as auditor's choice; closure-criteria item 4 wording
aligned with new Step 4.
Frontmatter status bumped to "Draft v2"; date line records
revision provenance.
* docs(recognition): scope recognizer storage against existing thermodynamic substrate
Two changes:
1. New scope: docs/decisions/recognizer-storage-scope.md (draft v1).
Reframes the recognizer-storage question against ADR-0006 (field
energy operator) and ADR-0014 (vault promotion policy) — the
thawed ↔ crystallized lattice already implemented under
core/physics/{energy,learning}.py. The three-candidate framing
(pack / vault / substrate) was drafted without acknowledging this
substrate; once it's in view, the storage question collapses to:
how does a derived recognizer participate in the existing
excitation / cooling / coherence-settling / promotion / re-thaw
dynamics, and what extension is needed for HITL-gated drop-off.
Names three measurements that need definition (recognizer
excitation, coherence residual, promotion criteria), one sibling
ADR (drop-off / deprecation), and the forever-running runtime
principle. Explicitly rejects pack-as-recognizer-container,
vault-without-substrate-reframe, per-session re-derivation, and
approximate match.
2. Amendment to docs/decisions/teaching-derived-recognition-scope.md.
Appends a "Connection to existing thermodynamic substrate" section
acknowledging the three-candidate omission, citing ADR-0006/0014,
and pointing forward to the recognizer-storage scope. The original
framing is preserved for history.
Neither doc proposes a decision. Both define the question.
Process note: the omission this corrects motivated saving a project
memory (feedback-adr-cross-reference-discipline) to prevent
independent reinvention in future ADR work.
* docs(recognition): revise recognizer-storage scope to v2 (self-review fixes)
Self-review surfaced two HIGH and two MEDIUM gaps in v1.
HIGH-1 — Substrate liveness overclaim. v1 described the entire
field-energy + vault-promotion lattice as live. Verified: only the
energy half is wired (FieldEnergyOperator called by ingest/gate.py,
field/propagate.py, language_packs/compiler.py); core/physics/learning.py
(VaultPromotionPolicy) is imported by no module outside core/physics/.
Added "Substrate liveness audit" subsection that honestly accounts for
which pieces are live vs. dormant, and explicitly states that the
recognizer-storage ADR must deliver both wiring the dormant promotion
path AND extending it for recognizers as content type.
HIGH-2 — Meta-irony: v1's drop-off section invented a HITL ratification
path without cross-referencing ADR-0057's existing teaching-chain
review/replay/append-only-log machinery — exactly the failure mode the
new feedback-adr-cross-reference-discipline memory was meant to prevent.
Added explicit cross-reference: drop-off reuses ADR-0057's review-and-log
plumbing; load-bearing originality is the recency-driven trigger and
the (non-replay-equivalence) gate. Plus HITL latency named as a
load-bearing architectural constraint, not just queue plumbing.
MEDIUM-1 — "Forever-running runtime" was framed as an assumption. Honest
status: current runtime is session-bounded (core chat is a CLI; each
invocation builds a fresh ChatRuntime; no long-lived process). Reframed
as a prerequisite (own scope, gates this one), not an assumption.
MEDIUM-2 — "Substrate-resident destination" was named but never sketched,
making the IOU concrete-free. Added a one-paragraph sketch (recognizer
as versor; recognizer as null-cone region) to keep the destination
honest. Explicitly illustrative, not committed.
LOW corrections inline: recognizer-excitation temporal-direction note;
0.05 residual threshold marked as default; cold-path latency reframed
as a general vault concern recognizers inherit rather than introduce.
Frontmatter status bumped to "Draft v2"; date line records revision
provenance.
566-line scope document defining the next recognition phase after
ADR-0144's epistemic carrier. Not a decision — defines the question
the follow-up ADR must answer.
v2 reframes from v1:
- feature-bundle outputs whose type emerges from lifted features (not
pre-decided proposition categories)
- evidence-bound lifts with span pointers + contradiction detection
for adversarial robustness
- multi-resolution decoding (chunked-first / word-by-word fallback)
Companion to docs/decisions/proposition-graph-scope.md (shipped with
ADR-0144). Anchored to the decoding-not-generating thesis.
Implements the PropositionGraph epistemic carrier (ADR-0144):
recognition/carrier.py — EpistemicTransition, EpistemicNode, EpistemicGraph.
Frozen, JSON-serializable, byte-deterministic. EpistemicNode wraps a
RecognitionOutcome with an append-only provenance chain; epistemic_state
property tracks last transition's to_state or outcome.state when empty.
recognition/connector.py — epistemic_node_to_graph_node(). Maps an admitted
EpistemicNode's FeatureBundle (agent/relation/count/unit) to a GraphNode
for the generation-side articulation planner.
CognitiveTurnPipeline gains a recognizer: DerivedRecognizer | None param
(default None — all existing callers unaffected). When attached, run()
calls recognize() at the top of every turn and wraps admitted outcomes in
an EpistemicGraph. CognitiveTurnResult.epistemic_graph carries it.
RuntimeConfig.recognition_grounded_graph: bool = False — opt-in flag that
replaces the intent-derived PropositionGraph with one derived from the
admitted EpistemicNode via the connector.
RatificationOutcome gains three specific PASSTHROUGH sub-values
(PASSTHROUGH_NO_FIELD / NO_VOCAB / NO_VERSOR) for _ratify_intent
observability (ADR-0142 debt 1). All normalise to "passthrough" before
trace_hash so pre-ADR-0144 hashes are byte-identical.
24/24 acceptance tests pass; 67/67 smoke tests pass; no regressions.
Adds recognition/outcome.py: RecognitionOutcome, FeatureBundle,
BoundFeature, EvidenceSpan, NegativeEvidence, the three typed refusal
classes (ShapeRefusal, FeatureEvidenceRefusal, FeatureConsistencyRefusal),
and RecognitionProvenance. Frozen dataclasses, JSON-serializable,
byte-deterministic invariants enforced in __post_init__.
ADR-0143 commits to Mechanism D (multi-resolution anti-unification over
token sequences) and defines the two-phase acceptance test.
* feat(epistemic): populate normative_detail on TurnEvent and ChatResponse
Adds normative_detail_from_verdicts() to core.epistemic_state and wires
it into both the stub and main ChatResponse/TurnEvent construction sites.
The field carries a sorted comma-separated list of violated boundary or
commitment IDs when normative clearance is VIOLATED or SUPPRESSED; empty
string otherwise.
* docs(ADR-0142): ratify epistemic state taxonomy — 14-state vocabulary + normative clearance axis
Formalises the six-subsystem Framing 1 audit findings into a first-class
decision. Accepts the 14-state taxonomy and companion 4-value normative
clearance axis. Documents Phase 3 deliverables already landed and defers
structured provenance + cross-subsystem transition machinery to ADR-0144.
* feat(epistemic): add first-class state enums
* feat(epistemic): tag TurnEvent with state axes
* feat(epistemic): serialize turn state axes
* feat(packs): tag curated and inferred unit entries
* feat(epistemic): expose word-level state on manifold
* feat(epistemic): expose vault status mapping
* feat(epistemic): preserve pack entry states through compiler
* test(epistemic): cover phase 3 state tagging spine
* feat(runtime): wire epistemic_state + normative_clearance into ChatResponse
Add first-class epistemic_state and normative_clearance fields to
ChatResponse (defaulting to "undetermined"/"unassessable" for backward
compat). Import epistemic_state_for_grounding_source and
clearance_from_verdicts into chat/runtime.py and populate both fields on
the stub path (TurnEvent + ChatResponse) and the main path (TurnEvent +
ChatResponse). Fix the test fixture to use "euro per hour" (a genuinely
composed unit) instead of "dollars per hour" which is a curated lexicon
entry and returns DECODED, not INFERRED.
* test(cognition): update term_capture_rate baseline from 0.9167 to 1.0
unknown_logos_019 now correctly surfaces "light" as a pack-resident
token near the logos versor — producing term_capture_rate 1.0 on both
main and Phase 3. The 0.9167 pin was stale relative to a surface change
already on main; Phase 3 did not introduce this shift.
* docs(epistemic-scope): mark Framing 1 audit complete across all six subsystems
Teaching pipeline (47 pts, 0 new states), cognition pipeline (42 pts,
0 new states, 1 EPISTEMIC_STATE_NEEDED placeholder), and chat runtime
(47 pts, 0 new states, 6 provenance gaps) audits complete. Taxonomy
confirmed stable; remaining work is implementation debt and provenance,
not taxonomy extension.
Vault, language-packs, and runtime-packs audits complete. Findings:
- Ratifies INFERRED (14th epistemic state): derived from DECODED
primitives by ratified deterministic rule; composite never curated.
Grounded by language_packs composition rules (per/square/cubic unit
synthesis). Sits between DECODED and UNVERIFIED-POSSIBLE in the
epistemic progression.
- Ratifies normative clearance axis as orthogonal companion to the
epistemic axis. Safety/ethics verdicts are not epistemic states;
they answer a different question (normative compliance vs. truth-
value). Four clearance states: CLEARED, VIOLATED, UNASSESSABLE,
SUPPRESSED. Every proposition in ChatResponse/TurnEvent carries
both an epistemic_state and a normative_clearance tag.
- Closes open question 5 (identity/safety/ethics interaction):
identity grounds the epistemic axis; safety/ethics live on the
normative axis; they coexist without collapsing.
- Updates RecognitionOutcome shape with both axes.
- Marks all four subsystem audits complete in Framing 1 block;
documents vault implementation debt (_status_admits conflation)
and deferred candidate (COMPOSED_RECOGNITION).
- Records four Phase 2 implementation bugs in summary:
evidence.py empty-pairs silent-FALSIFIED, runner DECODED-
UNARTICULATED misclassification, domain_contract present=False
inconsistency, _status_admits FALSIFIED/SPECULATIVE conflation.
Taxonomy is now stable. Phase 2 (bug fixes) and Phase 3 (first-class
state tagging in ChatResponse/TurnEvent) are the remaining work.
Math-subsystem audit (40 decision points, 9 files) confirmed the 9-state
starter taxonomy was too coarse. Ratifies four new states surfaced as
EPISTEMIC_STATE_NEEDED gaps:
- EVIDENCED-INCOMPLETE: lift completed for a sub-span; proposition
structurally partial but not contradicted (grounded by orphan-rate
refusal in math parser)
- DECODED-UNARTICULATED: trace is replay-equal; surface realization
path broke — exposes a runner misclassification where RealizerError
on a verified trace is currently outcome="wrong"
- SCOPE_BOUNDARY: engine recognized the proposition type; refuses
because the type is outside the current capability envelope, not
because the lift failed
- COMPUTATIONALLY_BOUNDED: search budget exhausted before answers were
enumerated; distinct from AMBIGUOUS (no answers compared) and
UNDETERMINED (text likely has structure)
Also: updates primitives mapping table, marks Framing 1 audit complete
for math subsystem, closes the "too coarse" risk item (9 → 13 states),
and records the runner misclassification as a load-bearing correctness
issue for the next runner revision.
* feat(ADR-0141): multiply as CGA dilator versor (positive non-zero)
Adds `multiply(scale)` to `generate/math_versor_arithmetic.py` as the
standard CGA dilator for multiplicative scaling along e1, restricted to
`scale > 0`. All ten ADR-0141 assertion families pass.
Preliminary measurement confirmed:
N = n_o ∧ n_inf: component -1 at index 15 (blade (3,4) = e4∧e5)
N² = +1.0 (pure scalar) → closed-form D_s = cosh(α/2) + sinh(α/2)·N
n_o · n_inf = -1; n_o² = n_inf² = 0
Because N² = +1, the cosh/sinh expansion is exact in float64 and
D_s · ~D_s = cosh² − sinh² = 1 holds to machine epsilon.
The sandwich D_s·X·~D_s produces a null point with n_inf normalization
1/s. `decode_quantity` is updated to divide by that factor, recovering
value · s. For translator outputs (normalization = 1) the result is
identical to the previous direct e1 read; all 152 prior add/subtract
tests pass unchanged.
`embed_quantity` is updated to embed directly in float64, eliminating
float32 quantization error for values like 0.01 (float32(0.01) ≠ 0.01);
all prior test-case values were exactly representable in float32.
* docs(ADR-0141): add decision document for multiply-as-dilator spike
The ADR doc was drafted in a separate branch and not present when the
implementation worktree was created from origin/main. Adding it now so
the decision record lands on main with the implementation it specifies.
Content unchanged from the draft — same spec the implementation already
satisfies (10 assertion families, fixed test cases, falsification
discipline, deferred scope for negative / zero / divide / Rate).
No code or test changes in this commit.
Extends generate/math_versor_arithmetic.py with one new function:
def subtract(addend: float) -> np.ndarray:
return translator(-float(addend))
Single-line delegate to translator(); no new algebra.
Adds tests/test_arithmetic_subtract_and_group.py covering all nine
ADR-0140 acceptance families:
Families 1-6 (ADR-0139 families applied to subtract):
1. Embedding well-formedness — null cone preserved for subtract cases
2. Translator-of-negative well-formedness — versor_condition < 1e-6
3. Closure — sandwich result stays on null cone
4. Arithmetic correctness — decoded value == a − b within 1e-9
5. Replay determinism — byte-identical across runs
6. Composability — subtract(c) ∘ subtract(b) decodes to a − b − c
New group-property families (structural verification of ADR-0139 claim):
7. Inverse composition — T_{-b} * T_b = identity (max residual: 0.000e+00)
8. Round-trip closure — versor_apply(T_{-b}, versor_apply(T_b, X)) → (a, u)
9a. Sum composition — T_a * T_b = T_{a+b} (max residual: 0.000e+00)
9b. Commutativity — T_a * T_b byte-equals T_b * T_a (all 10 cases)
All 96 tests pass. Group residuals are exactly 0.0 in float64.
The additive subgroup of Cl(4,1) translators along e1 is abelian and
closed; ADR-0139's algebraic claim holds at the group level.
First step of the Engine A lift program (CLAUDE.md commits the project to a
single deterministic cognitive engine; Engine B / math pipeline was always
intentional scaffolding per math_solver.py:24). Proves the load-bearing
unknown: one arithmetic operation can be represented as a closed versor at
the required tolerance, with no new normalization and no weakened invariant.
Scope (frozen by ADR-0139):
- One operation: add
- Single-axis embedding: quantities on e1 axis
- No graph wiring, no pipeline integration, no GSM8K case routed
- Unit carried as caller metadata
Construction:
- embed_quantity(v, u) = embed_point([v, 0, 0]) (existing CGA primitive)
- translator(b) = 1 - 0.5 * (b*e1 * n_inf) (textbook CGA translator)
- decode_quantity(F, u) = (F[1], u) (e1 coordinate)
Measured values (all 11 fixed cases + composability):
a b vcond(T) |<R,R>| decode_err
0.0 0.0 0.000e+00 0.000e+00 0.000e+00
0.0 1.0 0.000e+00 0.000e+00 0.000e+00
1.0 0.0 0.000e+00 0.000e+00 0.000e+00
3.0 4.0 0.000e+00 0.000e+00 0.000e+00
7.0 -3.0 0.000e+00 0.000e+00 0.000e+00
0.25 0.75 0.000e+00 0.000e+00 0.000e+00
1.5 2.5 0.000e+00 0.000e+00 0.000e+00
-5.0 5.0 0.000e+00 0.000e+00 0.000e+00
-2.0 -3.0 0.000e+00 0.000e+00 0.000e+00
100.0 1.0 0.000e+00 0.000e+00 0.000e+00
1.0 100.0 0.000e+00 0.000e+00 0.000e+00
compose (2, 3, 5) → 10: |<R2,R2>| = 0.000e+00, decode_err = 0.000e+00
Every residual is exactly 0.0 in float64. The construction is algebraically
closed: T_t * reverse(T_t) = 1 - 0.25*B^2 where B = t*n_inf, and B^2 = 0
because (e14)^2 + (e15)^2 = -1 + 1 and cross-terms cancel. No machine-epsilon
drift accumulates because the relevant cancellation happens at the algebraic
level before float arithmetic.
ADR-0139 acceptance items 1-6 (one parametrized test family each):
1. Embedding well-formedness — test_family1_embedding_is_null (11 cases)
2. Translator well-formedness — test_family2_translator_unit_versor (11 cases)
3. Closure — test_family3_sandwich_preserves_null (11 cases)
4. Arithmetic correctness — test_family4_decode_matches_sum (11 cases)
5. Replay determinism — test_family5_replay_byte_identical (11 cases)
6. Composability — test_family6_two_translators_compose (1 case)
Total: 56 tests, all passing.
Lift program decision: proceeds. Follow-on ADRs (subtract, multiply, Rate,
compare, MathProblemGraph → PropositionGraph, pipeline integration, first
GSM8K case end-to-end through Engine A) are now justified by a concrete
algebraic foundation rather than design speculation.
Out of scope per ADR-0139:
- No modifications to algebra/, core/cognition/, chat/, math_solver.py,
math_verifier.py, math_realizer.py, math_candidate_parser.py
- No GSM8K runner changes
- No pack changes
- Engine B continues serving GSM8K unchanged; the 3/50 admission set is
preserved
CLI lanes intentionally not run — main has known test-rot orthogonal to
this PR. The 56 new tests are self-contained and the diff touches only
three new files.
A small typed linguistic layer (FractionOperand, ComparativeOperand,
QuantityReference, ReferenceTarget) replacing the closed-and-deferred
ADR-0137 with a shape-driven design. Justified by the rescan-v3 ledger
showing 7 cases under fraction_operand + compound_comparative that
share a common deep structure (value relative to literal or
prior-sentence anchor) with multiple surface shapes.
Key commitments:
- Three typed phrase structures handle the closed surface set
(frac/percent of X, N times as much/greater than X, frac more/less
than X), where X is either a sentence-internal literal or a
QuantityReference to a prior grounded quantity.
- Binding pass is minimal: sentence-order lookup of QuantityReference
against prior grounded candidates with unique-match gating. No
generic DeferredCandidate apparatus.
- Pinned English convention: "N times greater" = N+1 multiplier
(documented + tested). Other reading requires explicit revisit.
- Supersedes ADR-0137 in scope; the deferral note's reopen criterion
is met by this lighter, more specific design.
- S.1/S.2 short-circuits stay as joins of grounded candidates;
this ADR does not subsume them.
Forcing case for cross-sentence binding: gsm8k-0029 ("three times
greater than the cost of the mouse" + "the mouse cost $16"). Clean,
unambiguous, two-sentence binding.
Self-correction caught before push: gsm8k-0010 was initially framed
as the forcing case, but its expected dataset answer requires a
non-standard English parse of "1/4 more than X" (the natural reading
gives 17; the dataset says 9, requiring X/4 + 7). Per non-negotiable
#2 the binding pass MUST refuse on this; 0010 is reclassified from
"expected admission" to "negative probe." 0029 takes the forcing-case
slot.
Honest expected admission delta: +1 to +2 (0005, 0029 high
confidence; 0041, 0043 partial credit only).
In scope: in the same PR family, fix the issue gsm8k-0029 surfaces by
implementing the binding pass. Out of scope: inverse arithmetic
(0004), multi-term aggregate references (0036), pronoun anchoring,
self-referential cycles.
S.4 extends initial-state parsing with two closed subject-slot widenings:
- Indefinite-article: `A <noun> has N <unit>` (gsm8k-0046 sentence 1)
- Prepositional-prefix existential: `In a <place>, there are N <unit>...`
(gsm8k-0038 sentence 1)
Design choice: sibling regexes (_INITIAL_HAS_INDEF_RE,
_INITIAL_THERE_ARE_PREFIX_RE) rather than widening the global _ENTITY
pattern — preserves existing behavior across all other initial-state
extractors (cascade-safety).
Per the S.x corridor discipline: no new short-circuit; new candidates
flow through extract_initial_candidates and the existing graph machinery.
No solver/graph/verifier changes.
Honest delta:
- Direct admissions: 0 (admission set unchanged at {0014, 0018, 0042})
- Barrier shifts: +2 (gsm8k-0038: novel_initial_form → compound_comparative;
gsm8k-0046: novel_initial_form → fraction_operand)
- wrong == 0 on every lane
Bundled with this PR for ledger currency:
1. tests/test_rescan_v3_invariants.py refactored to read frozen on-disk
v3 artifacts only (no more re-running build_rescan against live
parser). The previous design tied a historical snapshot to live code
and broke the moment any new phase landed.
2. rescan_v4.py + refusal_rescan_v4.json + refusal_taxonomy_v4.json +
tests/test_rescan_v4_invariants.py — the current live snapshot.
Shifts: exactly 2 (0038, 0046). Same pattern as v3.
Sonnet wrote: S.4 parser/axis-lane/tests/ADR.
Opus wrote: rescan_v4.py + v3 test refactor + bundling.
Files:
- generate/math_candidate_parser.py (+142 lines)
- evals/math_capability_axes/S4_novel_initial_form/v1/ (20-case lane)
- tests/test_adr_0136_S4_novel_initial_form.py (40 tests)
- docs/decisions/ADR-0136.S.4-novel-initial-form.md
- evals/gsm8k_math/train_sample/v1/{rescan_v4.py, *_v4.json}
- tests/test_rescan_v4_invariants.py (8 tests)
- tests/test_rescan_v3_invariants.py (refactored to artifact-only)
Re-runs parse_and_solve on the 50-case GSM8K train sample on current
main (post-S.3) and compares to v2. Result: admitted=3/50 (unchanged),
wrong=0, exactly 1 barrier shifted v2→v3.
Shift: gsm8k-0010 (compound_statement → fraction_operand). S.3's
_INIT_MUTATION_RE resolves "Yun had 20 paperclips initially, but then
lost 12" to InitialPossession(Yun, 8, paperclips). First refusal moved
to sentence 2: "Marion has 1/4 more than what Yun currently has, plus
7" — needs fraction-operand + coreference-quantity + comparative-additive
arithmetic.
Top blockers (v3):
compound_statement 5 (was 6)
novel_initial_form 5 (unchanged)
fraction_operand 4 (was 3 — gsm8k-0010 moved here)
novel_initial_verb 4 (unchanged)
Artifacts:
- evals/gsm8k_math/train_sample/v1/rescan_v3.py
- evals/gsm8k_math/train_sample/v1/refusal_rescan_v3.json
- evals/gsm8k_math/train_sample/v1/refusal_taxonomy_v3.json
- docs/decisions/ADR-0136.S3-post-rescan.md
- tests/test_rescan_v3_invariants.py (7 tests; determinism + admission
set unchanged + exactly-one-shift + 0010-specific shift assertions)
PR #204 (ADR-0137 retrospective binding, rescoped to subsumption-only)
was drafted then closed without merging. The deferral reason is
structural: the DeferredCandidate/BindingProof apparatus is the right
shape for true retrospective binding (first-pass candidate has open
slots awaiting later evidence), but re-examination showed neither S.1
nor S.2 short-circuit fits that definition — both join two fully
grounded candidates. With no kind that has open slots in the v2
ledger, the machinery would degenerate to a join everywhere, which is
speculative infrastructure ahead of a forcing function (CLAUDE.md
warns against this pattern).
Update appended to the post-rescan notes so the deferral lives in the
repo, not just in a closed PR thread. Includes a reopen criterion:
ADR-0137 (or successor) may return when a case appears whose first-pass
candidate has genuinely open slots only later-sentence evidence can
close. Until then the unlock vehicle is S.x.
The S.1/S.2 short-circuits remain as tactical bridges; the canonical
runner staleness is better fixed in the runner than in a binding
layer.
Measurement-only branch. Re-runs parse_and_solve on all 50 GSM8K train-sample
cases against the current parser (post-S.1/S.2) and produces a barrier-shift
ledger comparing v1 taxonomy to current behavior.
Results: admitted=3/50 (0014, 0018, 0042), wrong=0, barrier_shifted=27/50.
Context-filler dominance collapsed from 23→3 cases; compound_statement (6)
and novel_initial_form (5) are now the largest buckets.
Subsumption directive pinned: ADR-0137 SHALL re-derive all short-circuit
admissions as (DeferredCandidate, evidence, BindingProof) triples.
All five G.x capability axes shipped (G.1–G.5); admission remained 0/50
throughout — expected, because the 50 sample cases fail at the statement
layer (rate verbs, implicit entities, multi-step ops), orthogonal to the
question-layer and initial-state-verb work the G.x axes targeted.
Decisions:
- Probe retired as a per-iteration gate (signal-to-noise ≈ 0 at 0/50)
- `admitted_wrong == 0` invariant preserved as a standing architectural
invariant, CI-pinned by existing test + per-axis regression guards
- Re-activation criterion named: statement-layer expansion that
plausibly admits ≥1 new case (Δadmission_rate ≥ 0.02)
- Probe runner + report stay in-repo for future statement-layer push
Docs-only. No test, parser, solver, or eval changes.
Rebases onto current main (dec98ea, post-G.1/G.3.1/G.4/promotion).
Parser:
- Extend _COMPARE_MULT_ANCHOR_RE anchor alternation to include 'quarter'
and 'third'; add optional 'a\s+' article prefix so "a quarter as many"
and "a third as many" parse. Both anchors are in COMPARE_MULTIPLICATIVE_ANCHORS
and the round-trip factor-divisor table ("quarter":4, "third":3), so
round-trip checks pass. quarter→0.25 (exact), third→1/3 (float).
- Add _ANCHOR_TO_FACTOR entries for quarter and third.
Gate regex (test_adr_0131_G2_comparatives.py):
- Widen _COMPARATIVE_STATEMENT_PATTERNS multiplicative pattern from
'\d+\s+times' to '\w+\s+times' to match word-number forms ("four times")
that would be missed by the digit-only pattern if a future GSM8K case
contains one in a still-refused statement.
Cases (31 total, was 24):
- G2-mul-frac-005/006: two 'quarter' cases (fraction direction now has
half×4 + quarter×2 + third×1 = 7 cases, was 4 all-half).
- G2-mul-frac-007: 'third' case.
- G2-refuse-006: hyphenated 'one-third' pins the closed-anchor boundary.
- G2-refuse-007: 'double as many' pins the deferred grammar shape.
Tests (25, was 21):
- Add quarter and third parametric entries to test_multiplicative_direction_admits.
- Add one-third and double-as-many refusal params to test_refusal_cases.
- Add quarter/third to test_direction_literals_closed_set.
- Update test_runner_per_category_minima comment to reflect new counts.
ADR: document quarter/third admission, updated case table, deferred list.
report.json: refreshed to 31 cases, wrong==0 preserved.
Bundles the three pieces needed to consummate the promotion after
the reviewer signature lands:
1. Wire the expert tier in the capability ledger
2. Path-stability fix (digest filesystem-independence)
3. Reviewer-registry allow-list extension (regression fix for #194)
Result: mathematics_logic is now the first expert-tier domain in
the capability ledger.
$ ledger_report() -> mathematics_logic row:
status: "expert"
predicates: { seeded, grounded, reasoning_capable,
audit_passed, expert: True }
expert_reason: "ADR-0120-math composer admitted"
1. Ledger wiring (core/capability/reporting.py):
- _EXPERT_DOMAIN_STATUSES extends to 6 tiers with "expert"
after "audit-passed" (strict super-tier).
- New _EXPERT_COMPOSERS dict — per-domain registry of composer
module names. Currently only mathematics_logic ->
core.capability.expert_promotion_math.
- New `expert` predicate computation gated on audit_passed;
calls registered composer's evaluate_math_expert_promotion()
and reads promote_admitted as the verdict. Fail-closed on
exception or missing composer.
- status = "expert" when predicate True.
- predicates dict gains "expert" key; row gains expert_reason.
2. Path-stability fix (composite_math_gate.py + expert_promotion_math.py):
- New _rel(path) helpers return repo-root-relative POSIX
strings instead of str(absolute_path).
- claim_digest now commits to relative paths, so operator A
on ~/work/core and operator B on /srv/checkouts/core compute
the SAME digest for identical evidence.
- Without this fix no signature would ever match across
filesystems — a real bug that would have blocked every
signing attempt.
3. Allow-list regression fix (core/capability/reviewers.py):
- ALLOWED_TOP_LEVEL_KEYS extended with "math_expert_claims".
- PR #194 added the section to docs/reviewers.yaml but didn't
extend the allow-list, silently breaking the audit_passed
predicate for ALL 3 prior domains (loader rejected the file).
This PR's test_allowed_top_level_keys_includes_math_expert_claims
regression-pins the fix.
Reviewer signature (operator-only action by shay-j) carried in
docs/reviewers.yaml:
math_expert_claims:
- domain_id: mathematics_logic
signed_by: shay-j
claim_digest: "94149794e8c19896851e062cf1f921cfa9ba04770b674bc3b4c33023f7c7331b"
The auto-mode safeguard correctly blocked the agent from self-
signing during PR construction; the signature was performed by the
reviewer directly and brought into this PR. Future signatures stay
human-only.
Tests: 12/12 new ledger-flip tests + 174/174 across full obligation
auditor / composer / composite-gate / expert-demo / reviewer-registry
regression. Updated #194's awaiting-state snapshot to reflect the new
promote_admitted=True state on main.
GSM8K (honest disclosure, not gating): still 0/50 admission, wrong=0,
safety_rail_intact=True, substrate=candidate_graph. Probe lift is
future work (bounded pronoun coref is the highest-leverage item —
~28% of refusals route through it). The promotion does not depend
on GSM8K per ADR-0131.
Final wire-up after all 10 ADR-0114a obligations + ADR-0131.4
composite gate landed. Composes:
- all 10 obligation verdicts (5 from new auditor modules,
5 from inline checks over existing infrastructure)
- ADR-0131.4 composite math gate verdict
- ADR-0092 reviewer-signed claim entry from docs/reviewers.yaml
into a single deterministic promotion verdict + canonical
signed/unsigned ``expert_claims_math_v1_signed.json`` artifact.
Empirical verdict on current main (first evaluation):
all_obligations_passed: True
composite_gate_passed: True
technical_pass: True
claim_digest: d164866975341d9b82503caf50c0404ee140eab21fd60f589536c6daf6e1d706
reviewer_signature_present: False
promote_admitted: False
refusal_reason: awaiting reviewer signature
Every technical gate passes. The PR ships in the architecturally-
correct "awaiting reviewer signature" state — the reviewer's
signature is the separate, auditable operator action that
consummates the promotion.
Operator workflow (post-merge):
1. Run `core capability math-expert-promote`, confirm verdict,
capture claim_digest.
2. Add entry to docs/reviewers.yaml under math_expert_claims:
- domain_id: mathematics_logic
signed_by: shay-j
claim_digest: "d164866975341d9b82503caf50c0404ee140eab21fd60f589536c6daf6e1d706"
3. Re-run — promote_admitted flips to True.
4. Separate ledger-flip PR (out of scope here) consumes the
signed artifact and writes the capability ledger.
Safety property: if the evidence bundle changes after signing
(B-lane re-run, pack edit, obligation report shift), the digest
changes and the existing signature stops matching. The verdict
reports the mismatch explicitly and the operator must re-inspect
and re-sign — a ledger flip can't survive a silent evidence change.
New files:
- core/capability/expert_promotion_math.py — the composer
- tests/test_adr_0120_math_expert_promotion.py — 18 tests
- docs/decisions/ADR-0120-math-expert-promotion-wireup.md — ADR
Modified:
- core/cli.py — new `core capability math-expert-promote` cmd
- docs/reviewers.yaml — added math_expert_claims: [] section
with documentation comment
Tests: 18/18 covering each inline obligation evaluator
(#1/#3/#4/#7/#9 pass + failure modes), composer integration
against current main, reviewer-signature path (matching → admitted;
mismatched → refused with explicit diagnostic), digest
reproducibility, artifact byte-equality. All pass in 0.49s.
Trust boundary: read-only access to 4 B-lane reports +
GSM8K probe + 5 obligation auditor reports (transitively) +
frontier dir + docs/reviewers.yaml; single deterministic write
to the artifact path; no dynamic imports, no shell, no network.
This is the last PR before the first mathematics_logic -> expert
ledger flip attempt. The actual flip is reserved for a separate
small PR that consumes the signed artifact.
35-case OOD set (ood-001..ood-035): surface-varied siblings of B3's 35
solved_correct public cases. Entity-name pool: Maya/Liam/Noah/Diana/Felix/
Priya/Omar/Rosa/Jun/Kai. Unit-noun pool: oranges/marbles/pencils/books/
stamps/coins/balls (all parser-allowed count nouns). Every case in-grammar
per ADR-0131.3 and parseable without error.
Auditor (core/capability/ood_ratio.py): reads B3 public report.json + OOD
report.json, computes ood_ratio = ood_accuracy / public_accuracy, enforces
two independent gates — ratio ≥ 0.95 and wrong == 0.
CLI: core capability ood-ratio (exit 0 iff both gates pass).
Measured: public 50/50=1.000, OOD 35/35=1.000, ratio=1.000. Obligation #10
and B3 public lane unchanged.
Implements the external auditor for ADR-0114a Obligation #6:
"depth_curve.py produces a per-bucket curve;
accuracy(N) >= accuracy(depth_1) * (1 - eps)^(N - 1) for eps = 0.05."
Mirrors PR #189's auditor pattern (re-runs lane via the candidate-
graph pipeline, aggregates over committed cases, emits deterministic
report). Uses len(trace.steps) as the authoritative depth — the
engine's actually-executed reasoning, not the case's declared depth.
New module core/capability/depth_curve.py:
- Bucket schema mirrors ADR-0119.6: depth_1, depth_2-3,
depth_4-5, depth_6-8. Depth > 8 raises rather than silently
extending. Depth == 0 (initial-only problems) skipped — nothing
to decay.
- representative_depth = min(bucket) — most permissive bound
convention; tightening requires an ADR amendment.
- epsilon = 0.05 pinned per ADR-0120 §Threshold rationale.
- Two-axis verdict: obligation_6_mechanism_wired (always true if
auditor ran), obligation_6_assertion_holds (every populated
bucket satisfies the decay bound), coverage_sufficient (>=2
buckets populated AND >=3 cases each — required for the
assertion to be statistically meaningful).
CLI: core capability depth-curve (added to core/cli.py).
Writes evals/obligation_6_depth_curve/<lane_id>.json.
Empirical verdict on current main:
lane: B3_bounded_grammar
cases_total: 50
cases_solved: 22
mechanism_wired: True
assertion_holds: True
coverage_sufficient: False
populated: [depth_1 (21/21=1.0000), depth_2-3 (1/1=1.0000)]
Both populated buckets satisfy the decay bound. Coverage gap is
honestly named in the refusal_reason: depth_2-3 has only 1 case,
depth_4-5 and depth_6-8 have none. This is B3-owner work (case
authoring under the existing grammar contract), not auditor work;
reserved as a B3 v1.1 follow-up PR.
Honest scope-limit: B3 only. B1 (algebra, no trace) and B2 (chain
validation, not problem-solving) need different metrics — separate
sub-ADRs.
Trust boundary: read-only access to B3 cases + transitive pack
reads via the pipeline; single deterministic write to artifact path.
Tests: 24/24 covering bucket schema closure (depth 1..8 + raise on
9+), decay bound math (epsilon pinned, formula correct, depth_1 has
no bound), coverage-sufficient policy (thresholds pinned), lane
evaluation (passes on real B3 + refuses on missing cases),
coverage-sufficient distinction (B3 today vs synthetic 5+5 fixture
showing both pass), determinism (report identical + artifact
byte-equal).
External auditor for ADR-0114a Obligation #8:
"adversarial/score.py reports wrong == 0 across all families;
>= 30 cases x >= 8 families."
Verdict on current main:
cases_total: 36
families_total: 9
cases_refused: 28
cases_solved: 8
cases_wrong: 0 <-- the gate
obligation_8_passed: True
New module core/capability/adversarial.py mirrors PR #189/#190/#191
auditor pattern. Pure function over the committed cases set; broad
exception capture (correctly classified as refused — engine
couldn't process the input) makes the auditor robust to upstream
typed-refusal gaps.
New dataset evals/obligation_8_adversarial/v1/cases.jsonl — 36
cases x 9 families, closed taxonomy:
- paraphrase (verb outside initial-anchor whitelist)
- unrecognized_unit (not in en_units_v1)
- conditional (if/would/suppose)
- pronoun_coref (cross-sentence he/she/they)
- hedged_quantity (about/almost/approximately)
- ordinal_confusion (the 5th/third in cardinal position)
- implicit_subject (no named entity)
- self_reference (actor as comparison ref or transfer target)
- distractor_noise (adjectival/temporal/irrelevant siblings)
CLI: core capability adversarial. Writes
evals/obligation_8_adversarial/<lane_id>.json. Exit 0 iff
obligation passes.
Honest disclosure — 8 of 36 cases solved rather than refused;
none produced wrong answers. Two parser-layer gaps surfaced:
Gap A (pronoun_coref, 4/4 solved): unbound sibling sentences
silently drop; engine returns last-asserted state. Faithful but
semantically poor. Reserved follow-up: tighten admissibility so
unbound sentences refuse the whole case.
Gap B (unrecognized_unit, 4/4 solved): _canonicalize_unit
falls back to '+s' plural rule when pack doesn't recognize
the unit. Reserved follow-up: opt-in strict mode behind a flag
(some B3 units aren't in en_units_v1 either; strict mode
requires parallel pack extension).
Bug caught: adv-self-reference-003 ("Sam gives 3 apples to
Sam.") raises uncaught MathGraphError from
Operation.__post_init__. Auditor catches it as
refused-via-exception; ~3-line follow-up in
_build_op_candidate fixes the parser side.
Trust boundary: read-only access to cases + transitive pack reads;
single deterministic write to artifact path.
Tests: 11/11 in tests/test_adr_0114a_8_adversarial.py covering
threshold pinning (>= 30 cases / >= 8 families), closed taxonomy
(every documented family has cases; no unknown families),
obligation-passes snapshot, per-family wrong=0 invariant, failure
modes (missing file, below-threshold count), determinism (report
identical + artifact byte-equal).
Implements the external auditor ADR-0114a Obligation #10 requires:
"Every SolutionTrace.steps[*].pack_lemma_id resolves to a real
lexicon entry in the domain's operator pack." The solver enforces
this at solve time; this PR audits it from outside.
New module core/capability/pack_provenance.py:
- _load_lexicon_lemmas(): independent re-read of pack lexicon
- _parse_lemma_id(): <pack_id>:<lemma> shape parser
- validate_lane(): re-runs candidate-graph pipeline on a B-lane's
cases, walks every solver step, validates pack_lemma_id parses
AND resolves to a lexicon entry. Per-case + per-lane verdict.
- emit_provenance_report(): deterministic artifact emission.
CLI: core capability pack-provenance (added to core/cli.py).
Writes evals/obligation_10_pack_provenance/<lane_id>.json.
Empirical verdict on current main (post-PR #186):
lane: B3_bounded_grammar
cases_total: 50
cases_validated: 25 (every expected-correct B3 case)
cases_skipped_unsolved: 25 (refusal-expected probes — by design)
cases_violated: 0
obligation_10_passed: True
5 distinct lemma_ids observed (add, subtract, transfer,
compare_additive, compare_multiplicative) — all resolve to
en_arithmetic_v1. The other 3 op kinds (multiply, divide,
apply_rate) ratify-at-solve-time via _resolve_pack_lemmas so the
obligation holds for them too if a future case exercises them.
Honest scope-limit: B3 only. B1 (symbolic equivalence) and B2
(teaching corpus) equivalents deferred to separate sub-ADRs —
B1 needs reframing (algebra normalization chain, not arithmetic
steps); B2 can use this same auditor signature once corpus
solver-trace exercise is confirmed case-by-case.
Composition with ADR-0131.4: orthogonal. Composite gate verdict
+ obligation #10 verdict + 4 other obligation auditors (when
they land) + reviewer signature → full ADR-0120 wire-up.
Trust boundary: read-only access to pack lexicon + B3 cases;
single deterministic write to artifact path. No dynamic imports,
no shell passthrough, no network. Pure deterministic auditor.
Tests: 19/19 in tests/test_adr_0114a_10_pack_provenance.py
covering lemma-id parser (well-formed + malformed), lexicon loader
(real pack + every failure mode), lane validator (passes on real
B3 + refuses on missing pack/cases + skips refusal-expected cases
without false violation), determinism (report identical across
calls + artifact byte-equal).
Cognitive capability: extend bounded grammar to admit acquisition/action
verbs (buys, bought, collected, saved, saved-up, makes, sells) as
operation-kind entries, and pure-possession verbs (had, started, started-with)
as initial-possession anchors.
What invariant proves correctness:
- wrong == 0 across all G1 curated cases (20/20) and GSM8K probe (0 wrong/50).
- versor_condition and field invariants untouched — no algebra-path changes.
- Round-trip filter (math_roundtrip.roundtrip_admissible) unchanged.
Which CLI suite / eval proves the lane:
pytest tests/test_adr_0131_G1_verb_classes.py — 15/15 pass
pytest tests/test_adr_0126_runner_wiring.py — 9/9 pass (3 regressions fixed)
pytest tests/test_adr_0131_{1,3}_*lane.py — 17/17 pass
pytest tests/test_adr_0131_G_gsm8k_coverage_probe.py — 8/8 pass
pytest tests/test_gsm8k_math_runner.py — 11/11 pass
Key architectural change:
Acquisition verbs that also appear in ADD_VERBS/SUBTRACT_VERBS were
previously listed in _INITIAL_HAS_RE, causing branch-disagreement refusals
when a canonical 'has' initial preceded an acquisition sentence for the
same entity. Fix: narrow _INITIAL_HAS_RE to pure-possession anchors only
(has/have/had/started); acquisition verbs remain exclusively in KIND_TO_VERBS.
The solver's default-from-zero means 'Sam buys 5 apples. How many does
Sam have?' resolves as 0+5=5 without any initial-possession candidate.
Optional verb particle (up/down/out/...) added to _op_pattern to handle
'saved up N', 'picked up N' etc.
No changes to binding graph, solver, verifier, or versor/CGA algebra.
No stochastic generation, approximate recall, or hidden normalization.
Trust boundaries unaffected — no new dynamic imports or user-input paths.
Implements ADR-0131's revision of the ADR-0120 expert-promotion
contract for mathematics_logic: replaces the single-benchmark
GSM8K-coverage check with a composite B1+B2+B3 requirement.
New module core/capability/composite_math_gate.py:
- evaluate_composite_math_gate(): pure function over already-
committed B-lane reports; handles heterogeneous report shapes
(B1/B2 counts vs B3 metrics); applies pinned thresholds
(correct_rate >= 0.95 AND wrong == 0); composes verdicts.
- Reproducible SHA-256 claim_digest over canonical evidence bundle.
- GSM8K honest-disclosure (admission/wrong/refused/substrate)
embedded in artifact but never gates per ADR-0131.
CLI: core capability math-expert-gate (added to core/cli.py).
Writes evals/math_expert_claims/v1/expert_claims_math_v1.json.
Empirical verdict on current main (post-PR #182/#183/#184/#185):
composite_gate_passed: True
B1_public: 185/185 wrong=0 rate=1.0000
B1_sealed: 14/14 wrong=0 rate=1.0000
B2_teaching_corpus: 40/40 wrong=0 rate=1.0000
B3_bounded_grammar: 50/50 wrong=0 rate=1.0000
GSM8K disclosure: 0/50 admission, wrong=0, substrate=candidate_graph
The math expert is gate-passing under ADR-0131's revised composite
contract. The architectural bet ADR-0131 placed has paid off.
Honest scope-limit: this implements only the ADR-0131-specific
revision (composite benchmark portion). The full ADR-0120 10-
obligation contract still requires substrate for 5 missing
obligations (OOD ratio, perturbation, depth curve, adversarial,
operation-provenance-via-pack). Those are sequencing-wise *after*
ADR-0131.4, not bundled. Reviewer signature via ADR-0092 registry
is also reserved.
Trust boundary: read-only access to 5 committed lane reports;
single deterministic write to the artifact path. No dynamic
imports, no recomputation of lane verdicts.
Tests: 12/12 in tests/test_adr_0131_4_composite_math_gate.py
covering threshold pinning, heterogeneous shape handling, gate
logic (passing + every failure mode), GSM8K honest disclosure
(never gates), determinism (claim_digest + artifact byte-equality),
and a snapshot test confirming current main satisfies the gate.
ADR-0131.4 module note: the parent ADR-0131 plan named
formation/ratify.py + formation/promote.py as the wire-up site —
that was a misidentification (those govern teaching-example
SPECULATIVE→COHERENT bridging per ADR-0021, not domain-tier
promotion). Correct site is core/capability/, where audit-passed
gate already lives.
Four axes deferred from ADR-0131.G.3 (PR #183):
1. Fractions end-to-end: new _INITIAL_FRACTION_OF_RE extractor handles
`N/M of [a/an] <unit>` shape; _resolve_value already handles N/M arithmetic.
2. Multi-currency: _MONEY_SYMBOL widened to six symbols; _CURRENCY_SYMBOLS table
+ _resolve_currency dispatcher; ¢/€/¥/₱ wired end-to-end. £/pound sterling
deferred to G.3.2 (question extractor's single-token unit slot cannot parse
two-word surface "pounds sterling").
3. Multi-token cardinals: dedicated _MULTI_WORD_CARDINAL_RE extractor (approach a)
delegates to parse_compound_cardinal; avoids greedy unit-slot boundary ambiguity
from widening _VALUE.
4. Word-num-adjective: optional adjective group added to _INITIAL_HAS_RE and
_MULTI_WORD_CARDINAL_RE; closed adjective list identical to _CONJ_OBJECT_RE.
Also fixes six pre-existing G4 type bugs where _resolve_value() result was used
directly as a numeric operand (TypeError: _ResolvedValue is not a number).
Axis lane v1_1: 20/20 solved_correct, 0 wrong, 8/8 refusals, overall_pass=True.
GSM8K probe: 0/50 admission_rate unchanged, admitted_wrong=0 (safety rail intact).
42/42 new tests pass; parent v1 lane (26/26) unaffected.
Highest-risk axis of the ADR-0131.G capability iteration: within-
sentence multi-clause composition. Four extractors land in the
candidate-emitting parser; no graph-side or solver changes.
Parser extension (generate/math_candidate_parser.py)
- _conj_subject_each_candidates: '<A> and [his/her/their <kin>] <B>
each <verb> <N> <unit>' → 2 CandidateInitial (one per actor).
- _conj_object_candidates: '<E> has <N1> <unit1> and <N2> <unit2>' →
2 CandidateInitial for the same entity; same-unit conjuncts refuse
(would silently collide under solver overwrite-on-collision).
- _embedded_quantifier_candidates: '<E> has <N> <container> with <M>
<unit> in each [<container>]' → 1 derived CandidateInitial
(value=N*M).
- _embedded_quantifier_candidates (conj branch): '... <N1> <C> with
<M1> <U> in each ... and <N2> <C> with <M2> <U> in each ...' → 1
SUM CandidateInitial (value=N1*M1+N2*M2); mixed-unit refuses.
- CandidateInitial anchor whitelist widened to include
saved/earned/got/received/bought/made/paid (and inflections) —
narrow widening needed for the conjoined-subject-each shape.
Closed-set discipline
- Distributive 'each' only — 'each ... together/altogether' refuses.
- Two-way conjunction only — 3-way refuses by non-match.
- Cross-sentence coreference stays refused (within-sentence axis).
- Ambiguous 'each' scope refuses (container2 must agree).
Curated axis lane (32 cases)
- evals/math_capability_axes/G4_multi_clause/v1/cases.jsonl:
conj_subject_each ×6, conj_object ×6, embedded_quantifier ×6,
conj_embedded ×6, refusal ×8.
- evals/math_capability_axes/G4_multi_clause/v1/runner.py +
report.json: deterministic; wrong==0 gate; byte-equal across runs.
Tests (26 new)
- tests/test_adr_0131_G4_multi_clause.py: per-shape emission,
refusal probes (parametric), distributive-only policy,
cross-sentence refusal, runner byte-equality, GSM8K-probe gate.
GSM8K-probe gate (chosen: multi-clause refusals ↓)
- evals/gsm8k_math/train_sample/v1/report.json (candidate-graph
probe): multi-clause statement-refusal count 2 → 1. Case 0042
('Ella has 4 bags with 20 apples in each bag and six bags with 25
apples in each bag.') moves from statement-clause refusal to
question-layer refusal. Case 0026 ('Aaron and his brother Carson
each saved up $40') stays refused on the '$' value slot
(deferred to G.3 numeric-literals axis).
- evals/gsm8k_math/train_sample/v1/train_sample_coverage_report.json
(legacy probe): refreshed, byte-identical (legacy parser
untouched).
B3 + candidate-graph + GSM8K probe lanes all pass (95/95
regression). wrong==0 preserved everywhere — load-bearing for the
highest-risk axis.
Zero behavior delta on the main baseline (both substrates produce
0/50 admission today) — but every subsequent ADR-0131.G.<n> iteration
now produces attributable admission deltas on the probe, instead of
silently extending a parser layer the probe wasn't measuring.
Background: ADR-0131.G's probe consulted run_lane → _score_one →
parse_problem (legacy first-match-wins parser, pre-ADR-0126). Every
G.<n> iteration extends the candidate-graph parser via
_score_one_candidate_graph → parse_and_solve. The mismatch was
discovered during G.3 development and explicitly reserved as this
follow-up.
Changes:
- run_coverage_probe.py: switch import to _score_one_candidate_graph;
new private _score_lane aggregator mirrors run_lane's output shape
via per-case scoring; report root adds "substrate": "candidate_graph"
for audit trail.
- train_sample_coverage_report.json: regenerated. All metrics
byte-identical to prior baseline (0/50 admission, wrong=0).
refused_reasons_top text differs (candidate_graph: prefix instead
of parser:) — expected and part of the substrate audit-trail shift.
Discipline: separate small PR per ADR-0131.G's "expansion that only
moves admission must be a standalone PR" principle. Substrate swap
attributable; future G.<n> deltas attributable.
Evidence:
- python3 -m evals.gsm8k_math.train_sample.v1.run_coverage_probe
→ admission 0/50, wrong=0, safety_rail_intact=True, exit 0
- pytest tests/test_adr_0131_G_gsm8k_coverage_probe.py
→ 8/8 pass in 0.18s (no test edits needed; tests pin invariants
not numbers)
- No changes to runner.py, no changes to any G.<n> work in flight.
Effect on in-flight iterations: each G.<n> PR (G.1 Gemini / G.2 #182 /
G.3 #183 / G.4 Opus#2) rebases after this lands and refreshes its
committed train_sample_coverage_report.json with the new substrate's
numbers. Rebase is mechanical.
First capability-axis iteration after ADR-0131.G baseline. Extends the
candidate-graph parser's <value> slot to recognize:
- Money symbol literals: $N and $N.NN (1-2 decimals); $N.NNN refused
- Money word forms: N dollars / N cents
- Hyphenated multi-word cardinals: twenty-five, ninety-nine, ...
All money values normalize to integer cents, unit 'cents' — pack-aligned
with en_units_v1's canonical_unit='cent' for the money dimension.
en_numerics_v1's parse_compound_cardinal handles hyphenated cardinals.
Parser changes (generate/):
- math_candidate_parser.py: _VALUE alternation widened; _resolve_value
refactored to return _ResolvedValue|None carrying optional unit
override; _INITIAL_HAS_RE unit slot made optional; dollar/dollars →
cents normalization at candidate build.
- math_roundtrip.py: new _unit_grounds helper (money-aware); _value_grounds
widened for the three new literal shapes; roundtrip_admissible uses
_unit_grounds for the unit check.
- math_candidate_graph.py: _initial_admissible and _question_admissible
use _unit_grounds.
New axis lane (evals/math_capability_axes/G3_numerics/v1/):
- 26 curated cases (20 positive across 4 classes + 6 refusal probes)
- runner.py wraps _score_one_candidate_graph; byte-equal report.json
- 20/20 positive solved correct; 6/6 refusal probes refused typed;
solved_wrong == 0; overall_pass == True
Tests: 27/27 in 0.19s. 420 existing candidate-parser/math-parser/pack
tests still green. GSM8K probe safety rail (admitted_wrong == 0)
preserved.
Honest scope-limit (documented in ADR): admission_rate on the GSM8K
probe stays at 0/50 because (a) the probe currently consults the legacy
parser path, not the candidate-graph pipeline G.3 extends, and (b) most
money-bearing GSM8K cases fail first on verb (G.1) or multi-clause (G.4)
shape, not on the money literal. The axis lane is the load-bearing
measurement for this iteration. Reserved follow-up: a small probe-
infra ADR to switch run_coverage_probe.py to the candidate-graph
pipeline.
Out of scope, deferred to G.3.1: fractions end-to-end (resolver supports
N/M but no axis cases), multi-currency (¢ € £ ¥ ₱), space-separated
multi-word cardinals (one hundred), word-number-adjective compositions
(five full boxes).
Wire compare_additive / compare_multiplicative extractors into the
candidate-emitting sentence parser, closing the deferred phase flagged
at generate/math_candidate_parser.py:30.
Capability axis: comparatives (additive + multiplicative)
- generate/math_candidate_parser.py: new _compare_additive_candidates,
_compare_multiplicative_candidates, _compare_nested_candidates
emitting CandidateOperation records keyed to the four
Comparison.direction literals registered in ADR-0123.
- Closed-set anchor alternation; 'less' admitted as surface synonym of
'fewer'; reference slot widened to admit "the number/amount of <unit>"
for nested forms.
- Nested 'A has N more <unit> than M times <REF>' emits two flat
candidates (additive + multiplicative); binding-graph picks the
admissible composition or refuses (no solver stub).
Curated axis lane (24 cases)
- evals/math_capability_axes/G2_comparatives/v1/cases.jsonl:
8 additive / 8 multiplicative / 3 nested / 5 refusal
- evals/math_capability_axes/G2_comparatives/v1/runner.py +
report.json: deterministic, wrong==0 gate, byte-equal across runs.
Tests (21 new)
- tests/test_adr_0131_G2_comparatives.py: per-direction at-least-one
passing, nested-both-emitted, closed-set refusal, runner
byte-equality, GSM8K-probe gate (comparative-clause refusals
strictly decrease).
GSM8K-probe gate (chosen: comparative-clause refusals ↓)
- evals/gsm8k_math/train_sample/v1/report.json (candidate-graph
probe): comparative-clause refusal count 2 → 1 (case 0009 'Jen has
10 more ducks than four times the number of chickens' moves from
statement-clause refusal to question-layer refusal). admitted_wrong
remains 0; admission_rate unchanged (downstream composition is a
follow-up ADR).
- evals/gsm8k_math/train_sample/v1/train_sample_coverage_report.json
(legacy probe): refreshed, byte-identical (legacy parser untouched).
B3 + candidate-graph + GSM8K probe lanes all pass (90/90). Direction
vocab stays closed to {more, fewer, times, fraction}; wrong==0
preserved everywhere.
ADR-0131 deferred GSM8K because it rewards paraphrase flexibility,
which is the deterministic engine's structural weakness. This ADR
re-engages it on architecture-aligned terms: as a *coverage probe*
of the bounded grammar + binding graph, not a promotion gate.
The framing pinned by this ADR:
GSM8K is not a target. The model's capability is the target.
GSM8K passing is the symptom of capability, not the goal of
the work.
Wrong mindset (rejected by ADR's iteration discipline):
"Find templates that admit more GSM8K cases."
Right mindset (load-bearing):
"Extend the model's NL-to-typed-graph capability along
principled axes (verb classes, comparative structures, numeric
forms, multi-clause grammar). GSM8K admission rises as a
side effect alongside every other word-problem corpus."
Baseline pinned by this commit:
admission_rate: 0/50 = 0.0%
admitted_wrong: 0 (gate intact, safety rail bulletproof)
refused: 50/50 = 100.0%
Every refusal is a typed parser error citing the specific clause
that did not match a template. Zero crashes, zero confabulations
— refusal-first works perfectly at admission rate zero.
What's in this PR:
- ``docs/decisions/ADR-0131.G-gsm8k-coverage-probe.md``: the ADR.
Cites parents (ADR-0131, -0115/-0116/-0117, -0131.3, -0132..-0135).
Documents the capability-first iteration discipline that every
subsequent ADR-0131.G.<n> must follow:
1. Name a single capability axis the iteration extends
2. Add B3-style curated coverage cases (capability proves
itself OUTSIDE GSM8K)
3. Re-run both B3 lane + GSM8K probe; B3 must not regress
4. Reject any expansion that only moves GSM8K admission
- ``evals/gsm8k_math/train_sample/v1/run_coverage_probe.py``:
pure-adapter wrapper around the existing run_lane. Emits a
deterministic train_sample_coverage_report.json with metrics,
per-case outcomes, and the top refused-reason families (the
work queue for capability extension).
- ``evals/gsm8k_math/train_sample/v1/train_sample_coverage_report.json``:
the baseline report. Diff-able artifact every future iteration
moves.
- ``tests/test_adr_0131_G_gsm8k_coverage_probe.py``: 8 contract
tests pinning the safety rail (admitted_wrong == 0), typed
refusal invariant (every refused case has non-empty reason),
closed outcome vocabulary, deterministic replay, committed-
report matches fresh-run.
The promotion-gate composite (B1 + B2 + B3) is unaffected.
ADR-0131.4 still consumes those three. The GSM8K probe is
empirical context for honest external claims, not a gate.