feat(provenance,teaching): close the lateral gaps the assessment actually found

Squashes the arc's work into one commit; the workflow-file edit it originally
carried is excluded (see the end of this message).

## Lane 1 — Workbench recorded a proved answer as ungrounded

With deduction_serving_enabled ratified ON (ADR-0256), workbench/api.py's live
chat route builds a bare ChatRuntime(), so the deduction composer decides
Workbench turns and stamps grounding_source="deduction" — but
_coerce_grounding_source carried a hand-copied whitelist of the six pre-arc
labels and silently rewrote anything else to "none". The runtime comment
reasoned this was inert because "REPL turns do not flow through Workbench's
CognitivePipelineRecord path". True, and irrelevant: the traffic flows the
other way. Stale since 2026-07-24.

Scope is one field. workbench/api.py:818 prefers TurnEvent.epistemic_state,
which read epistemic_state_needed — honest. So the UNregistered path degraded
honestly while the hand-copied whitelist asserted a falsehood; a second copy of
a closed enum was worse than no copy. Hence registration AND derivation:
GROUNDING_SOURCES exposes the Literal's members, and the coercion reads it.
workbench-ui badges/tokens/snapshot follow; enumCoverage.test.ts forces atomicity.

## Lane 2 — the ratification ceremony

The discovery loop was instrumented but not closed. teaching/ratification.py
turns a reviewed decision into a chain record, a corpus commit, and a receipt.

Its design turns on one observation: _ratified_rows DROPS unadmissible rows
silently — correct when serving, a trap when ratifying, because the file grows,
the commit lands, and the band count does not move. So the ceremony refuses to
call an append a ratification until it has re-read the curriculum through the
real loader and seen the chain arrive; a non-admitted append is rolled back.
Validation is a pre-flight courtesy, admission is the proof.

Arena queue entry and ledger reseal are deliberately NOT performed (bridge rule
1); the receipt names them. Front door: `core proposal-queue ratify`, a sibling
of `review` rather than a flag on it.

## Lane 3 — structural closures

- ADR-0263 gains rule 5: absence policy is DECLARED in CAPABILITY_LEDGERS, not
  passed at the call site. An AST-matched test fails if a serving path passes
  missing_ok again.
- Deductive suite added WHOLE to the pre-push gate: 285 tests in 29s against
  smoke's 216 in 62s, so no coverage trade was needed.
- Smoke/CI parity assertion made bidirectional. It was one-directional, and had
  drifted.
- test_prior_surface_deduction_binding.py pins correction binding on the
  deduction path. The review's diagnosis did NOT reproduce — hash_surface moves
  in lockstep — so it pins what is there. Mutation-checked.
- Domain-keyed ADR index over 312 flat-numbered files, explicitly partial.
- Arc-close brief template, plus this arc's own brief filled in against it.

## Lanes 4 and 5 — two premises falsified by measurement, one of them mine

Math 4.2: baseline reproduced (correct=5 wrong=0 refused=495); all four named
cases traced to one seam with each gap isolated by one-variable probes. Then the
number that changes the recommendation: the gap blocking case 0000 affects 1
case in 500, the 'than' gap blocking 0001 affects 2. ADR-0251's prohibition on
per-case growth now rests on a count. No reader change made.

CGA: versor_condition is 0.22% of a turn, not the "~10x proof latency" I claimed
— that multiplied an isolated microbenchmark by a call count and compared it to
a single verdict's latency. The real cost is geometric_product at 33,986
calls/turn (~73%) via cga_inner in search paths. The obvious closed form is NOT
bit-exact (954/4000 in f32); backend.vault_recall's serial fold IS (3000/3000,
worst-rel 0) and is the correct target. cargo test could not run —
static.crates.io is denied by the sandbox network policy — so the Rust parity
question stays open and the typestate lane is carried forward, not shipped
uncompiled.

## Not landed: three lines owed to .github/workflows/smoke.yml

The CI smoke gate is narrower than the local one —
test_pack_draft_serve_boundary.py (ADR-0253 INV-33) has been local-only, unseen
because the parity pin checked one direction. The edit was authored and rejected
at push for lacking the `workflow` OAuth scope, so it is recorded as a named,
dated PENDING_IN_CI exception rather than dropped: the assertion still fires on
any new divergence, and a second guard fires once the three land.

[Verification]: pre-push gates all green — smoke 236 passed, warmed_session 10
passed, deductive 285 passed. Ratification 14, ADR index 5, CLI suites 10.
Grounding/epistemic sweep 741 passed 1 skipped. workbench-ui 598 passed across
73 files, tsc -b clean. capability index 11 passed, digest unchanged. Math
holdout correct=5 wrong=0 refused=495. Committed chain corpora byte-unchanged
after the tests that write to them.
Environment caveat: the repo pins requires-python ==3.12.13, which uv cannot
fetch for linux-x86_64, so `uv sync --locked` fails. All Python runs used a
scratch venv on 3.12.11 with declared deps — not the locked universe, not the
full ~12k suite. The pin was left untouched. Re-run on a 3.12.13 host before
treating this as merge evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FduW6Krm3PPQv3P5iwBYtx
This commit is contained in:
Claude 2026-07-25 04:51:15 +00:00
parent 676b6555b4
commit 71bf04fb44
No known key found for this signature in database
32 changed files with 2249 additions and 56 deletions

View file

@ -22,28 +22,34 @@ absent ledger reads as an empty table, so every answer is served DISCLOSED.
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from core.ratified_ledger import (
RatifiedLedgerError as RatifiedCurriculumLedgerError,
load_sealed_ledger,
ledger_spec,
load_capability_ledger,
serve_license,
)
from core.reliability_gate import Ceilings, ClassTally, LicenseDecision
_LEDGER_PATH = Path(__file__).resolve().parent / "data" / "curriculum_serve_ledger.json"
_LEDGER_CAPABILITY = "curriculum_serve"
_LEDGER_PATH = ledger_spec(_LEDGER_CAPABILITY).path
@lru_cache(maxsize=1)
def load_ratified_ledger() -> dict[str, ClassTally]:
"""Load + verify the ratified curriculum-serve ledger → per-band tallies.
An ABSENT ledger is not an error here: no curriculum band has earned
anything yet, and the honest reading of "no file" is "no committed
evidence", which the gate turns into a disclosed answer rather than a
withheld one.
An ABSENT ledger is not an error *for this capability*: no curriculum band
has earned anything yet, and the honest reading of "no file" is "no
committed evidence", which the gate turns into a disclosed answer rather
than a withheld one.
That policy is declared once in ``CAPABILITY_LEDGERS``, not asserted here
this module names the capability and inherits its registered absence
contract, so it cannot grant itself a softer failure mode than the bridge
recorded (ADR-0263 rule 5).
"""
return load_sealed_ledger(_LEDGER_PATH, missing_ok=True)
return load_capability_ledger(_LEDGER_CAPABILITY)
def curriculum_serve_license(

View file

@ -19,16 +19,17 @@ stay at the safe defaults (invariant #4 — the engine cannot raise its own bar)
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from core.ratified_ledger import (
RatifiedLedgerError,
load_sealed_ledger,
ledger_spec,
load_capability_ledger,
serve_license,
)
from core.reliability_gate import Ceilings, ClassTally, LicenseDecision
_LEDGER_PATH = Path(__file__).resolve().parent / "data" / "deduction_serve_ledger.json"
_LEDGER_CAPABILITY = "deduction_serve"
_LEDGER_PATH = ledger_spec(_LEDGER_CAPABILITY).path
@lru_cache(maxsize=1)
@ -38,8 +39,12 @@ def load_ratified_ledger() -> dict[str, ClassTally]:
Raises :class:`RatifiedLedgerError` if the file is absent/malformed or its
recomputed ``content_sha256`` does not match the committed one (tamper-evidence:
only the sealed-practice output is trusted, never a hand-edited ledger).
This capability is registered ``missing_ok=False`` in ``CAPABILITY_LEDGERS``
it ships with its ledger, so absence is a broken deployment, and that
verdict is the manifest's rather than this call's (ADR-0263 rule 5).
"""
return load_sealed_ledger(_LEDGER_PATH)
return load_capability_ledger(_LEDGER_CAPABILITY)
def deduction_serve_license(

View file

@ -468,17 +468,20 @@ class ChatResponse:
# "none" — universal "insufficient grounding" disclosure on stub.
# "deduction" — answer decided by the verified propositional
# entailment engine (deduction-serve arc, Phase 1;
# chat/deduction_surface.py). NOTE: this value is NOT
# yet registered in core.epistemic_state.GroundingSource
# (the Workbench-coupled closed Literal) or the Workbench
# UI badge contract — epistemic_state_for_grounding_source
# falls through to the honest EPISTEMIC_STATE_NEEDED
# default for it. core chat REPL turns do not flow
# through Workbench's CognitivePipelineRecord path, so
# this is inert today; registering "deduction" as a
# first-class GroundingSource + Workbench badge is
# deferred to a follow-up if/when that visibility is
# needed.
# chat/deduction_surface.py — ADR-0256).
# "curriculum" — answer decided from the ratified domain-chain
# curriculum of the question's subject
# (chat/curriculum_surface.py — ADR-0262).
# Both are registered in core.epistemic_state.GroundingSource (the
# Workbench-coupled closed Literal) and map to DECODED. That registration
# is load-bearing rather than cosmetic: workbench/api.py's live chat route
# builds a bare ChatRuntime(), so with deduction_serving_enabled ratified
# ON these composers decide Workbench turns too. While the labels were
# unregistered, the API's coercion floored them to "none" and recorded a
# proved answer as ungrounded (pinned by
# tests/test_workbench_deduction_provenance.py). Adding a grounding source
# here means registering it there in the same change — the workbench-ui
# enum-coverage test fails the build otherwise.
# The string is preserved verbatim in TurnEvent for downstream audit.
grounding_source: str = "none"
# ADR-0071 (R4) — pre-decoration surface. ``surface`` is the

View file

@ -95,6 +95,52 @@ def cmd_proposal_queue_review(args: argparse.Namespace) -> int:
return 0
def cmd_proposal_queue_ratify(args: argparse.Namespace) -> int:
"""The ratification ceremony (Lane 2) — a reviewed decision becomes artifacts.
Deliberately a sibling of ``review`` rather than a flag on it: ``review``
records that a human looked and must stay incapable of ratifying. This
command is the separate, explicit act, and it still ratifies nothing on its
own judgement the operator supplies the edge, the identity, and the
rationale, and the ceremony only enforces that the result is *routable*.
"""
from teaching.ratification import (
RatificationError,
build_chain_record,
ratify_chain,
)
try:
record = build_chain_record(
domain=args.domain,
subject=args.subject,
connective=args.connective,
obj=args.object,
reviewer=args.reviewer,
rationale=args.rationale,
intent=args.intent,
)
receipt = ratify_chain(record, dry_run=args.dry_run)
except RatificationError as exc:
print(f"REFUSED: {exc}")
return 1
if args.json:
print(json.dumps(receipt.as_dict(), indent=2, sort_keys=True))
return 0
verb = "would ratify" if args.dry_run else "RATIFIED"
print(f"{verb} {receipt.chain.chain_id}: "
f"{record.subject} {record.connective} {record.object}")
print(f" corpus : {receipt.corpus_id}")
print(f" domain chains : {receipt.chains_before} -> {receipt.chains_after}")
print(f" {record.operator_family} band : "
f"{receipt.family_chains_before} -> {receipt.family_chains_after}")
if not args.dry_run:
print(f" still pending : {', '.join(receipt.pending_stages)}")
return 0
def register(subparsers: argparse._SubParsersAction) -> None:
"""Attach the ``core proposal-queue`` subcommand tree to a top-level parser."""
queue = subparsers.add_parser(
@ -141,5 +187,28 @@ def register(subparsers: argparse._SubParsersAction) -> None:
review.add_argument("--note", default=None, help="optional free-text note")
review.set_defaults(func=cmd_proposal_queue_review)
ratify = sub.add_parser(
"ratify",
help="ratify one reviewed edge into the domain chain corpus (writes)",
description=(
"The ratification ceremony: validate -> append -> CONFIRM the "
"curriculum loader admits the chain -> receipt. Refuses, and rolls "
"back, if the appended row would be silently dropped. Does not "
"write a ledger (bridge rule 1) or queue an arena entry; the "
"receipt names those stages."
),
)
ratify.add_argument("domain")
ratify.add_argument("subject")
ratify.add_argument("connective")
ratify.add_argument("object")
ratify.add_argument("--reviewer", required=True, help="who ratified this")
ratify.add_argument("--rationale", required=True, help="why it is ratified")
ratify.add_argument("--intent", default="cause")
ratify.add_argument("--dry-run", action="store_true",
help="report the band delta without writing")
ratify.add_argument("--json", action="store_true")
ratify.set_defaults(func=cmd_proposal_queue_ratify)
__all__ = ["register"]

View file

@ -56,6 +56,17 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
# were conflated with an unrelated flake. Promoted so this axis can
# never silently regress again.
"tests/test_register_substantive_consumption.py",
# Deduction serving is LIVE (ADR-0256 ratified the flag ON), so its
# cross-stack provenance contract is a serving-path contract, not a
# capability-under-development one. Workbench's live chat route builds
# a bare ChatRuntime(), which means these composers decide Workbench
# turns too — and while their labels were unregistered the API coerced
# them to "none" and recorded a proved answer as ungrounded. ~3s.
"tests/test_workbench_deduction_provenance.py",
# Correction binding anchors to hash_surface, in lockstep with every
# substantive override of the served surface. Cheap, and the axis is
# invisible until it breaks silently.
"tests/test_prior_surface_deduction_binding.py",
),
"runtime": (
"tests/test_chat_runtime.py",

View file

@ -9,16 +9,44 @@ serialize stably into JSONL, metadata dictionaries, and test fixtures.
from __future__ import annotations
from enum import Enum, unique
from typing import Any, Literal
from typing import Any, Literal, get_args
GroundingSource = Literal["pack", "teaching", "vault", "partial", "oov", "none"]
GroundingSource = Literal[
"pack",
"teaching",
"vault",
"partial",
"oov",
"none",
"deduction",
"curriculum",
]
"""Ratified grounding-source labels.
Single source of truth for the values ``epistemic_state_for_grounding_source``
maps, the cold-start-grounding lane validates, and the Workbench UI badges
bind to (ADR-0162 §3d). Adding a value here without adding a corresponding
badge fails the build-time enum coverage test under ``workbench-ui/``.
maps, the cold-start-grounding lane validates, the Workbench API coerces
against, and the Workbench UI badges bind to (ADR-0162 §3d). Adding a value
here without adding a corresponding badge fails the build-time enum coverage
test under ``workbench-ui/``.
``deduction`` (ADR-0256) and ``curriculum`` (ADR-0262) joined the closed set
once deduction serving was ratified ON: ``workbench/api.py``'s live chat route
builds a bare ``ChatRuntime()``, so those composers decide Workbench turns and
stamp their label on the ``TurnEvent``. While the labels were unregistered the
API's coercion floored them to ``"none"`` — recording a proved answer as
ungrounded in a durable audit artifact. Registration is what makes the
recorded provenance true; ``GROUNDING_SOURCES`` below is what keeps any second
copy of this set from drifting away from it again.
"""
GROUNDING_SOURCES: frozenset[str] = frozenset(get_args(GroundingSource))
"""The runtime-iterable form of :data:`GroundingSource`.
A ``Literal`` is invisible at runtime, which is why consumers historically
restated its members by hand and why one of those restatements silently fell
behind. Anything that needs to *check* a grounding source reads this set;
nothing re-types the members.
"""
@ -133,7 +161,14 @@ def normative_detail_from_verdicts(verdicts: Any = None, *, safety_verdict: Any
def epistemic_state_for_grounding_source(source: str | None) -> EpistemicState:
"""Default runtime mapping for existing grounding-source labels."""
normalized = (source or "none").strip().lower()
if normalized in {"pack", "teaching", "vault"}:
# ``deduction`` / ``curriculum`` rank with the decoded sources rather than
# the evidenced ones: both are *decided* by the ROBDD entailment engine
# over ratified structure (an argument's own premises, or a subject's
# ratified curriculum), not retrieved as supporting evidence. Serving them
# authoritatively additionally requires a SERVE license on the sealed
# ledger — an unearned band is served DISCLOSED, which is a surface-level
# hedge and does not change how the turn was grounded.
if normalized in {"pack", "teaching", "vault", "deduction", "curriculum"}:
return EpistemicState.DECODED
if normalized == "partial":
return EpistemicState.EVIDENCED_INCOMPLETE
@ -145,6 +180,7 @@ def epistemic_state_for_grounding_source(source: str | None) -> EpistemicState:
__all__ = [
"GROUNDING_SOURCES",
"EpistemicState",
"GroundingSource",
"NormativeClearance",

View file

@ -35,6 +35,7 @@ identically and no lane pin moves.
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@ -52,6 +53,85 @@ class RatifiedLedgerError(ValueError):
"""A committed ledger is malformed or does not verify against its own hash."""
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
@dataclass(frozen=True, slots=True)
class LedgerSpec:
"""One capability's committed ledger, and whether its absence is an error."""
capability: str
path: Path
#: ``False`` — this capability SHIPS with a sealed ledger, so a missing file
#: means a broken deployment and the load must refuse.
#: ``True`` — this capability's practice volume is still being built, so a
#: missing file honestly means "nothing earned yet" and serves disclosed.
missing_ok: bool
note: str
#: Rule 5 of the bridge: **absence policy is declared, not passed.**
#:
#: Rules 1-4 (docstring above) are all enforced structurally; this one was not.
#: ``missing_ok`` began life as a ``load_sealed_ledger`` keyword, which meant
#: each adapter chose its own answer to "is a missing ledger a broken
#: deployment, or an unearned capability?" — a question about the capability,
#: not about the call. Any new subject onboarding through the bridge could pass
#: ``missing_ok=True`` and silently downgrade a should-be-hard-refuse into a
#: disclosed hedge, and nothing would catch it.
#:
#: Declaring it here means adding a capability is a manifest edit that a
#: reviewer reads as a policy change, which is what it is. The keyword survives
#: on the primitive for tests and one-off tooling; no production adapter passes
#: it.
CAPABILITY_LEDGERS: dict[str, LedgerSpec] = {
"estimation": LedgerSpec(
capability="estimation",
path=_PROJECT_ROOT / "generate" / "determine" / "data" / "estimation_ledger.json",
missing_ok=False,
note="ADR-0175 — ships sealed with the converse-estimation gate.",
),
"deduction_serve": LedgerSpec(
capability="deduction_serve",
path=_PROJECT_ROOT / "chat" / "data" / "deduction_serve_ledger.json",
missing_ok=False,
note="ADR-0256 — ships sealed; 25 bands at 720/720 wrong=0.",
),
"curriculum_serve": LedgerSpec(
capability="curriculum_serve",
path=_PROJECT_ROOT / "chat" / "data" / "curriculum_serve_ledger.json",
missing_ok=True,
note=(
"ADR-0262 §5 — no band has earned a license from present curriculum "
"volume; every served band is DISCLOSED. Flips to False when the "
"first band earns SERVE and the ledger is committed."
),
),
}
def ledger_spec(capability: str) -> LedgerSpec:
"""The declared spec for *capability*, or a refusal naming the manifest."""
try:
return CAPABILITY_LEDGERS[capability]
except KeyError:
raise RatifiedLedgerError(
f"unregistered ledger capability: {capability!r} — declare it in "
"core.ratified_ledger.CAPABILITY_LEDGERS before consuming it"
) from None
def load_capability_ledger(capability: str) -> dict[str, ClassTally]:
"""Load the committed ledger for *capability* under its declared policy.
The production entry point. A caller names what it is, not how absence
should be treated so no call site can grant itself a softer failure mode
than the capability was registered with.
"""
spec = ledger_spec(capability)
return load_sealed_ledger(spec.path, missing_ok=spec.missing_ok)
def tally_dict(tally: ClassTally) -> dict[str, Any]:
"""The committed per-class row. The field set is the contract — a reader
of an older ledger must be able to name every field it finds."""
@ -148,7 +228,11 @@ def serve_license(
__all__ = [
"CAPABILITY_LEDGERS",
"LedgerSpec",
"RatifiedLedgerError",
"ledger_spec",
"load_capability_ledger",
"load_sealed_ledger",
"seal_artifact",
"serve_license",

136
docs/adr/INDEX-by-domain.md Normal file
View file

@ -0,0 +1,136 @@
# ADR index, keyed by domain
`docs/adr/` holds 312 files under a flat, purely sequential numbering. Sequence
records *when* a decision was made and nothing about *what it governs*, so
answering "which ADRs constrain the serving flag contract?" or "what governs
the register axis?" currently means grepping. Past ~300 files that is how a
decision corpus becomes a write-only archive.
This index is the lightweight fix: a maintained table keyed by capability
domain and cross-referenced by the files each ADR actually governs. It is **not**
a renumbering, and it does not replace `README.md` (which owns the
Master-Blueprint collision reconciliation and the binding non-ADR notes).
## Scope, stated honestly
This index covers the **live serving, telemetry, and governance surfaces** — the
domains a Phase 5 / next-arc session needs to query. It is not a complete index
of all 312 ADRs, and it does not pretend to be: an index asserting coverage it
does not have is worse than none, because it converts "I should grep" into "I
already checked."
**Maintenance rule: index on mint.** Adding a row when you write an ADR is
cheap; back-filling 300 is not. Rows below carry the files each ADR governs so
a reader can jump from a decision to its enforcement site.
---
## Deduction serving (the deduction-serve arc)
| ADR | Decision | Governs |
|---|---|---|
| [0256](./ADR-0256-deduction-serve-earned-license.md) | Deduction serving governed by an **earned** reliability license; flag ratified ON 2026-07-24 | `chat/deduction_serve_license.py`, `chat/data/deduction_serve_ledger.json`, `core/config.py::deduction_serving_enabled` |
| [0257](./ADR-0257-english-clause-argument-band.md) | Band v2-EN — English-clause opaque-atom propositional serving; realizer-guard exemption for quoted templates | `generate/proof_chain/english.py`, `chat/runtime.py` (guard exemption sites) |
| [0258](./ADR-0258-member-chain-band.md) | Band v3-MEM — singular membership + universal premises | `generate/proof_chain/categorical.py` |
| [0259](./ADR-0259-conditional-membership-fusion-band.md) | Band v4-CM — conditional-membership fusion | `generate/proof_chain/cond_member.py` |
| [0260](./ADR-0260-verb-predicate-band.md) | Band v5-VP — verb-predicate arguments | `generate/proof_chain/verb.py` |
| [0261](./ADR-0261-existential-witness-band.md) | Band v6-EX — existential witnesses; anonymous witness never transfers to a named individual | `generate/proof_chain/exist.py` |
Engine underneath all six: `generate/proof_chain/entail.py` (ROBDD tautology
check). Surfaces: `generate/proof_chain/render.py`, `chat/deduction_surface.py`.
## Curriculum serving
| ADR | Decision | Governs |
|---|---|---|
| [0262](./ADR-0262-curriculum-grounded-serving.md) | Exams answered from the ratified curriculum, read OPEN-world; §5 records that curriculum **volume** is the binding constraint (~25× gap) | `chat/curriculum_surface.py`, `chat/curriculum_serve_license.py`, `evals/curriculum_serve/runner.py` |
## Reliability licensing and the ledger bridge
| ADR | Decision | Governs |
|---|---|---|
| [0175](./ADR-0175-calibrated-attempt-and-eliminate-learning.md) | Two regimes under wrong=0; θ_SERVE Wilson floor; an engine cannot raise its own bar | `core/reliability_gate.py`, `generate/determine/estimation_license.py` |
| [0263](./ADR-0263-ratified-ledger-bridge.md) | Seal → ratify → SHA-verify → serve-gate, extracted from three instances. **Rule 5** (2026-07-25): absence policy is declared in `CAPABILITY_LEDGERS`, not passed at the call site | `core/ratified_ledger.py`, all three license adapters |
| [0206](./ADR-0206-response-governance-bridge.md) | Response governance bridge (`govern_response` / `shape_surface`) | `generate/determine/` |
## Grounding sources and the epistemic taxonomy
| ADR | Decision | Governs |
|---|---|---|
| [0142](./ADR-0142-epistemic-state-taxonomy.md) | Epistemic state as first-class vocabulary | `core/epistemic_state.py` |
| [0048](./ADR-0048-pack-grounded-surface.md) / [0050](./ADR-0050-pack-grounded-comparison.md) | Pack-grounded cold-start DEFINITION / RECALL / COMPARISON | `chat/pack_grounding.py` |
| [0052](./ADR-0052-teaching-grounded-surface.md) | Teaching-grounded cold-start CAUSE / VERIFICATION | `chat/teaching_grounding.py` |
| [0064](./ADR-0064-cross-pack-teaching-chains.md) | Cross-pack residency + cross-corpus chain lookup for the discovery gate | `teaching/discovery.py` |
The closed set itself lives at `core/epistemic_state.py::GroundingSource`.
Registration is a three-part cross-stack change — see
`docs/specs/runtime_contracts.md` § "Grounding-source registration".
## Register axis and the realizer guard
| ADR | Decision | Governs |
|---|---|---|
| [0069](./ADR-0069-realizer-register-parameter.md) | Realizer register parameter; **invariant C** — register must not move `trace_hash` | `core/cognition/surface_resolution.py`, `core/cognition/pipeline.py` |
| [0071](./ADR-0071-seeded-surface-variation.md) | Seeded surface variation + discourse markers; `pre_decoration_surface` | `chat/register_variation.py` |
| [0075](./ADR-0075-realizer-slot-type-guard.md) | Realizer slot-type guard (C1 coherence floor). Deduction surfaces are **exempt** — quoted templates are not slot-composed articulations (rationale documented at both guard sites) | `chat/runtime.py` (stub + main guard sites) |
| [0077](./ADR-0077-substantive-register-knobs.md) | Substantive register knobs (R6); `register_canonical_surface` | `chat/register_substantive.py` |
## Telemetry, trace, and audit
| ADR | Decision | Governs |
|---|---|---|
| [0039](./ADR-0039-audit-completeness.md) | `TurnVerdicts` bundle, stub-path `TurnEvent`, `hedge_injected` | `chat/runtime.py` |
| [0153](./ADR-0153-turn-event-trace-hash-backstamp.md) | `TurnEvent` `trace_hash` back-stamp (W-020a) | `core/cognition/pipeline.py`, `chat/runtime.py` |
| [0255](./ADR-0255-discovery-yield-baseline-telemetry.md) | Discovery-yield-per-served-turn baseline; fail-closed on a missing baseline | `teaching/discovery_yield.py` |
| [0018](./ADR-0018-tool-use-scope.md) | Typed deterministic operators folded into `trace_hash` | `core/cognition/trace.py` |
`trace_hash` is **not** reconstructable from `TurnEvent` — see
`docs/specs/runtime_contracts.md`. Audit-ledger **R7** (telemetry sink receives
the pre-override event) is open and recorded there.
## Workbench and the cross-stack UI contract
| ADR | Decision | Governs |
|---|---|---|
| [0160](./ADR-0160-core-workbench-v1.md) | Operator/auditor UI before public chat | `workbench/` |
| [0161](./ADR-0161-hitl-async-queue.md) | HITL async queue | `teaching/proposals.py`, `core proposal-queue` CLI |
| [0162](./ADR-0162-workbench-design-system.md) | Design system v1; **§3d** — engine enums bind to UI badges, enforced by a build-time coverage test | `workbench-ui/src/design/`, `workbench-ui/enum-snapshot.json` |
## Teaching, proposals, and review
| ADR | Decision | Governs |
|---|---|---|
| [0057](./ADR-0057-teaching-chain-proposal-review.md) | Teaching-chain proposal + review + replay-equivalence gate | `teaching/proposals.py`, `teaching/review.py`, `teaching/store.py` |
| [0254](./ADR-0254-grounded-open-hedge-arm.md) | Grounded-open hedge arm for the shadow-coherence gate | `chat/runtime.py` |
## Reader arc / math capability
| ADR | Decision | Governs |
|---|---|---|
| [0251](./ADR-0251-reader-arc-recalibration-geometric-normalization-spike-proposal.md) | **Halt bespoke per-case regex work**; clean-base reset. The prohibition that governs every math reader increment | `generate/math_roundtrip.py`, `generate/math_candidate_parser.py` |
Companion evidence: `docs/research/reader-arc-overfit-inventory-2026-07-19.md`,
`docs/research/math-reader-phase-4-1-status-2026-07-24.md` (Phase 4.1 =
complete-with-null-yield).
## Substrate, field, and packs
| ADR | Decision | Governs |
|---|---|---|
| [0180](./ADR-0180-crdt-sharded-vault-concurrency.md) | Delta-CRDT sharded substrate | `core-rs/src/vault.rs`, `engine_state/` |
| [0243](./ADR-0243-wave-field-cognitive-lifecycle-comprehension-reasoning-and-resonant-learning.md) | Wave-field cognitive lifecycle | `field/` |
| [0244](./ADR-0244-wave-field-identity-manifold-and-inalienable-geometric-alignment.md) | Wave-field identity manifold | `persona/`, `chat/runtime.py` |
| [0253](./ADR-0253-master-blueprint-adr-collision-and-dual-pack-boundary.md) | Blueprint collision resolution + dual-pack serve boundary (INV-33) | `packs/` |
---
## Numbering note
The corpus is at ADR-0263 and minted eight ADRs in roughly 48 hours during the
generalization arc. Sequential numbering is fine and should stay — the cost it
imposes is discoverability, which is what this file pays down. ADR-0300 is a
near-term milestone with no structural meaning; do not treat it as one.
The ADR-0206/ADR-0256 numbering collision that had to be resolved in doc form
is the concrete failure this index is meant to make less likely: a domain-keyed
view shows an in-flight number before it is minted twice.

View file

@ -0,0 +1,107 @@
# Arc-Close Brief — TEMPLATE
> Copy to `docs/handoff/<arc-slug>/BRIEF-CLOSE-<arc-slug>.md` when an arc ends.
> Delete every instruction block (`>`) as you fill it in.
## Why this template exists
The handoff system is mature in one direction only. `BRIEF-O` / `BRIEF-S` are
**downward** briefs: a planning tier telling an execution tier what to build.
Nothing goes back **up**. When an arc closes, the tier that actually executed it
holds the corrected ground truth — which assumptions were falsified, what the
flag state now is, which "next steps" the measurements killed — and that
knowledge currently survives only as commit messages and research docs, which
the next arc's opener must reconstruct by re-reading every ADR from the arc's
first number onward.
The generalization arc minted eight ADRs in roughly 48 hours. That is the
cadence this template is sized for: an opener should be able to start scoping
from **this one file** and reach for ADRs on demand, not as a prerequisite.
The single most important section is **Falsified assumptions**. A downward brief
states a plan; an arc-close brief states which parts of that plan turned out to
be wrong. That is the part which cannot be recovered from the diff.
---
# Arc-Close Brief — <arc name>
**Arc:** <slug> · **Closed:** <YYYY-MM-DD> · **Closing tier:** <Fable/Opus/Sonnet + risk tier>
**Plan of record:** `docs/plans/<plan>.md`
**ADRs minted:** <ADR-NNNN ADR-NNNN>
## 1. What shipped
> One line per landed capability, each naming its enforcement site. Not commit
> messages — capabilities. A reader should be able to tell what the system can
> now do that it could not before.
| Capability | ADR | Enforcement site | Evidence |
|---|---|---|---|
| | | | |
## 2. Flag state at close
> The single highest-value table here. Every flag the arc touched, its default
> **now**, and what ratified it. A flag flipped ON is a live capability whose
> test suite is a regression suite — say so.
| Flag | Default at close | Ratified by | Notes |
|---|---|---|---|
| | | | |
## 3. Falsified assumptions
> **Do not skip this section.** For each: what the plan assumed, what the
> measurement showed, and where the evidence lives. An assumption that was
> merely *not reached* is not falsified — say "untested" and mean it.
| Plan assumed | Measurement showed | Evidence |
|---|---|---|
| | | |
## 4. Binding constraint going into the next arc
> One paragraph. Not a list. If the next arc has one thing standing between it
> and progress, name it and quantify it. "Curriculum volume: every band is
> 24×73× short of the entailed-bucket floor" is a binding constraint;
> "we should improve coverage" is not.
## 5. Trigger state
> For any phase the plan declared trigger-gated: is the trigger now met? Cite
> what met it. If met, say what the next tier must design **before** writing
> code — the design work that should land in a higher tier than the
> implementation.
## 6. What is NOT next, and why
> The most expensive thing an opener can do is resume a thread the closing tier
> already killed. List the plausible-looking next steps that the arc's own
> evidence rules out, each with its reason. Be specific enough that someone
> cannot re-propose them without engaging the evidence.
## 7. Open items carried forward
| Item | Kind | Where it is recorded |
|---|---|---|
| | deferred / blocked / untested | |
## 8. Gates and their state at close
> Local-first is the merge bar (`AGENTS.md`), so record what was actually run,
> with counts. "Green" without a count is not evidence.
| Gate | Command | Result at close |
|---|---|---|
| smoke | `uv run core test --suite smoke -q` | |
| deductive | `uv run core test --suite deductive -q` | |
| lane pins | | |
## 9. Ground truth corrections for the next opener
> Free text. The things you would say out loud if you could brief the next
> session in person — including anything in the plan of record that is now
> stale. If a comment in the code reasons from a premise that has since
> changed, name the file and line: that is exactly the class of staleness
> nobody greps for.

View file

@ -0,0 +1,161 @@
# Arc-Close Brief — Assessment Verification & Lateral Closures
**Arc:** assessment-verification-2026-07-25 · **Closed:** 2026-07-25 · **Closing tier:** Opus 5
**Plan of record:** independent verification of an external Tier-S assessment + its five-pillar blueprint
**ADRs minted:** none (one amendment: ADR-0263 gains rule 5, recorded in `core/ratified_ledger.py`)
> First use of `ARC-CLOSE-TEMPLATE.md`, written in the same arc. Filling it in
> was also the test of it.
## 1. What shipped
| Capability | Enforcement site | Evidence |
|---|---|---|
| `deduction`/`curriculum` registered as grounding sources, cross-stack | `core/epistemic_state.py`, `workbench-ui/` badges + snapshot | `tests/test_workbench_deduction_provenance.py` (14) |
| Coercion derives its whitelist from the enum instead of restating it | `workbench/api.py::_coerce_grounding_source` | same file, parametrized over the registration |
| Ledger absence policy declared, not passed (ADR-0263 rule 5) | `core/ratified_ledger.py::CAPABILITY_LEDGERS` | `tests/test_ratified_ledger_bridge.py::TestCapabilityManifest` |
| Deductive suite in the pre-push gate | `scripts/hooks/pre-push` | measured 285 tests / 29s vs smoke 216 / 62s |
| Smoke↔CI parity assertion made bidirectional | `tests/test_cli_test_suites.py` | mutation-checked; carries a dated `PENDING_IN_CI` list — see §7 |
| Correction-binding lockstep pinned on the deduction path | `tests/test_prior_surface_deduction_binding.py` (6) | mutation-checked |
| Ratification ceremony + `core proposal-queue ratify` | `teaching/ratification.py`, `core/cli_proposal_queue.py` | `tests/test_ratification_ceremony.py` (14) |
| Domain-keyed ADR index | `docs/adr/INDEX-by-domain.md` | `tests/test_adr_index.py` (5) |
| Arc-close brief template | `docs/handoff/ARC-CLOSE-TEMPLATE.md` | this file |
## 2. Flag state at close
| Flag | Default at close | Ratified by | Notes |
|---|---|---|---|
| `deduction_serving_enabled` | **True** | ADR-0256, 2026-07-24 | Unchanged. Its suite is now a pre-push gate, not async CI. |
| `curriculum_serving_enabled` | False | — | Unchanged. Blocked on volume, not machinery; the ceremony is the volume path. |
| `accrue_realized_knowledge` | False | — | Unchanged, and **not** debt: a deployment profile (session memory), ON in production L10. |
No flag was flipped in this arc.
## 3. Falsified assumptions
| The assessment assumed | Measurement showed | Evidence |
|---|---|---|
| `parents[n]` audit is high-payoff | 168 sites, **0** outside the repo root | audit run |
| Guard exemption undocumented | Documented at **both** guard sites with rationale | `chat/runtime.py:2371`, `:3007` |
| T12 ledger entry missing | Present as an open pattern-watch item | `weekly-audit-2026-07-22-stragglers-todo.md:170` |
| `assert_corpus_sound` has one impl | Already domain-parameterized, called unconditionally | `evals/curriculum_serve/runner.py:71,105-108` |
| Promotion oracle must be built | Already the `wrong=0` lane gate | `tier-s-housekeeping-2026-07-24.md` §3 |
| `EntailmentTrace` drops proof chains | ROBDD tautology check; **no intermediate steps exist** | `generate/proof_chain/entail.py:125-190` |
| Cross-subject proof could emerge accidentally | `resolve_domain` refuses `ambiguous_reading` | `chat/curriculum_surface.py:138` |
| Second subject arena is next | 11 bands across 4 subjects already route; all 24×73× short | `curriculum-volume-quantification-2026-07-24.md` §2 |
| **(mine)** `versor_condition` is the hot spot | **0.22%** of a turn | `cga-hot-path-measurement-2026-07-25.md` §2 |
| **(mine)** Workbench also falsifies `epistemic_state` | It does not — reads `epistemic_state_needed`, honest | `workbench/api.py:818` |
## 4. Binding constraint going into the next arc
Unchanged and unmoved: **ratified curriculum volume.** Eleven live bands, every
one 24×73× short of the entailed-bucket floor, and physics — the deepest — has
16 chains against a ~219 floor. This arc built the machine that converts a
reviewed decision into a routable chain and proved it moves the band
(`causal: 7 → 8` on a dry run), but it ratified no content. The ceremony is a
pipe; someone still has to put curriculum through it. Nothing else in the
roadmap unblocks until that number moves.
## 5. Trigger state
**Core Logos Phase 5 (multi-step articulation) is NOT triggered**, contrary to
the assessment's reading. The trigger was described as "the renderer discards
intermediate proof chains" — there are none to discard.
`evaluate_entailment_with_trace` decides by asking whether
`(P1 & … & Pn) -> Q` is a tautology; `EntailmentTrace` carries five opaque BDD
node keys.
What must land in a higher tier **before** any implementation: a feasibility
study for **proof-term extraction** over the ROBDD — a derivation calculus with
a real soundness surface, not a data-structure refactor. Scoping it as the
latter produces a renderer with nothing to render.
## 6. What is NOT next, and why
- **Widening the math reader for cases 0000/0001/0148/0082.** Traced; their
blocking gaps affect **1 and 2 cases out of 500**. ADR-0251's prohibition now
rests on a count.
- **A `.metal` Cl(4,1) kernel / MLX fusion / bf16.** The Cayley table already
exists at compile time in `cl41.rs`; the fusion target is 0.22% of a turn;
bf16's ε ≈ 7.8e-3 against a 1e-6 gate. And there is an exact CPU win
available first.
- **The diagonal `cga_inner` shortcut.** Tempting, 32× fewer ops, and **not
bit-exact** (954/4000 in f32). It would move lane pins.
- **Discovery-yield stratification.** Blocked on new persisted per-source
counters in the engine_state manifest.
- **Band-level capability index.** Blocked on a scoring decision — 25 near-1.0
entries inflate `coverage_geomean` against the index's own anti-gaming claim.
- **`engine_refused` as a standing gate.** Classifier already CI-gated; pinning
a count on an empty bucket is the stale-pin failure mode.
## 7. Open items carried forward
| Item | Kind | Recorded |
|---|---|---|
| Does `core_rs` hold bit-exact parity? | blocked (network policy denies `static.crates.io`) | `cga-hot-path-measurement-2026-07-25.md` §7 |
| Rust typestate `UnverifiedClaim`→`VersorClaim` | blocked on a build | same, §7 |
| Serial-fold `cga_inner` in the search paths | untested (exactness proven, change not made) | same, §6 |
| Audit-ledger **R7** — telemetry sink gets pre-override events | deferred | `docs/specs/runtime_contracts.md` |
| `hash_surface` on `TurnEvent`, or not | undecided | same |
| Arena queue entry + ledger reseal after ratification | deferred by design (bridge rule 1) | `RatificationReceipt.pending_stages` |
| Forgejo zero-length-object claim | unverified from this environment | assessment doc, preamble |
| **Three files owed to `smoke.yml`** | blocked (push credential lacks `workflow` OAuth scope) | `tests/test_cli_test_suites.py::PENDING_IN_CI` |
**On that last row — the one thing this arc found and could not finish.** The
CI smoke gate is narrower than the local one. `test_pack_draft_serve_boundary.py`
(ADR-0253 dual-pack boundary, INV-33) has been in the local pre-push gate and
absent from `smoke.yml`, and the parity pin only ever checked the *other*
direction, so it went unseen. The two new deduction-provenance files belong
there for the same reason. The three-line edit was authored and rejected at push
(`refusing to allow an OAuth App to create or update workflow smoke.yml without
workflow scope`), so it is recorded as a named, dated exception rather than
silently dropped: the assertion still fires on any *new* divergence, and a
second guard fires once the three land so the list cannot rot. Anyone pushing
with `workflow` scope closes it by adding three lines and emptying the tuple.
## 8. Gates and their state at close
| Gate | Command | Result |
|---|---|---|
| smoke | `core test --suite smoke -q` | **236 passed** (216 baseline + 20 new) |
| deductive | `core test --suite deductive -q` | **285 passed** |
| workbench-ui | `vitest run` | **598 passed / 73 files**; `tsc -b` clean |
| capability index | `pytest tests/test_capability_*` | **11 passed**, baseline digest unchanged |
| math holdout | `evals/gsm8k_math/holdout_dev/v1/runner.py` | `correct=5 wrong=0 refused=495` |
| proposal/curriculum sweep | `pytest -k "proposal_queue or ratification or curriculum"` | **246 passed** |
| grounding/epistemic sweep | `pytest -k "workbench or epistemic or grounding or …"` | **741 passed, 1 skipped** |
**Environment caveat, and it matters for how much these are worth.** The repo
pins `requires-python == 3.12.13`, which `uv` cannot fetch for
linux-x86_64, so `uv sync --locked` fails. All Python runs above used a scratch
venv on 3.12.11 with the declared deps — **not the locked dependency universe**,
and not the full ~12k suite. The pin was left untouched. Re-run the real gates
on a 3.12.13 host before treating any of this as merged-quality evidence.
## 9. Ground truth corrections for the next opener
1. **`chat/runtime.py:471-481` was stale and is now rewritten.** It argued the
unregistered `deduction` label was "inert today" because REPL turns do not
reach Workbench. True, and irrelevant — Workbench builds a bare
`ChatRuntime()`, so the traffic flows the other way. Live from the moment
the flag was ratified ON. This is the class of staleness nobody greps for:
a comment reasoning from a premise that has since changed.
2. **A second copy of a closed enum is worse than no copy.** The *unregistered*
path degraded honestly (`epistemic_state_needed` = "needs determination").
The hand-copied whitelist asserted a falsehood (`none` = ungrounded). Derive
from the enum; do not restate it.
3. **Silent-drop is correct at serving time and a trap at ratification time.**
`_ratified_rows` dropping unadmissible rows is right when serving. Appending
a row the loader will drop looks identical to appending one that works. Any
future corpus-writing path needs the same confirm-admission discipline.
4. **Do not compare a microbenchmark to a latency figure.** §3's last two rows
are both mine, and both came from reasoning about measurements instead of
taking them. The turn is the denominator.
5. **`assert_corpus_sound` is two functions with one name** —
`evals/deduction_serve/practice/gold.py:1163` (no args, practice corpus) and
`evals/curriculum_serve/runner.py:71` (`domain, cases`, lane contract). Not
a missing contract; a name collision. Worth renaming before a third appears.

View file

@ -142,10 +142,19 @@ that has not switched on the CPU kernel it already wrote — `core-rs/src/cl41.r
the Cl(4,1) Cayley table at compile time via bitmask anticommutation-parity and metric contraction
(`[u8;1024]` index + `[i8;1024]` sign), which is its own recommendation 1 minus the GPU.
Meanwhile `chat/runtime.py` calls `versor_condition(field_state.F)` three times per turn
(`:2313`, `:2464`, `:2514`) at a measured Python p50 of 0.536 ms — ≈1.6 ms/turn against a
FrameVerdict TTFV of 0.151 ms. The per-turn geometric cost is ~10× the entire proof latency, and
the remedy is already written and merged.
> **Corrected 2026-07-25 by measurement — see
> `cga-hot-path-measurement-2026-07-25.md`.** This section originally argued that
> `versor_condition` (3 calls/turn × the benchmark's `p50 = 0.536 ms`) was "~10× the entire proof
> latency". That was wrong: it multiplied an *isolated microbenchmark* by a call count and
> compared the product against `FrameVerdict` TTFV, which is a single verdict's latency rather
> than a turn's. Measured directly, `versor_condition` is **0.448 ms/turn against a 200 ms turn —
> 0.22%**, so switching the Rust backend on recovers essentially nothing through that path.
>
> The profile does show CGA dominating (~73% of turn time), but through
> `cga_inner``geometric_product` (33,986 calls/turn) in nearest-neighbour and salience search,
> not through the versor invariant. Both this assessment and the hardware blueprint aimed at the
> wrong function. The correct target, and why the obvious shortcut is not bit-safe, are in the
> measurement document.
| Proposal | Verdict |
|---|---|
@ -156,8 +165,9 @@ the remedy is already written and merged.
| bfloat16 asymmetric precision routing | **Reject as specified.** bf16 ε ≈ 7.8e-3 — four orders coarser than the 1e-6 gate; upcasting inside the shader cannot recover bits lost at storage. `cga.rs:embed_point_raw` computes `0.5*(r²∓1)`, cancellation-prone at bf16 for r ≈ 1 — exactly the unit-normalized regime. |
The actionable question the document missed: **why is `CORE_BACKEND=rust` not the default?**
Establish whether `core_rs` still holds bit-exact parity; if it does, flipping the default is the
order-of-magnitude win being reached for.
Establish whether `core_rs` still holds bit-exact parity. (Attempted 2026-07-25 and **blocked**:
`cargo` cannot reach `static.crates.io` under the sandbox network policy. The question stays open
— the measurement above just removes the urgency the performance argument was supplying.)
None of this is on the critical path of the arc that just closed — FrameVerdict TTFV is 0.151 ms
with `producer=proof_chain.entail`, and the deduction/curriculum serving path is pure-Python ROBDD

View file

@ -0,0 +1,176 @@
# Where the per-turn CGA cost actually is — measured, 2026-07-25
**Result:** the substrate-performance premise in circulation (mine and an
external hardware blueprint's) was wrong. `versor_condition` is **0.22%** of a
turn. The real cost is `cga_inner``geometric_product` at **33,986 calls per
turn, ~73% of turn time**, driven by nearest-neighbour and salience search.
The obvious algebraic shortcut is **not** bit-safe and must not ship. A
different, already-ratified pattern in this repo **is** bit-exact and is the
correct target.
## 1. What was claimed
Two claims were in play going into this measurement, and both were reasoned
from microbenchmarks rather than from a turn:
1. An external Apple-Silicon blueprint proposed custom `.metal` Cl(4,1)
kernels, SoA layouts, MLX kernel-fusion of the versor invariant, and
bfloat16 routing — arguing the CPU is the bottleneck.
2. My own assessment claimed `versor_condition` is the hot spot: three calls
per turn × the benchmark report's `p50 = 0.536 ms` ≈ 1.6 ms/turn, "~10× the
entire proof latency."
Claim 2 is **wrong**, and it is worth naming the error precisely because it is
easy to repeat: it multiplies an *isolated microbenchmark* by a call count and
compares the product against `FrameVerdict` TTFV (0.151 ms) — which is the
latency of a single proof verdict, not of a turn. Those are not comparable
quantities. The correct denominator is the turn.
## 2. Direct measurement
`versor_condition` wrapped in a counter, three real `ChatRuntime.chat()` turns:
```
turns : 3
versor_condition calls : 9 (3.0 per turn)
time inside it : 1.34 ms total (0.448 ms/turn)
total chat() wall : 601.5 ms (200.5 ms/turn)
share of turn : 0.22%
```
The call count (3/turn) was right. The cost conclusion was not. **Switching the
Rust backend on would recover 0.22% of a turn through this path.** That is not
a reason to do it, and it removes the only quantified justification the
substrate track had.
## 3. Where the time actually goes
`cProfile`, three turns, post-warmup:
| calls | tottime | cumtime | function |
|---:|---:|---:|---|
| 3 | 0.001 | 2.242 | `chat/runtime.py::chat` |
| **33,986** | **1.631** | 1.656 | **`algebra/cl41.py::geometric_product`** |
| 16,418 | 0.029 | 1.608 | `algebra/cga.py::cga_inner` |
| 24 | 0.009 | 1.156 | `generate/proposition.py::_nearest_by_cga` |
| 3 | 0.023 | 0.626 | `generate/salience.py::compute` |
| 4,577 | 0.003 | 0.458 | `algebra/backend.py::cga_inner` |
So the CGA algebra **is** the dominant per-turn cost — roughly 73% — but
through `cga_inner` in nearest-neighbour and salience search, not through the
versor invariant. Both documents aimed at the wrong function.
The multiplier is structural. `algebra/cga.py::cga_inner` is:
```python
XY = geometric_product(X, Y)
YX = geometric_product(Y, X)
return 0.5 * scalar_part(XY + YX)
```
Two full 32×32 products — ~2,048 multiply-adds — to read **one scalar**. At
16,418 calls per turn that is the entire profile.
## 4. The tempting shortcut, and why it must not ship
`algebra/backend.py` already documents the closed form: for Cl(p,q) basis
blades `e_i * e_j` is scalar only when `i == j`, so
```
cga_inner(X, Y) == sum_i metric[i] * X[i] * Y[i]
```
32 multiply-adds instead of 2,048. Tested against the scalar path, 4,000 random
pairs per dtype, using `np.sum`:
```
float32: bit-exact 954/4000 worst-rel 1.037e-04
float64: bit-exact 929/4000 worst-rel 2.283e-13
```
**Not bit-exact.** A 1e-4 relative divergence in f32 would move surfaces that
`trace_hash` folds, break the committed lane SHA pins, and violate the
Rust/Python parity contract (`core-rs/tests/test_crdt_hash_parity.rs`). This is
the same class of error as proposing bfloat16 against a `1e-6` versor gate:
mathematically identical, numerically not.
## 5. The pattern that *is* exact
The divergence above is not the identity's fault — it is the *reduction order*.
`np.sum` reduces pairwise. `algebra/backend.py::vault_recall` folds serially in
component order:
```python
scores = np.zeros(M.shape[0], dtype=np.float32)
for i in range(M.shape[1]):
scores += (_CGA_INNER_METRIC[i] * M[:, i]) * q[i]
```
Its docstring claims this is "bit-identical to the scalar `cga_inner` path
because the per-versor sum is folded in the same serial component order
(ADR-0019 Stage 1)". Tested directly, 3,000 random f32 pairs:
```
serial-fold vs scalar cga_inner (f32): bit-exact 3000/3000 worst-rel 0.000e+00
```
**The claim holds exactly.** So this repo already contains a proven, ratified,
bit-exact vectorization of the very operation that dominates the profile —
scoring one query against N versors without a Python-level product loop.
## 6. The actual next step
Apply the `vault_recall` serial-fold pattern to the nearest-neighbour search
paths that dominate the profile — `generate/proposition.py::_nearest_by_cga`
and `generate/salience.py::compute` — which today call scalar `cga_inner` in a
Python loop over candidates.
Why this is the right target:
- **No GPU, no Metal, no MLX, no Rust, no new numerics.** Nothing in the
determinism doctrine has to move.
- **The exactness is proven, not assumed** (§5), and the acceptance test is
falsifiable and cheap: N turns before/after must produce byte-identical
surfaces and identical `trace_hash` values.
- It attacks 73% of turn time rather than 0.22%.
**Not done in this session, deliberately.** These functions feed articulation,
and the local-first merge bar is the full suite; this session could run curated
suites only (see §7). The measurement and the exactness proof are the expensive
parts and they are done — the change itself should land where it can be gated
properly.
## 7. What could not be verified here, and why
`cargo test` in `core-rs/` **could not run**: the sandbox network policy denies
`static.crates.io` (the proxy's `noProxy` list covers `index.crates.io`, so the
sparse index resolves but crate tarballs 403 at the gateway). No crates are
cached. So:
- **"Does `core_rs` still hold bit-exact parity?"** — the question the plan put
first, and the one the hardware blueprint skipped — remains **open**. It is
still the right question; §2 just removes the urgency the performance
argument was supplying.
- **The Rust typestate lane (`UnverifiedClaim` → `VersorClaim`)** was not
written. It is still the strongest of the blueprint's five proposals — the
only one with no determinism cost, and it needs no GPU — but shipping Rust
that cannot be compiled or tested in the session that wrote it is not
something to do. It carries forward unchanged.
Python measurements above ran on a scratch venv at 3.12.11; the repo pins
`==3.12.13`, which `uv` cannot fetch for linux-x86_64. The pin was left
untouched.
## 8. Standing verdicts on the blueprint's five items
Unchanged by this measurement except where noted.
| Proposal | Verdict |
|---|---|
| Rust typestate | **Adopt** — carried forward, blocked only on a build. |
| SoA layouts | **Resequence** — and now further down: the copy boundary is not what the profile shows. |
| Custom sparse `.metal` Cl(4,1) kernel | **Premature**, and now *doubly* so: there is a bit-exact CPU win (§56) available before any GPU question is live. |
| MLX lazy kernel fusion of the versor invariant | **Rejected on target selection**, additionally to the doctrine objection: the function it fuses is 0.22% of a turn. |
| bfloat16 asymmetric precision | **Reject** — §4 is the empirical form of the same argument at a *much* coarser epsilon. |
Relates to [[project-generalization-arc]].

View file

@ -0,0 +1,174 @@
# Math Phase 4.2 — case-first traces for 0000 / 0001 / 0148 / 0082
**Baseline reproduced:** `holdout_dev: correct=5 wrong=0 refused=495 (n=500)`
unchanged from the 2026-07-24 measurement, five days and one arc later.
**Deliverable:** the per-case trace `math-reader-phase-4-1-status-2026-07-24.md`
asked for, plus a corpus-frequency measurement that **changes the
recommendation**. All four named cases are blocked by surface gaps that are
among the *rarest* in the 500. Building for them is the overfit move ADR-0251
prohibits — and this document is the first time that has been quantified rather
than argued.
**Nothing was changed in the reader.** See §5.
## 1. Where all four refuse — one seam
Every one of the four fails at the same place, with the same shape of message:
```
candidate_graph: recognizer matched but produced no injection for statement:
'<first statement>' (category=...)
```
Traced through `generate.recognizer_match.match(statement, registry)`:
| case | category assigned | `parsed_anchors` | injection |
|---|---|---|---|
| 0000 | `DISCRETE_COUNT_STATEMENT` | `()` | `()` |
| 0001 | `DISCRETE_COUNT_STATEMENT` | `()` | `()` |
| 0148 | `DESCRIPTIVE_SETUP_NO_QUANTITY` | `()` | `()` |
| 0082 | `DISCRETE_COUNT_STATEMENT` | `()` | `()` |
So the recognizer **classifies** each statement and then extracts **no grounded
anchor**. `_is_comparative_multiplicative_v1_surface` is `False` for all four,
so none of them ever reaches `inject_comparative_multiplicative` — the injector
that would handle "twice as many X as Y". The gate is
`_COMPARE_MULT_ANCHOR_RE` (`generate/math_candidate_parser.py:1229`), anchored
`^…$`, so it must match the **whole** statement:
```
^(?P<actor>ENTITY)\s+VERB\s+(?:a\s+)?(?P<anchor>twice|thrice|half|quarter|third)
\s+as\s+many\s+(?P<unit>\w+(?:\s+\w+)?)\s+as\s+(?P<reference>REF)\s*\.?$
```
## 2. Gap isolation — one variable at a time
Each gap isolated by mutating a known-good baseline in exactly one dimension.
This is the evidence, not the regex reading:
| probe | matches | isolates |
|---|---|---|
| `Aria has twice as many credits as Emily.` | **YES** | baseline |
| `… as many school credits as …` (2-word unit) | **YES** | unit slot admits 2 |
| `… as many high school credits as …` (3-word) | no | **unit slot caps at 2 words** |
| `… as many pounds of lobster as …` | no | **`of`-headed unit phrase** |
| `… as many credits than Emily.` | no | **`than` connector** |
| `… as Emily, who has twice … as Spencer.` | no | **trailing relative clause** |
| `At a station, Aria has twice …` | no | **leading adjunct** |
| `The number of people was twice the total number counted.` | no | **copula + bare `twice the N`** |
| `Mary has twice the amount of carrots as green beans.` | no | **`twice the amount of X as Y`** |
| `… as many credits as the two other students combined.` | no | **aggregate reference** |
## 3. Per-case traces
### Case 0000 — `expected 140.0`
> Aria has twice as many high school credits as Emily, who has twice the number
> of high school credits as Spencer. If Emily has 20 credits, what's twice the
> total number of high school credits the three have?
Blocked by **two** gaps in one sentence: a 3-word unit (`high school credits`)
and a trailing relative clause carrying a *second* comparative. Solving needs a
chained resolution (Emily=20 → Spencer=10, Aria=40), a summation (70), and a
final ×2 on the aggregate. **Four capabilities.**
### Case 0001 — `expected 480.0`
> Hooper Bay has twice as many pounds of lobster than the two other harbors
> combined. If the other two harbors have 80 pounds of lobster each, how many
> pounds of lobster are the three harbors holding?
Blocked by **four** gaps: `than` where the template requires `as`; an
`of`-headed unit (`pounds of lobster`); an aggregate reference (`the two other
harbors combined`); and a multi-token proper name —
`extract_proper_noun_subject` returns `'Hooper'`, not `'Hooper Bay'`, which
alone would fail the injector's narrow actor binding (`actor != actor_token`).
### Case 0148 — `expected 1500.0`
> At a people counting station, the number of people counted on the first day
> was twice the total number counted on the second day. If 500 people were
> counted on the second day, how many people were counted on the two days?
The only one classified `DESCRIPTIVE_SETUP_NO_QUANTITY` — the recognizer does
not see a count statement at all. Three gaps: a leading adjunct; a noun-phrase
quantity as subject (`the number of people counted on the first day`) rather
than an entity; and a copula + bare `twice the total number` form with no
`as many … as` at all.
### Case 0082 — `expected 2.0`
> Mary uses plastic grocery bags that can hold a maximum of twenty pounds. She
> buys 4 pounds of green beans, 6 pounds milk, and twice the amount of carrots
> as green beans. How many more pounds of groceries can Mary fit in that bag?
The refusing statement is not the comparative at all — it is the **capacity**
frame (`bags that can hold a maximum of twenty pounds`), a relative-clause
capacity assertion with a spelled-out cardinal. The comparative arrives in the
next sentence in the `twice the amount of X as Y` form. Then the question wants
capacity Σ(4, 6, 8). **Four capabilities**, none of which is the v1 template.
## 4. The measurement that changes the recommendation
Frequency of each blocking surface feature across all 500 holdout cases:
| surface feature | cases | % of 500 |
|---|---:|---:|
| aggregate reference (`combined` / `together` / `altogether`) | **53** | 10.6% |
| leading adjunct (`At/In/On/During …,`) | **28** | 5.6% |
| `twice/N times as many … as` (any unit width) | 29 | 5.8% |
| `twice the number/amount/total of` | **13** | 2.6% |
| copula + `was/is twice` | **13** | 2.6% |
| capacity frame (`hold` / `maximum of` / `capacity`) | 14 | 2.8% |
| — of the 29, unit is >2 words | **1** | **0.2%** |
| — of the 29, connector is `than` | **2** | **0.4%** |
**The gap blocking case 0000 affects one case in five hundred. The `than` gap
blocking case 0001 affects two.**
That is the whole finding. "Widen the unit slot from two words to three" and
"admit `than` alongside `as`" are precisely the per-case pattern growth ADR-0251
and `reader-arc-overfit-inventory-2026-07-19.md` exist to prohibit — and until
now the prohibition rested on a prior about overfitting. It now rests on a count.
The forms that actually recur are different ones: aggregate reference (53),
leading adjunct (28), `twice the number/amount of` (13), copula-twice (13).
**Honest limit on these numbers.** They are *surface-feature* counts, not
conversion estimates. A case containing `combined` is not thereby convertible —
§3 shows every one of these problems needs three or four capabilities at once,
so a case can carry a frequent feature and still strand on the other three.
The counts bound the opportunity; they do not predict yield. Treating them as
yield is the exact error the arc already made once, when tune-set parse-rate
gain was read as capability and shipped answers that were *wrong* on the exam.
## 5. What was deliberately not done
**No reader change was made.** The plan's Lane 4 exit criterion is the traces
plus `wrong=0` holding, not a shipped extension, and three things argue against
shipping one from this session:
1. §4 shows the named cases' gaps are the rarest in the corpus. Building them
is the prohibited move, now with a number attached.
2. The sealed 1,319-case test split is the final arbiter and must not be read.
A change justified only by movement on the open dev set has exactly the
evidentiary shape ADR-0251 was written against.
3. The four cases are each 34 capabilities deep. There is no "minimum reader
extension" that converts one of them — which is itself the strongest form of
the arc's own conclusion that *real GSM8K statements are individually
multi-capability*.
`wrong=0` still holds: `correct=5 wrong=0 refused=495`.
## 6. Recommendation
Phase 4.2 as scoped — case-first on 0000/0001/0148/0082 — should be recorded
as **traced and declined**, not attempted. The tracing was the right
instruction; its result is that these four are the wrong four.
If the reader arc resumes, the measured entry point is **aggregate reference**
(53 cases, 10.6%) — the single most common blocking surface in the corpus, and
the one whose semantics (`the two other harbors combined` → Σ over a named
complement set) is a genuine compositional capability rather than a template
widening. Scope it the same way this document scopes: isolate the gap, count
its reach, and require that the conversion evidence be a *capability* argument,
not a parse-rate delta.
Relates to [[project-generalization-arc]].

View file

@ -419,6 +419,65 @@ frame's half-space.
`TurnEvent.flagged`
: Mirrors `IdentityScore.flagged` for filtering and trace inspection.
`TurnEvent.grounding_source`
: The composer that grounded the turn, verbatim. Must be a member of
`core.epistemic_state.GroundingSource` — the closed, Workbench-coupled set.
Consumers that need to *validate* one read `GROUNDING_SOURCES`; nothing
restates the members. See "Grounding-source registration" below.
### `trace_hash` is not reconstructable from `TurnEvent`
This is a contract, not a defect, and it is stated here because the two fields
involved look interchangeable and are not.
- `compute_trace_hash` folds **`hash_surface`** — the register-invariant
truth-path capture (ADR-0069 inv C, ADR-0077 R6). Register decoration must
not move the truth-path identity, so the hash deliberately does not see the
bytes the user read.
- `finalize_turn_surface` (T13, 2026-07-22) back-stamps the **served** surface
onto `TurnEvent`, because the telemetry record must show what the user
actually saw — the `warmed_session_consistency` invariant.
These serve different invariants and are correct to differ. `TurnEvent` does
not carry `hash_surface`, so a consumer holding only telemetry **cannot**
recompute `trace_hash`; it will silently get a different value on any turn
carrying register decoration. Recomputation is not a supported operation.
Verify a `trace_hash` by replaying the pipeline, not by rebuilding it from a
turn record.
**Open, tracked as audit-ledger R7.** The opt-in external telemetry sink
(`attach_telemetry_sink`) still receives the **pre-override** event, so a
durable telemetry stream carries both a stale surface and a stale `trace_hash`
— strictly worse than the divergence above, which is at least internally
consistent. `chat/runtime.py::finalize_turn_surface` records the limitation at
the site. The fix is to defer the single sink emission to the pipeline serve
boundary, which repairs both fields at once; it was deliberately not bolted
onto the T13 red-fix. The default sink is `None`, so no default deployment is
affected today.
### Grounding-source registration
`core.epistemic_state.GroundingSource` is the single source of truth. Adding a
value is a three-part change that must land together:
1. the `Literal` in `core/epistemic_state.py`, plus its
`epistemic_state_for_grounding_source` arm;
2. `workbench-ui/enum-snapshot.json` (regenerate: `pnpm enum:snapshot`);
3. the UI badge contract — `badges/types.ts`, `badges/mappings.ts`, a design
token, and `types/api.ts`.
`workbench-ui`'s `enumCoverage.test.ts` fails the build if (1) and (3) diverge,
which is what makes this atomic rather than aspirational.
`deduction` (ADR-0256) and `curriculum` (ADR-0262) were served for a period
before being registered, on the reasoning that REPL turns do not reach
Workbench. The traffic flows the other way: `workbench/api.py`'s chat route
builds a bare `ChatRuntime()`, so those composers decide Workbench turns too,
and the API's coercion floored the unregistered labels to `"none"` — recording
a proved answer as ungrounded in a durable audit artifact. Registering a
grounding source is therefore part of shipping the composer that emits it, not
a follow-up. Pinned by `tests/test_workbench_deduction_provenance.py`.
## Identity contract
Identity checks are telemetry/gating signals. A flagged identity score must not

View file

@ -20,14 +20,14 @@ converse-class naming, which is the one thing genuinely local to estimation).
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from core.ratified_ledger import RatifiedLedgerError, load_sealed_ledger
from core.ratified_ledger import RatifiedLedgerError, ledger_spec, load_capability_ledger
from core.ratified_ledger import serve_license as _serve_license
from core.reliability_gate import Ceilings, ClassTally, LicenseDecision
from generate.determine.estimate import converse_class_name
_LEDGER_PATH = Path(__file__).resolve().parent / "data" / "estimation_ledger.json"
_LEDGER_CAPABILITY = "estimation"
_LEDGER_PATH = ledger_spec(_LEDGER_CAPABILITY).path
@lru_cache(maxsize=1)
@ -37,8 +37,12 @@ def load_ratified_ledger() -> dict[str, ClassTally]:
Raises :class:`RatifiedLedgerError` if the file is absent/malformed or its
recomputed ``content_sha256`` does not match the committed one (tamper-evidence:
only the sealed-practice output is trusted, never a hand-edited ledger).
This capability is registered ``missing_ok=False`` in ``CAPABILITY_LEDGERS``
it ships with its ledger, so absence is a broken deployment, and that
verdict is the manifest's rather than this call's (ADR-0263 rule 5).
"""
return load_sealed_ledger(_LEDGER_PATH)
return load_capability_ledger(_LEDGER_CAPABILITY)
def serve_license(

View file

@ -2,11 +2,25 @@
# CORE local-first pre-push gate (weekly-audit T13, 2026-07-22).
#
# Automates the AGENTS.md §Local-First CI Validation Protocol "Pre-Push Gate":
# (1) the `smoke` suite — exact CI-gate parity, and
# (1) the `smoke` suite — exact CI-gate parity,
# (2) the warmed_session consistency lane pin — catches the T13
# telemetry-consistency regression class, which `smoke` does NOT cover
# (the #96 fail-closed resolve + #97 morph override escaped smoke and
# surfaced only in the warmed_session lane).
# surfaced only in the warmed_session lane), and
# (3) the `deductive` suite — added 2026-07-25.
#
# On (3): this suite was an ASYNC CI concern while deduction serving was a
# capability under development. ADR-0256 ratified the flag ON by default, which
# makes it a regression suite for a LIVE serving path — a push that breaks
# deduction serving would otherwise clear this gate and sit broken on main for
# the window between push and CI. The register-axis regression (red on main for
# the 2026-07-22..24 window, masked by an unrelated flake label) is the direct
# precedent for that failure mode.
#
# It was added WHOLE rather than as a subset because the cost measurement said
# it could be: 285 tests in ~29s, against smoke's 216 in ~62s. There is no
# coverage trade recorded here because none was needed.
#
# The full ~12k fast-lane stays an ASYNC CI concern by design — this gate is
# deliberately targeted so it prevents the regression class without gridlocking
# the push cycle.
@ -32,21 +46,29 @@ if [ "${read_any}" -eq 1 ] && [ "${only_deletes}" -eq 1 ]; then
exit 0
fi
echo "[pre-push] CORE local-first gate: smoke suite + warmed_session lane pin"
echo "[pre-push] CORE local-first gate: smoke + warmed_session lane pin + deductive"
echo "[pre-push] (1/2) smoke suite (core test --suite smoke) ..."
echo "[pre-push] (1/3) smoke suite (core test --suite smoke) ..."
if ! uv run core test --suite smoke -q; then
echo "[pre-push] BLOCKED: smoke suite failed. Fix before pushing." >&2
echo "[pre-push] (emergency bypass, discouraged: git push --no-verify)" >&2
exit 1
fi
echo "[pre-push] (2/2) warmed_session consistency lane ..."
echo "[pre-push] (2/3) warmed_session consistency lane ..."
if ! uv run python -m pytest tests/test_warmed_session_lane.py -q; then
echo "[pre-push] BLOCKED: warmed_session lane failed (T13 regression class)." >&2
echo "[pre-push] (emergency bypass, discouraged: git push --no-verify)" >&2
exit 1
fi
echo "[pre-push] (3/3) deductive suite (core test --suite deductive) ..."
if ! uv run core test --suite deductive -q; then
echo "[pre-push] BLOCKED: deductive suite failed — deduction serving is a" >&2
echo "[pre-push] LIVE capability (ADR-0256), not an experiment." >&2
echo "[pre-push] (emergency bypass, discouraged: git push --no-verify)" >&2
exit 1
fi
echo "[pre-push] gate PASSED — push proceeding."
exit 0

382
teaching/ratification.py Normal file
View file

@ -0,0 +1,382 @@
"""teaching/ratification.py — the curriculum ratification ceremony.
The discovery loop is instrumented but not closed. Proposals flow into
``teaching/proposals/`` and a human can read them via ``core proposal-queue
review``, but `review_log.jsonl` explicitly *never* ratifies it records that
someone looked. Nothing converts a reviewed proposal into a ratified chain, so
the loop terminates in a human decision that produces no artifact.
That gap is the binding constraint. Curriculum serving is OFF because no band
can earn a license from present volume (ADR-0262 §5) every one of the eleven
live bands is 24×73× short of the entailed-bucket floor and the only way
volume grows is one ratified chain at a time.
This module is the code-bound ceremony: **a reviewed decision produces a chain
record, a corpus commit, and a receipt that proves the chain is actually
routable.**
Why the last clause is the whole design
---------------------------------------
``teaching.curriculum_premises._ratified_rows`` DROPS every row it cannot
admit wrong ``review_status``, wrong domain, empty terms, a term absent from
its declared pack, a connective outside ``CONNECTIVE_FAMILY``. It drops
silently, by ``continue``, because at *serving* time that is right: a curriculum
must never route on a row it cannot license.
At *ratification* time the same silence is a trap. Appending a row that the
loader will drop looks exactly like appending a row that works: the file grows,
the commit lands, the band count does not move, and nobody learns why for weeks.
So this ceremony refuses to call an append a ratification until it has re-read
the curriculum through the real loader and observed the chain arrive. Validation
is a pre-flight courtesy; **admission is the proof**.
Scope, deliberately narrow
--------------------------
Nothing here decides *whether* a proposal is true. It cannot auto-ratify, has no
opinion on content, and requires a human-supplied reviewer identity and rationale
on every call. It converts an already-made decision into the artifacts that
decision implies which is precisely what was missing.
The two downstream stages of the full ceremony, arena queue entry and ledger
reseal, are intentionally NOT performed here. Both are sealed-practice outputs,
and bridge rule 1 is that nothing outside a practice run may write a ledger
(``core.ratified_ledger``). :func:`ratify_chain` returns the receipt those
stages consume; running them stays an explicit, separate operator step.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from core.capability.domains import (
DOMAIN_CAPABILITY_CORPORA,
DOMAIN_CORPORA,
DOMAIN_PACKS,
)
from teaching.curriculum_premises import CONNECTIVE_FAMILY, load_curriculum
_REPO_ROOT = Path(__file__).resolve().parents[1]
#: Chain ids are ``<domain>-<family>-<NNN>``, zero-padded to three.
_CHAIN_ID_RE = re.compile(r"^(?P<domain>[a-z_]+)-(?P<family>[a-z]+)-(?P<seq>\d{3,})$")
#: The committed row's field order. Chain corpora are reviewed as diffs, so the
#: key order is part of the artifact, not an implementation detail.
_ROW_FIELDS: tuple[str, ...] = (
"chain_id",
"domain",
"operator_family",
"subject",
"intent",
"connective",
"object",
"subject_pack_id",
"object_pack_id",
"review_status",
"provenance",
)
class RatificationError(ValueError):
"""A proposed chain cannot be ratified, with the reason stated."""
@dataclass(frozen=True, slots=True)
class ChainRecord:
"""One curriculum edge, in the committed on-disk shape."""
chain_id: str
domain: str
operator_family: str
subject: str
intent: str
connective: str
object: str
subject_pack_id: str
object_pack_id: str
review_status: str
provenance: str
def as_row(self) -> dict[str, Any]:
return {name: getattr(self, name) for name in _ROW_FIELDS}
def as_jsonl_line(self) -> str:
"""Byte-compatible with the committed corpora: compact separators,
insertion order preserved, one trailing newline."""
return json.dumps(self.as_row(), separators=(",", ":")) + "\n"
@dataclass(frozen=True, slots=True)
class RatificationReceipt:
"""Proof that a ratification actually changed what the engine can route."""
chain: ChainRecord
corpus_id: str
corpus_path: str
chains_before: int
chains_after: int
family_chains_before: int
family_chains_after: int
#: Follow-on stages this receipt authorizes but does NOT perform.
pending_stages: tuple[str, ...] = field(
default=("arena_queue_entry", "ledger_reseal")
)
@property
def admitted(self) -> bool:
return self.chains_after == self.chains_before + 1
def as_dict(self) -> dict[str, Any]:
return {
"chain": self.chain.as_row(),
"corpus_id": self.corpus_id,
"corpus_path": self.corpus_path,
"chains_before": self.chains_before,
"chains_after": self.chains_after,
"family_chains_before": self.family_chains_before,
"family_chains_after": self.family_chains_after,
"admitted": self.admitted,
"pending_stages": list(self.pending_stages),
}
def _pack_lemmas(pack_id: str) -> frozenset[str]:
"""Mirror of the loader's pack reader, so validation sees what it sees."""
path = _REPO_ROOT / "packs" / "data" / pack_id / "lexicon.jsonl"
if not path.exists():
return frozenset()
lemmas: set[str] = set()
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(row, dict):
lemma = str(row.get("lemma") or "").strip()
if lemma:
lemmas.add(lemma)
return frozenset(lemmas)
def corpus_path_for(domain: str) -> tuple[str, Path]:
"""The single writable chain corpus for *domain*.
A domain with several corpora (``philosophy_theology``) has no unambiguous
append target, so the ceremony refuses rather than guessing which file a
reviewer meant.
"""
corpora = DOMAIN_CORPORA.get(domain, ())
if not corpora:
raise RatificationError(f"unknown domain: {domain!r}")
writable = [c for c in corpora if c in DOMAIN_CAPABILITY_CORPORA]
if len(writable) != 1:
raise RatificationError(
f"{domain}: expected exactly one writable chain corpus, found "
f"{writable!r} — name the target explicitly before ratifying"
)
corpus_id = writable[0]
return corpus_id, _REPO_ROOT / DOMAIN_CAPABILITY_CORPORA[corpus_id]
def existing_rows(domain: str) -> list[dict[str, Any]]:
"""Every row in *domain*'s corpus, admitted or not (raw file read)."""
_, path = corpus_path_for(domain)
if not path.exists():
return []
rows: list[dict[str, Any]] = []
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
row = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(row, dict):
rows.append(row)
return rows
def next_chain_id(domain: str, family: str) -> str:
"""The next free ``<domain>-<family>-NNN``, deterministic from the corpus."""
highest = 0
for row in existing_rows(domain):
m = _CHAIN_ID_RE.match(str(row.get("chain_id") or ""))
if m and m.group("domain") == domain and m.group("family") == family:
highest = max(highest, int(m.group("seq")))
return f"{domain}-{family}-{highest + 1:03d}"
def build_chain_record(
*,
domain: str,
subject: str,
connective: str,
obj: str,
reviewer: str,
rationale: str,
intent: str = "cause",
chain_id: str | None = None,
) -> ChainRecord:
"""Construct the record a reviewed decision implies.
``reviewer`` and ``rationale`` are required and land in ``provenance``: a
ratified chain must carry who ratified it and why, or the corpus loses the
audit lineage that makes it reviewable as a diff.
"""
reviewer = reviewer.strip()
rationale = rationale.strip()
if not reviewer:
raise RatificationError("ratification requires a reviewer identity")
if not rationale:
raise RatificationError("ratification requires a stated rationale")
family = CONNECTIVE_FAMILY.get(connective.strip())
if family is None:
raise RatificationError(
f"connective {connective!r} is outside CONNECTIVE_FAMILY "
f"{sorted(CONNECTIVE_FAMILY)} — the loader would drop this row"
)
packs = DOMAIN_PACKS.get(domain, ())
if not packs:
raise RatificationError(f"domain {domain!r} mounts no packs")
subject, obj = subject.strip(), obj.strip()
subject_pack = next((p for p in packs if subject in _pack_lemmas(p)), "")
object_pack = next((p for p in packs if obj in _pack_lemmas(p)), "")
if not subject_pack:
raise RatificationError(
f"subject {subject!r} is taught by no pack mounted for {domain} "
f"({list(packs)}) — the loader would drop this row"
)
if not object_pack:
raise RatificationError(
f"object {obj!r} is taught by no pack mounted for {domain} "
f"({list(packs)}) — the loader would drop this row"
)
return ChainRecord(
chain_id=chain_id or next_chain_id(domain, family),
domain=domain,
operator_family=family,
subject=subject,
intent=intent,
connective=connective.strip(),
object=obj,
subject_pack_id=subject_pack,
object_pack_id=object_pack,
review_status="reviewed",
provenance=f"ratification-ceremony:{reviewer}:{rationale}",
)
def validate_admissible(record: ChainRecord) -> None:
"""Pre-flight: every rule ``_ratified_rows`` drops on, raised LOUDLY.
Serving is right to drop silently. Ratification is not: an append the
loader will discard looks identical to one that works.
"""
if record.review_status != "reviewed":
raise RatificationError(
f"review_status={record.review_status!r} — only 'reviewed' is admitted"
)
if not record.subject or not record.object:
raise RatificationError("subject and object must both be non-empty")
if CONNECTIVE_FAMILY.get(record.connective) is None:
raise RatificationError(f"connective {record.connective!r} has no family")
if record.subject_pack_id and record.subject not in _pack_lemmas(record.subject_pack_id):
raise RatificationError(
f"subject {record.subject!r} absent from pack {record.subject_pack_id}"
)
if record.object_pack_id and record.object not in _pack_lemmas(record.object_pack_id):
raise RatificationError(
f"object {record.object!r} absent from pack {record.object_pack_id}"
)
for row in existing_rows(record.domain):
if str(row.get("chain_id")) == record.chain_id:
raise RatificationError(f"chain_id {record.chain_id} already committed")
if (
str(row.get("subject")) == record.subject
and str(row.get("connective")) == record.connective
and str(row.get("object")) == record.object
):
raise RatificationError(
f"duplicate edge: {row.get('chain_id')} already teaches "
f"{record.subject} {record.connective} {record.object}"
)
def _count_chains(domain: str, family: str) -> tuple[int, int]:
load_curriculum.cache_clear()
curriculum = load_curriculum(domain)
return len(curriculum.chains), sum(
1 for c in curriculum.chains if c.family == family
)
def ratify_chain(record: ChainRecord, *, dry_run: bool = False) -> RatificationReceipt:
"""The ceremony: validate → append → **confirm admission** → receipt.
Raises :class:`RatificationError` if the appended row does not arrive in the
reloaded curriculum. On that failure the append is rolled back, because a
corpus carrying rows the engine silently ignores is worse than one that
never grew: it makes the volume ledger lie.
"""
validate_admissible(record)
corpus_id, path = corpus_path_for(record.domain)
before_total, before_family = _count_chains(record.domain, record.operator_family)
if dry_run:
return RatificationReceipt(
chain=record,
corpus_id=corpus_id,
corpus_path=str(path),
chains_before=before_total,
chains_after=before_total + 1,
family_chains_before=before_family,
family_chains_after=before_family + 1,
)
original = path.read_text(encoding="utf-8") if path.exists() else ""
if original and not original.endswith("\n"):
original += "\n"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(original + record.as_jsonl_line(), encoding="utf-8")
after_total, after_family = _count_chains(record.domain, record.operator_family)
if after_total != before_total + 1:
path.write_text(original, encoding="utf-8")
_count_chains(record.domain, record.operator_family)
raise RatificationError(
f"{record.chain_id} was appended but the curriculum loader did not "
f"admit it ({before_total} -> {after_total}); append rolled back"
)
return RatificationReceipt(
chain=record,
corpus_id=corpus_id,
corpus_path=str(path),
chains_before=before_total,
chains_after=after_total,
family_chains_before=before_family,
family_chains_after=after_family,
)
__all__ = [
"ChainRecord",
"RatificationError",
"RatificationReceipt",
"build_chain_record",
"corpus_path_for",
"existing_rows",
"next_chain_id",
"ratify_chain",
"validate_admissible",
]

73
tests/test_adr_index.py Normal file
View file

@ -0,0 +1,73 @@
"""The domain-keyed ADR index must not rot into a page of dead links.
`docs/adr/INDEX-by-domain.md` exists because the corpus is at 312 files under
flat sequential numbering, and sequence records *when* a decision was made and
nothing about *what it governs*. An index whose links have decayed is worse
than no index, because it converts "I should grep" into "I already checked".
Deliberately narrow: this pins that every ADR the index names exists, and that
the index stays honest about being partial. It does not assert coverage of all
312 the index says so itself, and a test demanding completeness would only
push someone to satisfy it with unread rows.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
ADR_DIR = Path(__file__).resolve().parents[1] / "docs" / "adr"
INDEX = ADR_DIR / "INDEX-by-domain.md"
@pytest.fixture(scope="module")
def index_text() -> str:
return INDEX.read_text(encoding="utf-8")
def test_index_exists(index_text: str) -> None:
assert index_text.strip(), "the ADR domain index is empty"
def test_every_indexed_adr_resolves(index_text: str) -> None:
"""A dead link means an ADR was renamed and the index was not touched."""
links = re.findall(r"\]\(\./([^)]+)\)", index_text)
assert links, "no relative links parsed — the index format changed"
dead = sorted({link for link in links if not (ADR_DIR / link).exists()})
assert dead == [], f"index points at ADRs that do not exist: {dead}"
def test_index_covers_the_live_serving_arc(index_text: str) -> None:
"""The domains a next-arc session actually queries.
These are the ADRs behind capabilities that are ON in production: if one is
minted-and-forgotten out of the index, the index has stopped being the
thing you can trust for the live surface, which is its whole job.
"""
required = [
"ADR-0256", # deduction serving, earned license, flag ratified ON
"ADR-0262", # curriculum-grounded serving
"ADR-0263", # ratified-ledger bridge
"ADR-0255", # discovery-yield telemetry
"ADR-0142", # epistemic-state taxonomy
"ADR-0162", # workbench design system (the enum/badge contract)
"ADR-0251", # reader-arc prohibition on per-case pattern growth
]
missing = [adr for adr in required if adr not in index_text]
assert missing == [], f"live-arc ADRs absent from the domain index: {missing}"
def test_index_declares_its_own_partiality(index_text: str) -> None:
"""The honesty property. An index that silently claims completeness is a
trap; this one states its scope, and that statement is load-bearing."""
assert "not a complete index" in index_text.lower()
def test_all_six_deduction_bands_are_indexed(index_text: str) -> None:
"""v1b through v6-EX. The band cascade is the thing most likely to grow
without the index following, because each new band is one more ADR."""
for adr in ("ADR-0257", "ADR-0258", "ADR-0259", "ADR-0260", "ADR-0261"):
assert adr in index_text, f"{adr} (a shipped band) is not indexed"

View file

@ -72,10 +72,43 @@ def test_cli_smoke_suite_covers_ci_smoke_gate() -> None:
ci_paths.add(token)
assert ci_paths, "no tests/ paths parsed from smoke.yml — pin needs updating"
missing = ci_paths - set(TEST_SUITES["smoke"])
assert not missing, (
local_paths = set(TEST_SUITES["smoke"])
missing_locally = ci_paths - local_paths
assert not missing_locally, (
"CLI smoke suite is narrower than the CI smoke gate; add to "
f"core/cli_test.py TEST_SUITES['smoke']: {sorted(missing)}"
f"core/cli_test.py TEST_SUITES['smoke']: {sorted(missing_locally)}"
)
# The other direction, which this pin did not check until 2026-07-25 and
# which had in fact drifted: tests/test_pack_draft_serve_boundary.py sat in
# the local gate and not in smoke.yml. The list's own comment promises the
# two are *equal* ("so the local-first pre-push gate equals the CI gate
# rather than silently narrowing it"), and a one-directional assertion can
# only ever hold half of that. A file only the local gate runs is a file
# whose regression is invisible to anyone reading a green CI badge.
#
# PENDING_IN_CI is a named, dated exception list, not a suppression. These
# three are already in the local gate and belong in smoke.yml; the edit was
# authored and could not be pushed from the authoring session, whose
# credential lacked the `workflow` OAuth scope. Anyone with that scope
# closes this by adding the three lines and emptying this tuple. The
# assertion below still fires on any NEW divergence, which is the
# protection that was missing entirely before.
PENDING_IN_CI = (
"tests/test_pack_draft_serve_boundary.py",
"tests/test_prior_surface_deduction_binding.py",
"tests/test_workbench_deduction_provenance.py",
)
missing_in_ci = local_paths - ci_paths - set(PENDING_IN_CI)
assert not missing_in_ci, (
"CI smoke gate is narrower than the CLI smoke suite; add to "
f".github/workflows/smoke.yml: {sorted(missing_in_ci)}"
)
stale_exceptions = set(PENDING_IN_CI) & ci_paths
assert not stale_exceptions, (
"these are in smoke.yml now — drop them from PENDING_IN_CI: "
f"{sorted(stale_exceptions)}"
)

View file

@ -114,11 +114,21 @@ def test_tampered_ledger_is_rejected(tmp_path, monkeypatch) -> None:
rejected on load only the sealed-practice output is trusted."""
import chat.deduction_serve_license as mod
from dataclasses import replace
from core.ratified_ledger import CAPABILITY_LEDGERS
original = json.loads(mod._LEDGER_PATH.read_text(encoding="utf-8"))
original["classes"]["conditional_single"]["correct"] = 999999 # tamper
tampered = tmp_path / "tampered.json"
tampered.write_text(json.dumps(original), encoding="utf-8")
monkeypatch.setattr(mod, "_LEDGER_PATH", tampered)
# Redirect at the manifest, which owns the path since ADR-0263 rule 5 —
# patching the module constant would no longer reach the load.
monkeypatch.setitem(
CAPABILITY_LEDGERS,
mod._LEDGER_CAPABILITY,
replace(CAPABILITY_LEDGERS[mod._LEDGER_CAPABILITY], path=tampered),
)
load_ratified_ledger.cache_clear()
with pytest.raises(RatifiedLedgerError):
load_ratified_ledger()

View file

@ -151,13 +151,23 @@ def test_tampered_ledger_is_rejected(tmp_path, monkeypatch) -> None:
import generate.determine.estimation_license as mod
from generate.determine.estimation_license import RatifiedLedgerError, load_ratified_ledger
from dataclasses import replace
from core.ratified_ledger import CAPABILITY_LEDGERS
good = _json.loads(mod._LEDGER_PATH.read_text(encoding="utf-8"))
good["classes"][converse_class_name(REFUSED_PREDICATE)]["correct"] = 999 # forge a SERVE
forged_path = tmp_path / "estimation_ledger.json"
forged_path.write_text(_json.dumps(good), encoding="utf-8")
load_ratified_ledger.cache_clear()
monkeypatch.setattr(mod, "_LEDGER_PATH", forged_path)
# Redirect at the manifest, which owns the path since ADR-0263 rule 5 —
# patching the module constant would no longer reach the load.
monkeypatch.setitem(
CAPABILITY_LEDGERS,
mod._LEDGER_CAPABILITY,
replace(CAPABILITY_LEDGERS[mod._LEDGER_CAPABILITY], path=forged_path),
)
try:
with pytest.raises(RatifiedLedgerError, match="content_sha256 mismatch"):
load_ratified_ledger()

View file

@ -0,0 +1,127 @@
"""Correction binding anchors to the truth path, including on deduction turns.
``CognitiveTurnPipeline`` remembers one surface per turn so the *next* turn can
be read as a correction of it (``_run_teaching`` ``extract_correction``).
Which bytes it remembers is an epistemic choice, pinned here:
``pipeline.py`` sets ``self._prior_surface = hash_surface`` the register-
invariant truth-path capture (ADR-0069 inv C), not the served surface. A
register pack that decorates or terses the served bytes therefore cannot
perturb what a correction binds to, which is what keeps teaching independent
of presentation.
The subtlety worth pinning is that "truth path" is not "pre-correction". The
Phase 0 register fix threads ``hash_surface`` through every *substantive*
override logos-morph refusal, the speculative marker so when one of those
fires, ``_prior_surface`` holds the post-override bytes the truth path
actually settled on. It is only the presentation layer that is excluded.
An architectural review of the deduction-serve arc flagged this as an untested
gap and predicted the opposite behaviour (that a correction on a deduction turn
would anchor to stale pre-correction bytes). These tests were written to check
that claim. It does not reproduce the lockstep holds so what they pin is
the behaviour that is actually there, on the deduction path that went live with
ADR-0256, so it cannot drift unobserved.
"""
from __future__ import annotations
from dataclasses import replace
import pytest
from chat.runtime import ChatRuntime
from core.cognition.pipeline import (
_SPECULATIVE_SURFACE_MARKER,
CognitiveTurnPipeline,
)
from core.config import DEFAULT_CONFIG
DEDUCTION_PROMPT = (
"If it rains then the ground is wet. It rains. "
"Therefore the ground is wet."
)
def _pipeline(**config_overrides: object) -> CognitiveTurnPipeline:
runtime = ChatRuntime(no_load_state=True)
if config_overrides:
runtime.config = replace(runtime.config, **config_overrides) # type: ignore[arg-type]
return CognitiveTurnPipeline(runtime)
def test_deduction_turn_binds_correction_to_the_served_conclusion() -> None:
"""A deduction turn must leave a bindable anchor, not an empty one.
If ``_prior_surface`` stayed ``None`` or held a stale disclosure after a
decided turn, the next turn's correction would silently bind to nothing.
"""
pipeline = _pipeline()
result = pipeline.run(DEDUCTION_PROMPT, max_tokens=24)
assert "entail" in result.surface, f"composer did not serve: {result.surface!r}"
assert pipeline._prior_surface, "a decided turn left no correction anchor"
assert "the ground is wet" in pipeline._prior_surface
def test_prior_surface_is_the_hash_surface_not_the_decorated_surface() -> None:
"""The truth-path capture, not the register-decorated served bytes.
``trace_hash`` folds ``hash_surface``; binding corrections to the same
bytes is what keeps a register pack from moving teaching provenance.
"""
pipeline = _pipeline()
pipeline.run(DEDUCTION_PROMPT, max_tokens=24)
assert pipeline._prior_surface is not None
# No register decoration is configured in this fixture, so the two agree —
# the assertion that matters is that the anchor is a real truth-path
# capture rather than the bounded-disclosure fallback.
assert "I don't know" not in pipeline._prior_surface
@pytest.mark.parametrize("flag", [True, False])
def test_correction_anchor_survives_the_deduction_flag_in_both_positions(
flag: bool,
) -> None:
"""Flag-off must not regress the anchor either.
ADR-0256's rollback contract is that flag-off is byte-identical to pre-arc
dispatch. That includes leaving a bindable correction anchor: the same
prompt falls through to the pack-token-gloss path, which is a different
surface but must still be remembered.
"""
pipeline = _pipeline(deduction_serving_enabled=flag)
pipeline.run(DEDUCTION_PROMPT, max_tokens=24)
assert pipeline._prior_surface, (
f"deduction_serving_enabled={flag} left no correction anchor"
)
def test_speculative_marker_is_carried_into_the_anchor() -> None:
"""The lockstep property, stated directly.
``hash_surface`` is updated alongside every substantive override of the
served surface the speculative marker is the cheapest one to exercise.
This is the mechanism that makes the review's "anchors to pre-correction
bytes" prediction not reproduce; if someone later mutates ``surface``
without mutating ``hash_surface``, this is what catches it.
"""
pipeline = _pipeline()
pipeline._speculative_subjects = {"rains"}
result = pipeline.run(DEDUCTION_PROMPT, max_tokens=24)
assert result.surface.startswith(_SPECULATIVE_SURFACE_MARKER), (
"fixture did not actually mark the turn, so this pin would be vacuous: "
f"{result.surface!r}"
)
assert pipeline._prior_surface is not None
assert pipeline._prior_surface.startswith(_SPECULATIVE_SURFACE_MARKER), (
"served surface was marked but the truth-path anchor was not — "
"hash_surface fell out of lockstep with surface"
)
def test_deduction_serving_is_live_for_these_pins() -> None:
"""Documents why this file targets the deduction path specifically."""
assert DEFAULT_CONFIG.deduction_serving_enabled is True

View file

@ -0,0 +1,282 @@
"""The curriculum ratification ceremony — a decision must produce artifacts.
The discovery loop was instrumented but not closed: proposals reached a human,
`review_log.jsonl` recorded that they looked, and nothing converted a reviewed
decision into a ratified chain. These pin the ceremony that closes it, and in
particular the property the whole design exists for **an append is not a
ratification until the curriculum loader is observed to admit it.**
"""
from __future__ import annotations
import pytest
from core.capability.domains import DOMAIN_CORPORA, DOMAIN_PACKS
from teaching.curriculum_premises import CONNECTIVE_FAMILY, load_curriculum
from teaching.ratification import (
ChainRecord,
RatificationError,
build_chain_record,
corpus_path_for,
existing_rows,
next_chain_id,
ratify_chain,
validate_admissible,
)
REVIEWER = "test-operator"
RATIONALE = "pinned by tests/test_ratification_ceremony.py"
@pytest.fixture
def physics_corpus_restored():
"""Any test that really writes must leave the committed corpus untouched."""
_, path = corpus_path_for("physics")
original = path.read_text(encoding="utf-8")
try:
yield path
finally:
path.write_text(original, encoding="utf-8")
load_curriculum.cache_clear()
# ---------------------------------------------------------------------------
# Construction — a decision's provenance is not optional
# ---------------------------------------------------------------------------
def test_reviewer_and_rationale_are_required() -> None:
"""A ratified chain carries who ratified it and why, or the corpus loses
the audit lineage that makes it reviewable as a diff."""
for reviewer, rationale in ((" ", RATIONALE), (REVIEWER, " ")):
with pytest.raises(RatificationError, match="requires a"):
build_chain_record(
domain="physics",
subject="force",
connective="causes",
obj="energy",
reviewer=reviewer,
rationale=rationale,
)
def test_chain_id_is_deterministic_and_next_in_sequence() -> None:
assert next_chain_id("physics", "causal") == next_chain_id("physics", "causal")
seq = int(next_chain_id("physics", "causal").rsplit("-", 1)[1])
committed = [
int(str(r["chain_id"]).rsplit("-", 1)[1])
for r in existing_rows("physics")
if str(r.get("chain_id", "")).startswith("physics-causal-")
]
assert seq == max(committed) + 1
def test_family_is_derived_from_the_connective_not_supplied() -> None:
record = build_chain_record(
domain="physics",
subject="force",
connective="requires",
obj="energy",
reviewer=REVIEWER,
rationale=RATIONALE,
)
assert record.operator_family == CONNECTIVE_FAMILY["requires"] == "modal"
def test_row_is_byte_compatible_with_the_committed_corpus() -> None:
"""Chain corpora are reviewed as diffs, so key order and separators are
part of the artifact. A reformatting ratification would show every row as
changed and make the real change unreviewable."""
committed = corpus_path_for("physics")[1].read_text(encoding="utf-8").splitlines()[0]
row = existing_rows("physics")[0]
rebuilt = ChainRecord(**{k: row[k] for k in ChainRecord.__slots__}).as_jsonl_line()
assert rebuilt.rstrip("\n") == committed
# ---------------------------------------------------------------------------
# Validation — every rule the loader drops on, raised loudly
# ---------------------------------------------------------------------------
def test_connective_outside_the_family_table_is_refused() -> None:
with pytest.raises(RatificationError, match="outside CONNECTIVE_FAMILY"):
build_chain_record(
domain="physics",
subject="force",
connective="influences", # not in CONNECTIVE_FAMILY
obj="energy",
reviewer=REVIEWER,
rationale=RATIONALE,
)
def test_untaught_term_is_refused_with_the_loader_s_reason() -> None:
"""The anti-recall boundary, enforced at ratification: a chain about a term
no mounted pack teaches cannot route, so committing it only inflates the
file."""
with pytest.raises(RatificationError, match="taught by no pack"):
build_chain_record(
domain="physics",
subject="force",
connective="causes",
obj="photosynthesis", # biology; no physics pack teaches it
reviewer=REVIEWER,
rationale=RATIONALE,
)
def test_duplicate_edge_is_refused() -> None:
"""Re-teaching an edge inflates the volume count without adding coverage —
exactly the repetition-padding ADR-0262 §5 rejected as dishonest."""
row = existing_rows("physics")[0]
dup = build_chain_record(
domain="physics",
subject=row["subject"],
connective=row["connective"],
obj=row["object"],
reviewer=REVIEWER,
rationale=RATIONALE,
)
with pytest.raises(RatificationError, match="duplicate edge"):
validate_admissible(dup)
def test_unreviewed_status_is_refused() -> None:
row = existing_rows("physics")[0]
record = ChainRecord(**{**{k: row[k] for k in ChainRecord.__slots__},
"chain_id": "physics-causal-900",
"subject": "entropy", "object": "temperature",
"review_status": "pending"})
with pytest.raises(RatificationError, match="only 'reviewed' is admitted"):
validate_admissible(record)
def test_every_served_domain_has_exactly_one_writable_corpus() -> None:
"""philosophy_theology lists three corpora in ``DOMAIN_CORPORA``, but only
one is writable (present in ``DOMAIN_CAPABILITY_CORPORA``), so the append
target is unambiguous today. This pins that it stays that way: the day a
second writable corpus is registered for a domain, ratification has a real
choice to make and must not make it silently."""
for domain in DOMAIN_CORPORA:
if domain not in DOMAIN_PACKS:
continue
corpus_id, path = corpus_path_for(domain)
assert corpus_id, domain
assert path.name.endswith(".jsonl"), domain
def test_ambiguous_append_target_refuses_rather_than_guessing(monkeypatch) -> None:
"""The guard itself, exercised against an injected second writable corpus.
Picking one silently would scatter a subject's curriculum across files."""
import teaching.ratification as mod
monkeypatch.setitem(
mod.DOMAIN_CORPORA, "physics", ("physics_chains_v1", "physics_chains_v2")
)
monkeypatch.setitem(
mod.DOMAIN_CAPABILITY_CORPORA,
"physics_chains_v2",
"teaching/domain_chains/physics_chains_v2.jsonl",
)
with pytest.raises(RatificationError, match="exactly one writable"):
corpus_path_for("physics")
# ---------------------------------------------------------------------------
# The ceremony itself — admission is the proof, not the append
# ---------------------------------------------------------------------------
def test_dry_run_reports_the_delta_without_touching_the_corpus() -> None:
before = corpus_path_for("physics")[1].read_text(encoding="utf-8")
record = build_chain_record(
domain="physics",
subject="entropy",
connective="causes",
obj="temperature",
reviewer=REVIEWER,
rationale=RATIONALE,
)
receipt = ratify_chain(record, dry_run=True)
assert receipt.admitted
assert receipt.chains_after == receipt.chains_before + 1
assert corpus_path_for("physics")[1].read_text(encoding="utf-8") == before
def test_ratification_moves_the_band_the_licence_is_scored_on(
physics_corpus_restored,
) -> None:
"""End to end on the real corpus. The family count is the number that
matters: licences are scored per (subject x family), so a ratification that
moves the total but not the family has not moved a band."""
record = build_chain_record(
domain="physics",
subject="entropy",
connective="causes",
obj="temperature",
reviewer=REVIEWER,
rationale=RATIONALE,
)
receipt = ratify_chain(record)
assert receipt.admitted
assert receipt.family_chains_after == receipt.family_chains_before + 1
assert receipt.chain.chain_id in {
c.chain_id for c in load_curriculum("physics").chains
}, "the ceremony reported success but the chain is not routable"
def test_receipt_names_the_stages_it_did_not_perform(
physics_corpus_restored,
) -> None:
"""Bridge rule 1: nothing outside a sealed practice run writes a ledger.
The ceremony hands those stages to the operator rather than doing them."""
record = build_chain_record(
domain="physics",
subject="entropy",
connective="causes",
obj="temperature",
reviewer=REVIEWER,
rationale=RATIONALE,
)
receipt = ratify_chain(record, dry_run=True)
assert receipt.pending_stages == ("arena_queue_entry", "ledger_reseal")
def test_append_that_the_loader_would_drop_is_rolled_back(
physics_corpus_restored, monkeypatch
) -> None:
"""**The property the design exists for.**
``_ratified_rows`` drops unadmissible rows silently, which is correct at
serving time and a trap at ratification time: the file grows, the commit
lands, the band count does not move, and nobody learns why. Simulated here
by making the loader refuse to see the new row.
"""
import teaching.ratification as mod
before = physics_corpus_restored.read_text(encoding="utf-8")
record = build_chain_record(
domain="physics",
subject="entropy",
connective="causes",
obj="temperature",
reviewer=REVIEWER,
rationale=RATIONALE,
)
calls = {"n": 0}
def stuck_counts(domain: str, family: str) -> tuple[int, int]:
calls["n"] += 1
return (16, 8) # never moves, whatever we append
monkeypatch.setattr(mod, "_count_chains", stuck_counts)
with pytest.raises(RatificationError, match="did not admit it"):
ratify_chain(record)
assert physics_corpus_restored.read_text(encoding="utf-8") == before, (
"a non-admitted append must not survive — a corpus carrying rows the "
"engine ignores makes the volume ledger lie"
)

View file

@ -7,12 +7,16 @@ committed artifacts byte-for-byte.
from __future__ import annotations
import ast
import json
from pathlib import Path
import pytest
from core.ratified_ledger import (
CAPABILITY_LEDGERS,
RatifiedLedgerError,
load_capability_ledger,
load_sealed_ledger,
seal_artifact,
serve_license,
@ -111,3 +115,79 @@ def test_every_adapter_reads_through_the_bridge() -> None:
assert "formation.hashing" not in text, (
f"{module.__name__} still re-implements verification"
)
class TestCapabilityManifest:
"""Rule 5 — absence policy is DECLARED, not passed at the call site.
``missing_ok`` answers a question about the capability ("does this ship
with a ledger, or is its practice volume still being built?"), not about
the call. While it was only a ``load_sealed_ledger`` keyword, any adapter
onboarding a new subject through the bridge could pass ``True`` and quietly
turn a should-be-hard-refuse into a disclosed hedge, with nothing to catch
it. These pin the manifest that took the choice away from the call site.
"""
def test_every_registered_ledger_resolves_inside_the_repo(self) -> None:
"""Same class as the ``derived_close_proposals`` ``parents[3]`` bug: a
path constant that silently pointed outside the tree for five weeks
because the feature was default-off and nothing asserted the path."""
root = Path(__file__).resolve().parents[1]
for spec in CAPABILITY_LEDGERS.values():
assert root in spec.path.parents, (
f"{spec.capability}: ledger path escapes the repo: {spec.path}"
)
def test_shipping_capabilities_are_declared_required(self) -> None:
"""A ledger that exists on disk must not be declared optional — that
combination reads "absence is fine" about a capability whose absence
would in fact be a broken deployment."""
for spec in CAPABILITY_LEDGERS.values():
if spec.path.exists():
assert spec.missing_ok is False, (
f"{spec.capability} ships a committed ledger but is "
"registered missing_ok=True"
)
def test_deduction_serve_is_required_and_loads(self) -> None:
ledger = load_capability_ledger("deduction_serve")
assert len(ledger) == 25, "the 25 sealed shape-bands (ADR-0256)"
assert all(tally.wrong == 0 for tally in ledger.values())
def test_curriculum_serve_is_optional_and_empty_today(self) -> None:
"""ADR-0262 §5 — no band has earned a license from present volume."""
assert CAPABILITY_LEDGERS["curriculum_serve"].missing_ok is True
assert load_capability_ledger("curriculum_serve") == {}
def test_unregistered_capability_refuses(self) -> None:
"""A capability the manifest never declared has no absence policy to
inherit, so it cannot be consumed at all."""
with pytest.raises(RatifiedLedgerError, match="unregistered"):
load_capability_ledger("philosophy_serve")
def test_no_production_adapter_passes_missing_ok(self) -> None:
"""The keyword survives on the primitive for tests and one-off tooling.
If a serving path starts passing it again, the manifest has been routed
around and this fails.
Matched on the AST rather than the text: the string ``missing_ok``
appears legitimately in the adapters' docstrings, which explain the
policy they inherit. What must not appear is an *argument*.
"""
root = Path(__file__).resolve().parents[1]
offenders: list[str] = []
for directory in ("chat", "core", "generate", "teaching", "workbench"):
for path in sorted((root / directory).rglob("*.py")):
if path.name == "ratified_ledger.py":
continue # the primitive's own definition site
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
func = node.func
name = func.attr if isinstance(func, ast.Attribute) else getattr(func, "id", "")
if name != "load_sealed_ledger":
continue
if any(kw.arg == "missing_ok" for kw in node.keywords):
offenders.append(f"{path.relative_to(root)}:{node.lineno}")
assert offenders == [], f"missing_ok passed outside the manifest: {offenders}"

View file

@ -0,0 +1,108 @@
"""Workbench must record the grounding source it was actually served.
The deduction-serve arc ratified ``deduction_serving_enabled`` ON by default
(ADR-0256, 2026-07-24). ``workbench/api.py``'s live chat route builds a bare
``ChatRuntime()``, so a deduction-shaped prompt through Workbench is decided by
the ROBDD entailment engine and stamped ``grounding_source="deduction"`` on the
``TurnEvent`` exactly like the REPL path.
Before this file existed, ``_coerce_grounding_source`` carried a hand-copied
whitelist of the six pre-arc labels and silently rewrote everything else to
``"none"``. A proved, SERVE-licensed answer was therefore recorded in the
journal, the evidence bundle, and the pipeline record as *ungrounded*. Not a
crash and not a dropped turn: a durable audit artifact asserting something
false about a turn the engine had decided.
The asymmetry that makes this worth pinning: the *unregistered* path degraded
honestly ``epistemic_state_for_grounding_source`` returns
``EPISTEMIC_STATE_NEEDED`` for a label it does not know, which reads "this
needs determination". The *coercion* asserted ``none`` = ungrounded. A
second, hand-maintained copy of a closed enum is worse than no copy at all,
so the tests here pin both halves: the values are registered, and the
whitelist is derived from the registration rather than restated beside it.
"""
from __future__ import annotations
import pytest
from core.config import DEFAULT_CONFIG
from core.epistemic_state import (
EpistemicState,
epistemic_state_for_grounding_source,
)
from workbench.api import _coerce_grounding_source, _run_chat_turn
#: A Band v1 propositional argument: the composer's commit gate
#: (``looks_like_deductive_argument``) fires on the sentence-initial
#: "Therefore", and the ROBDD decides it ENTAILED.
DEDUCTION_PROMPT = (
"If it rains then the ground is wet. It rains. "
"Therefore the ground is wet."
)
def test_deduction_serving_is_on_by_default() -> None:
"""The premise of every other test in this file (ADR-0256 ratification)."""
assert DEFAULT_CONFIG.deduction_serving_enabled is True
def test_workbench_records_deduction_as_its_own_grounding_source() -> None:
"""The regression: a decided answer recorded as ungrounded.
Drives the same entry point the HTTP chat route uses, so this fails if the
coercion whitelist, the ``GroundingSource`` registration, or the composer's
dispatch position regresses.
"""
result = _run_chat_turn(DEDUCTION_PROMPT)
assert "entail" in result.surface, (
f"the deduction composer did not serve this turn: {result.surface!r}"
)
assert result.grounding_source == "deduction", (
"Workbench recorded a ROBDD-decided answer as "
f"{result.grounding_source!r} — the audit artifact is asserting "
"something false about how this turn was grounded"
)
def test_workbench_records_deduction_as_decoded() -> None:
"""A decided answer is DECODED, not UNDETERMINED and not STATE_NEEDED."""
result = _run_chat_turn(DEDUCTION_PROMPT)
assert result.epistemic_state == EpistemicState.DECODED.value
@pytest.mark.parametrize(
"source",
["pack", "teaching", "vault", "partial", "oov", "none", "deduction", "curriculum"],
)
def test_coercion_preserves_every_registered_grounding_source(source: str) -> None:
"""No registered label may be rewritten to another label.
Parametrized over the registration itself rather than a restated list:
a value added to ``GroundingSource`` without a matching coercion entry
fails here rather than silently downgrading in production.
"""
assert _coerce_grounding_source(source) == source
def test_coercion_still_floors_unknown_labels_to_none() -> None:
"""An UNregistered label is not grounding evidence and must not pass."""
assert _coerce_grounding_source("not_a_grounding_source") == "none"
assert _coerce_grounding_source(None) == "none"
assert _coerce_grounding_source("") == "none"
def test_decided_grounding_sources_map_to_decoded() -> None:
"""Deduction and curriculum are decided, so they rank with pack/teaching/vault."""
for source in ("deduction", "curriculum"):
assert epistemic_state_for_grounding_source(source) is EpistemicState.DECODED
def test_unknown_grounding_source_still_asks_for_a_state() -> None:
"""The honest-degradation default must survive the registration."""
assert (
epistemic_state_for_grounding_source("not_a_grounding_source")
is EpistemicState.EPISTEMIC_STATE_NEEDED
)

View file

@ -22,7 +22,9 @@
"vault",
"partial",
"oov",
"none"
"none",
"deduction",
"curriculum"
],
"NormativeClearance": [
"cleared",

View file

@ -46,6 +46,8 @@ export const groundingSourceMeta = {
[GroundingSource.PARTIAL]: { label: "Partial", colorToken: "--color-grounding-partial", meaning: "Only partial grounding was available.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is partial." },
[GroundingSource.OOV]: { label: "OOV", colorToken: "--color-grounding-oov", meaning: "Out-of-vocabulary grounding was encountered.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is oov." },
[GroundingSource.NONE]: { label: "None", colorToken: "--color-grounding-none", meaning: "No grounding source was present.", adr: "ADR-0160 / ADR-0162", evidence: "grounding_source is none." },
[GroundingSource.DEDUCTION]: { label: "Deduction", colorToken: "--color-grounding-deduction", meaning: "Decided by the verified entailment engine from the argument's own premises.", adr: "ADR-0256 / ADR-0162", evidence: "grounding_source is deduction." },
[GroundingSource.CURRICULUM]: { label: "Curriculum", colorToken: "--color-grounding-curriculum", meaning: "Decided from the ratified domain-chain curriculum of the question's subject.", adr: "ADR-0262 / ADR-0162", evidence: "grounding_source is curriculum." },
} satisfies BadgeMeta<GroundingSource>;
export const safetyVerdictMeta = {

View file

@ -37,6 +37,8 @@ export enum GroundingSource {
PARTIAL = "partial",
OOV = "oov",
NONE = "none",
DEDUCTION = "deduction",
CURRICULUM = "curriculum",
}
export enum SafetyVerdict {

View file

@ -67,6 +67,8 @@
--color-grounding-partial: #6faea4;
--color-grounding-oov: #e9a94f;
--color-grounding-none: #6f7b8c;
--color-grounding-deduction: #8d8cff;
--color-grounding-curriculum: #58bfe0;
--color-state-info-bg: #10253a;
--color-state-info-border: #23648e;

View file

@ -45,6 +45,8 @@ export const tokens = {
"color-grounding-partial": "#6faea4",
"color-grounding-oov": "#e9a94f",
"color-grounding-none": "#6f7b8c",
"color-grounding-deduction": "#8d8cff",
"color-grounding-curriculum": "#58bfe0",
"color-state-info-bg": "#10253a",
"color-state-info-border": "#23648e",
"color-state-info-text": "#bde8ff",

View file

@ -14,7 +14,15 @@ export type ErrorCode =
export type Backend = "numpy" | "mlx" | "rust" | "unknown";
export type MutationMode = "read_only" | "runtime_turn";
export type GroundingSource = "pack" | "teaching" | "vault" | "partial" | "oov" | "none";
export type GroundingSource =
| "pack"
| "teaching"
| "vault"
| "partial"
| "oov"
| "none"
| "deduction"
| "curriculum";
export type TraceIntegrity = "pipeline_trace" | "legacy_unhashed";
export type PipelineEvidenceStatus = "recorded" | "missing_evidence";
export type CognitivePipelineStageKind =

View file

@ -13,6 +13,7 @@ from urllib.parse import parse_qs, unquote, urlparse
from chat.runtime import ChatRuntime
from core.cognition.pipeline import CognitiveTurnPipeline
from core.epistemic_state import (
GROUNDING_SOURCES,
clearance_from_verdicts,
coerce_normative_clearance,
epistemic_state_for_grounding_source,
@ -712,12 +713,18 @@ def _with_turn_cost_and_id(
def _coerce_grounding_source(value: object) -> str:
"""Floor an unregistered grounding label to ``"none"``, preserve the rest.
Reads ``core.epistemic_state.GROUNDING_SOURCES`` rather than restating the
members. A hand-copied whitelist here fell behind the ratified set when
deduction serving went ON (ADR-0256) and silently recorded proved answers
as ungrounded; deriving it means a new grounding source is registered once,
in one file, and this boundary follows. The floor itself is deliberate and
stays: an unregistered label is not grounding evidence, and the record must
not imply it is.
"""
text = str(value or "none").strip().lower()
return (
text
if text in {"pack", "teaching", "vault", "partial", "oov", "none"}
else "none"
)
return text if text in GROUNDING_SOURCES else "none"
def _identity_verdict(identity_score: object | None) -> TurnVerdict | None: