diff --git a/core/cli_test.py b/core/cli_test.py index cebd2880..7c4e45cc 100644 --- a/core/cli_test.py +++ b/core/cli_test.py @@ -67,6 +67,16 @@ TEST_SUITES: dict[str, tuple[str, ...]] = { # substantive override of the served surface. Cheap, and the axis is # invisible until it breaks silently. "tests/test_prior_surface_deduction_binding.py", + # ADR governance. An ADR that governs a default-ON flag records a + # decision already in force, so leaving it "Proposed" makes the + # governance record assert something false about the running system — + # which is what happened to ADR-0256 while deduction serving was live. + # Filesystem + regex only, ~1.2s, and it is the record for a serving + # path, so it belongs on the pre-push gate rather than under `full`. + # test_adr_index.py rides along: it landed in #113 registered in NO + # curated suite, which is the same silent-red shape it exists to catch. + "tests/test_adr_status_governance.py", + "tests/test_adr_index.py", ), "runtime": ( "tests/test_chat_runtime.py", @@ -97,6 +107,11 @@ TEST_SUITES: dict[str, tuple[str, ...]] = { "tests/test_math_composition_ratification.py", "tests/test_teaching_coverage_cli.py", "tests/test_proposal_queue.py", + # The ratification ceremony is the only path that turns a reviewed + # proposal into a routable ratified chain — i.e. the one mechanism that + # can move the curriculum-volume constraint. It landed in #113 in NO + # curated suite, so its 14 tests ran only under `full`. + "tests/test_ratification_ceremony.py", ), "packs": ( "tests/test_pack_draft_serve_boundary.py", diff --git a/docs/adr/ADR-0254-grounded-open-hedge-arm.md b/docs/adr/ADR-0254-grounded-open-hedge-arm.md index 7e48c440..b88606dc 100644 --- a/docs/adr/ADR-0254-grounded-open-hedge-arm.md +++ b/docs/adr/ADR-0254-grounded-open-hedge-arm.md @@ -1,6 +1,6 @@ # ADR-0254: Grounded-Open Hedge Arm for the Shadow Coherence Gate -**Status:** Proposed (predicate pre-authorized by Joshua Shay, 2026-07-22 weekly-audit ruling T13 decision 2; ratify on PR #103 merge) +**Status:** Accepted — ratified by Joshua Shay via the PR #103 merge (`da3447e9`, 2026-07-23), discharging this ADR's own ratify-on-merge predicate (pre-authorized by the 2026-07-22 weekly-audit ruling T13 decision 2) **Date:** 2026-07-23 **Deciders:** Joshua Shay (ruling authority) + Claude (implementation) **Companion docs:** [`ADR-0036-safety-refusal-policy.md`](ADR-0036-safety-refusal-policy.md), [`ADR-0037-per-predicate-ethics-refusal.md`](ADR-0037-per-predicate-ethics-refusal.md), [`ADR-0038-hedge-injection.md`](ADR-0038-hedge-injection.md), [`ADR-0252-problem-solving-paradigm-consolidation.md`](ADR-0252-problem-solving-paradigm-consolidation.md) diff --git a/docs/adr/ADR-0256-deduction-serve-earned-license.md b/docs/adr/ADR-0256-deduction-serve-earned-license.md index ca84986e..00df0c2c 100644 --- a/docs/adr/ADR-0256-deduction-serve-earned-license.md +++ b/docs/adr/ADR-0256-deduction-serve-earned-license.md @@ -1,6 +1,6 @@ # ADR-0256: Deduction Serving Governed by an Earned Reliability License -**Status:** Proposed (deduction-serve arc, Phase 3 — ratify-on-merge) +**Status:** Accepted — ratified by Joshua Shay via the deduction-serve Phase 5 merge (`1a6ccaf9`, 2026-07-23), discharging this ADR's own ratify-on-merge predicate. The `deduction_serving_enabled` default was flipped False → True by a separate, explicit ratification (`5dd61803`, 2026-07-24) **Date:** 2026-07-23 **Deciders:** Joshua Shay (ruling authority) + Claude (implementation) **Companion docs:** [`docs/research/deduction-serve-arc-phase0-baseline-2026-07-23.md`](../research/deduction-serve-arc-phase0-baseline-2026-07-23.md), [`docs/research/deduction-serve-arc-phase1-turn-spine-2026-07-23.md`](../research/deduction-serve-arc-phase1-turn-spine-2026-07-23.md), [`docs/research/deduction-serve-arc-phase2-eval-lane-2026-07-23.md`](../research/deduction-serve-arc-phase2-eval-lane-2026-07-23.md), [`docs/adr/ADR-0175-...`](.) (calibrated learning), [`docs/adr/ADR-0199-...`](.) (cross-domain arena), [`docs/adr/ADR-0206-...`](.) (response-governance bridge) diff --git a/docs/adr/ADR-0257-english-clause-argument-band.md b/docs/adr/ADR-0257-english-clause-argument-band.md index 048f6f47..c91e6b6f 100644 --- a/docs/adr/ADR-0257-english-clause-argument-band.md +++ b/docs/adr/ADR-0257-english-clause-argument-band.md @@ -1,6 +1,6 @@ # ADR-0257 — English-clause argument band (Band v2-EN): opaque-atom propositional serving -- **Status:** Proposed +- **Status:** Accepted — ratified by Joshua Shay via the PR #107 merge (`9405cf19`, 2026-07-23) - **Date:** 2026-07-23 - **Relates to:** ADR-0256 (deduction-serve earned license), ADR-0201 (ROBDD keystone), ADR-0218 (proof-carrying substrate), ADR-0175/0199 (calibrated diff --git a/docs/adr/ADR-0258-member-chain-band.md b/docs/adr/ADR-0258-member-chain-band.md index 2744cfa2..8cbad804 100644 --- a/docs/adr/ADR-0258-member-chain-band.md +++ b/docs/adr/ADR-0258-member-chain-band.md @@ -1,6 +1,6 @@ # ADR-0258 — Member-chain band (Band v3-MEM): singular membership + universal premises -- **Status:** Proposed +- **Status:** Accepted — ratified by Joshua Shay via the PR #108 merge (`dd2245a7`, 2026-07-24) - **Date:** 2026-07-23 - **Relates to:** ADR-0257 (Band v2-EN — this is its scope-out #1), ADR-0256 (earned license), ADR-0201 (ROBDD keystone), ADR-0175/0199 (calibrated diff --git a/docs/adr/ADR-0259-conditional-membership-fusion-band.md b/docs/adr/ADR-0259-conditional-membership-fusion-band.md index fffee6b9..64e6d02e 100644 --- a/docs/adr/ADR-0259-conditional-membership-fusion-band.md +++ b/docs/adr/ADR-0259-conditional-membership-fusion-band.md @@ -1,6 +1,6 @@ # ADR-0259 — Conditional-membership fusion band (Band v4-CM) -- **Status:** Proposed +- **Status:** Accepted — ratified by Joshua Shay via the PR #109 merge (`5224b5e0`, 2026-07-24) - **Date:** 2026-07-23 - **Relates to:** ADR-0258 (Band v3-MEM — this is its scope-out #1), ADR-0257 (Band v2-EN — the connective grammar this band reuses), ADR-0256 (earned diff --git a/docs/adr/ADR-0260-verb-predicate-band.md b/docs/adr/ADR-0260-verb-predicate-band.md index fcbe04e1..f82a04c7 100644 --- a/docs/adr/ADR-0260-verb-predicate-band.md +++ b/docs/adr/ADR-0260-verb-predicate-band.md @@ -1,6 +1,6 @@ # ADR-0260 — Band v5-VP: verb-predicate arguments, decided -- **Status:** Proposed +- **Status:** Accepted — ratified by Joshua Shay via the PR #111 merge (`2a82c8a3`, 2026-07-24) - **Date:** 2026-07-24 - **Arc:** deduction-serve (ADR-0256 → 0257 → 0258 → 0259 → this) - **Governs:** `generate/proof_chain/verb.py`, the `en_verb_*` shape-bands, diff --git a/docs/adr/ADR-0261-existential-witness-band.md b/docs/adr/ADR-0261-existential-witness-band.md index b7352aac..98ab51c5 100644 --- a/docs/adr/ADR-0261-existential-witness-band.md +++ b/docs/adr/ADR-0261-existential-witness-band.md @@ -1,6 +1,6 @@ # ADR-0261 — Band v6-EX: existential arguments, decided -- **Status:** Proposed +- **Status:** Accepted — ratified by Joshua Shay via the `feat/existential-band` merge (`a8488e9c`, 2026-07-24) - **Date:** 2026-07-24 - **Arc:** deduction-serve (ADR-0256 → 0257 → 0258 → 0259 → 0260 → this) - **Governs:** `generate/proof_chain/exist.py`, the `en_exist_*` shape-bands, diff --git a/docs/adr/ADR-0262-curriculum-grounded-serving.md b/docs/adr/ADR-0262-curriculum-grounded-serving.md index 955e6e44..5659794d 100644 --- a/docs/adr/ADR-0262-curriculum-grounded-serving.md +++ b/docs/adr/ADR-0262-curriculum-grounded-serving.md @@ -1,6 +1,6 @@ # ADR-0262 — Curriculum-grounded serving: exams answered from what was taught -- **Status:** Proposed +- **Status:** Accepted — ratified by Joshua Shay via the `feat/curriculum-serve-physics` merge (`0ae54ebb`, 2026-07-24). Accepting this ADR does NOT enable `curriculum_serving_enabled`, which stays False pending ratified curriculum volume (§5.1) - **Date:** 2026-07-24 - **Arc:** generalization Phase 2 (docs/plans/generalization-arc-2026-07-24.md §4) - **Governs:** `teaching/curriculum_premises.py`, `chat/curriculum_surface.py`, diff --git a/docs/adr/ADR-0263-ratified-ledger-bridge.md b/docs/adr/ADR-0263-ratified-ledger-bridge.md index 7d4536d8..a95206fe 100644 --- a/docs/adr/ADR-0263-ratified-ledger-bridge.md +++ b/docs/adr/ADR-0263-ratified-ledger-bridge.md @@ -1,6 +1,6 @@ # ADR-0263 — The ratified-ledger bridge -- **Status:** Proposed +- **Status:** Accepted — ratified by Joshua Shay via the `feat/curriculum-serve-physics` merge (`0ae54ebb`, 2026-07-24). Amended 2026-07-25 with rule 5 (ledger-absence policy declared, not passed) - **Date:** 2026-07-24 - **Arc:** generalization Phase 3.3 (docs/plans/generalization-arc-2026-07-24.md §2) - **Governs:** `core/ratified_ledger.py` and the three adapters over it — diff --git a/docs/research/adr-status-governance-2026-07-25.md b/docs/research/adr-status-governance-2026-07-25.md new file mode 100644 index 00000000..4425f394 --- /dev/null +++ b/docs/research/adr-status-governance-2026-07-25.md @@ -0,0 +1,126 @@ +# ADR Status Governance — Nine Stamps and an Enforcement Pin + +**Date:** 2026-07-25 · Follow-on to the assessment-verification arc (PR #113). +Evidence + mechanism only. No flag was touched; no ADR's *content* was changed. + +## 1. What was wrong + +Nine ADRs were merged into `main` and left stamped `Proposed`: + +| ADR | ratifying merge | why the stamp was contradictory | +|---|---|---| +| 0254 | PR #103 → `da3447e9` (2026-07-23) | its own status line said *"ratify on PR #103 merge"* | +| 0256 | `1a6ccaf9` (2026-07-23) | its own status line said *"ratify-on-merge"*, **and** it governs `deduction_serving_enabled`, ratified `True` on 2026-07-24 | +| 0257 | PR #107 → `9405cf19` | Band v2-EN, live in the serving cascade | +| 0258 | PR #108 → `dd2245a7` | Band v3-MEM, live | +| 0259 | PR #109 → `5224b5e0` | Band v4-CM, live | +| 0260 | PR #111 → `2a82c8a3` | Band v5-VP, live | +| 0261 | `a8488e9c` | Band v6-EX, live | +| 0262 | `0ae54ebb` | curriculum-grounded serving (flag still OFF — see §3) | +| 0263 | `0ae54ebb` | ratified-ledger bridge, consumed by three capabilities | + +Every merge above was verified an ancestor of `main` before stamping +(`git merge-base --is-ancestor`), and each ADR's ratifying merge was derived +from the commit that *added* the file, not assumed. + +ADR-0256 is the sharp case. It governs a flag that was ratified ON and serving +live traffic, so the governance record asserted "this decision is not yet made" +about a decision already in force. That is the same asymmetry the arc that just +closed found in `workbench/api.py`: **the honest path degrades, the stale +record lies.** An unregistered grounding label reported +`epistemic_state_needed`; a hand-copied whitelist reported `none`. Here, an +unwritten ADR would have been a visible gap; a `Proposed` one that is actually +in force is a false statement. + +## 2. What the stamps say + +Status lines now record the *actual* ratifying act rather than inventing a +ceremony: `Accepted — ratified by Joshua Shay via (, )`. +Merge authority in this repo is Shay's alone (`AGENTS.md`: no merge automation; +green PRs sit until explicit authorization), so the merge **is** the ratifying +act and naming it is traceable rather than decorative. + +## 3. Accepting an ADR is not flipping its flag + +ADR-0262's stamp says so explicitly. `curriculum_serving_enabled` stays +`False`; its blocker is ratified curriculum volume (§5.1), and eleven live +bands remain 24×–73× short of the entailed-bucket floor — re-measured +2026-07-25 against the live loader, unchanged from the Tier-S S6 figures +(physics `causal` 7, `modal` 9, `systems_software` `sequence` 3, +`mathematics_logic` `modal` 4). An accepted ADR records a decision about +*mechanism*; a flipped flag records a decision about *earned evidence*. Two +different rulings, and this document conflates neither. + +## 4. The pin + +`tests/test_adr_status_governance.py`, registered in the `smoke` suite (the +pre-push gate). Two independent invariants, both mutation-checked — reverting +ADR-0256 to `Proposed` fails each one separately: + +1. **A default-ON flag is not governed by a `Proposed` ADR.** The + flag → ADR mapping is **derived** by walking `core/config.py` for + `: bool = True` and reading the ADR references out of the preceding + comment block. Deliberately not a hand-written table: a second copy of a + closed set that falls behind the original in silence is the exact defect + ADR-0256's arc fixed one file over. +2. **A `ratify-on-merge` predicate cannot coexist with `Proposed`.** Self + discharging and needs no bookkeeping: the file being present on `main` *is* + the merge having happened, so the two states are contradictory by + construction. + +Plus `test_config_flag_parse_is_not_vacuous`, because a derivation that silently +parses zero flags would make every other assertion pass on an empty set — +silence reading as success. + +Measured: **314 passed in 1.18s** standalone; 2 failed / 312 passed under +mutation. + +Current binding scope is exactly one flag/ADR pair +(`deduction_serving_enabled` → 0256), because only one of the 32 runtime bool +flags is both default-True and ADR-citing. Narrow today by construction, and it +widens automatically the moment another earned path is ratified ON — which is +the point. + +## 5. Two orphaned test files, registered in passing + +`tests/test_adr_index.py` (5 tests) and `tests/test_ratification_ceremony.py` +(14) landed in PR #113 in **no** curated suite. `full` is the directory +`tests/`, so they ran there and nowhere that gates. Nineteen tests outside every +gate — including the ceremony, the one mechanism that can move the +curriculum-volume constraint, and an index test whose whole purpose is catching +staleness. + +That is the **fifth** instance of this shape: a stale suite tuple (S5), a stale +lane roster (#113), a masked lane pin (S2), an unregistered grounding label +(#113), and now unregistered test files. Registered `test_adr_index.py` into +`smoke` alongside the new pin, and `test_ratification_ceremony.py` into +`teaching` beside `test_proposal_queue.py`. + +## 6. Deliberately NOT fixed — recorded so it is not re-found + +The ADR corpus is 312 files and its status vocabulary has real drift: + +| status token | files | +|---|---:| +| `accepted` | 201 | +| `proposed` | 67 | +| *(status line unparseable)* | **27** | +| `draft` | 4 | +| `ratified` | 3 | +| `accepted.` / `active` / `phase` | 2 each | +| `implemented` / `empirical` / `superseded` | 1 each | + +A closed-vocabulary assertion over all 312 would fail on ~35 pre-existing files +and get muted or `xfail`ed, which is worse than no check — a muted gate reads as +coverage. So the pin's scope is exactly the ADRs whose status is load-bearing +*now*, and it fails loudly on any of *those* it cannot parse rather than +skipping it. + +**The open item:** normalize the 27 unparseable status lines and collapse +`ratified`/`draft`/`active`/`implemented` into the intended lifecycle, then +widen the pin to a closed vocabulary. Sizeable, mechanical, and a good +delegation candidate — it is a docs sweep with a test as its acceptance +criterion, not a design question. + +Relates to [[project-generalization-arc]], `docs/adr/INDEX-by-domain.md`, +`docs/handoff/BRIEF-CLOSE-assessment-verification-2026-07-25.md`. diff --git a/tests/test_adr_status_governance.py b/tests/test_adr_status_governance.py new file mode 100644 index 00000000..3007e6d1 --- /dev/null +++ b/tests/test_adr_status_governance.py @@ -0,0 +1,176 @@ +"""An ADR that governs live behaviour must not still be stamped ``Proposed``. + +Nine ADRs (0254, 0256-0263) sat at ``Proposed`` while merged into ``main``, +two of them carrying an explicit ``ratify-on-merge`` predicate that the merge +had already discharged, and one -- ADR-0256 -- governing +``deduction_serving_enabled``, which was ratified ``True`` and serving live +traffic. Nothing failed. The governance record and the running system +disagreed for a day and the only reason it surfaced was a human reading the +files. + +This is the "pinned list goes stale" family that has now been caught four +separate ways in this repo (a stale suite tuple, a stale lane roster, an +unregistered grounding label, a masked lane pin). Every previous instance was +fixed by making the divergence loud. This module does that for ADR status. + +Deliberately NOT a repo-wide status-vocabulary check. The 312-file ADR corpus +carries real drift -- 27 files whose status line this module's regex cannot +parse, plus ``draft`` / ``ratified`` / ``active`` / ``accepted.`` variants. +Normalizing that is a separate, larger job (recorded in +``docs/research/adr-status-governance-2026-07-25.md``). Asserting a closed +vocabulary here would fail on ~35 pre-existing files and get muted, which is +worse than no check. So the scope is exactly the ADRs whose status is +*load-bearing right now*, and the parser fails loudly on any of those it cannot +read rather than skipping it. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[1] +ADR_DIR = REPO_ROOT / "docs" / "adr" +CONFIG_PATH = REPO_ROOT / "core" / "config.py" + +#: ``**Status:** X`` / ``- **Status**: X`` / ``**Status**: **X**`` all appear in +#: the corpus. Tolerant on the label, strict on capturing the remainder. +_STATUS_RE = re.compile( + r"^[ \t]*(?:[-*][ \t]*)?\*\*Status\*{0,2}[ \t]*:?[ \t]*\*{0,2}[ \t]*:?[ \t]*(?P.+?)[ \t]*$", + re.MULTILINE, +) +_BOOL_FIELD_RE = re.compile(r"^\s*(?P\w+)\s*:\s*bool\s*=\s*(?PTrue|False)\b") +_ADR_REF_RE = re.compile(r"ADR-(\d{4})") +_RATIFY_ON_MERGE_RE = re.compile(r"ratify[- ]on[- ]merge|ratify on PR", re.IGNORECASE) + + +def _adr_paths() -> dict[str, Path]: + """Map a four-digit ADR number to its file. + + Numbers are not unique in principle (``ADR-0131.3`` exists), so the key is + the leading four digits and the first match wins deterministically by name. + """ + out: dict[str, Path] = {} + for path in sorted(ADR_DIR.glob("ADR-*.md")): + match = re.match(r"ADR-(\d{4})", path.name) + if match: + out.setdefault(match.group(1), path) + return out + + +def _status_of(path: Path) -> str | None: + """Return the first status value in ``path``, or ``None`` if unparseable.""" + match = _STATUS_RE.search(path.read_text(encoding="utf-8")) + if match is None: + return None + return match.group("value").strip().strip("*").strip() + + +def _is_proposed(status: str) -> bool: + return status.lower().lstrip("*").startswith("proposed") + + +def _flag_adr_citations() -> dict[str, tuple[str, frozenset[str]]]: + """Derive ``flag -> (default, cited ADR numbers)`` from ``core/config.py``. + + Derived, never restated. A hand-copied mapping here would be the very + defect ADR-0256's own arc fixed in ``workbench/api.py``: a second copy of a + closed set that falls behind the original in silence. A flag's comment + block citing ``ADR-NNNN`` is read as a claim that the ADR governs the flag. + """ + lines = CONFIG_PATH.read_text(encoding="utf-8").splitlines() + out: dict[str, tuple[str, frozenset[str]]] = {} + for index, line in enumerate(lines): + field = _BOOL_FIELD_RE.match(line) + if field is None: + continue + comment: list[str] = [] + cursor = index - 1 + while cursor >= 0: + stripped = lines[cursor].strip() + if stripped.startswith("#"): + comment.append(stripped) + elif not stripped and not comment: + pass # blank lines before the block start are skipped + else: + break + cursor -= 1 + cited = frozenset(_ADR_REF_RE.findall("\n".join(comment))) + out[field.group("name")] = (field.group("default"), cited) + return out + + +def test_config_flag_parse_is_not_vacuous() -> None: + """Guard the derivation itself. + + If ``core/config.py`` is reformatted such that the field regex stops + matching, every downstream assertion would pass on an empty set and this + module would go quietly blind -- silence reading as success, which is the + failure mode it exists to prevent. + """ + flags = _flag_adr_citations() + assert len(flags) >= 25, f"expected the runtime config's bool flags; parsed only {len(flags)}" + assert any( + default == "True" for default, _ in flags.values() + ), "no default-True flag parsed — the field regex has drifted from core/config.py" + assert any( + cited for _, cited in flags.values() + ), "no flag cites an ADR — the comment-block walker has drifted" + + +@pytest.mark.parametrize( + ("flag", "adr"), + sorted( + (flag, adr) + for flag, (default, cited) in _flag_adr_citations().items() + if default == "True" + for adr in cited + ), +) +def test_default_on_flag_is_not_governed_by_a_proposed_adr(flag: str, adr: str) -> None: + """A flag that is ON by default is serving live traffic. + + The ADR that governs it therefore records a decision already in force. + Leaving it ``Proposed`` makes the governance record assert something false + about the running system -- exactly the asymmetry ADR-0256's arc found in + ``workbench/api.py``, where the honest path degraded and the stale copy + lied. + + If this fails because a flag's comment cites an ADR only as background + rather than as its governing decision, move the citation into prose outside + the flag's comment block. Do not weaken the assertion. + """ + paths = _adr_paths() + assert adr in paths, f"{flag} cites ADR-{adr}, which does not exist in docs/adr/" + + status = _status_of(paths[adr]) + assert status is not None, ( + f"ADR-{adr} governs default-on flag {flag!r} but its status line is unparseable; " + "a load-bearing ADR must declare a readable status" + ) + assert not _is_proposed(status), ( + f"{flag} defaults to True (live serving) but ADR-{adr} is still {status!r}. " + "Either stamp the ADR with the decision that put the flag on, or turn the flag off." + ) + + +@pytest.mark.parametrize("adr_path", sorted(ADR_DIR.glob("ADR-*.md")), ids=lambda p: p.name) +def test_ratify_on_merge_predicate_is_discharged(adr_path: Path) -> None: + """An ADR whose status says "ratify on merge" cannot be merged AND Proposed. + + ADR-0254 (``ratify on PR #103 merge``) and ADR-0256 (``ratify-on-merge``) + both sat ``Proposed`` after the merges that were supposed to ratify them. + The predicate is self-discharging: the file being present on ``main`` IS + the merge having happened, so the two states are contradictory by + construction and need no external bookkeeping to detect. + """ + status = _status_of(adr_path) + if status is None or not _is_proposed(status): + return + assert not _RATIFY_ON_MERGE_RE.search(status), ( + f"{adr_path.name} is on main and still {status!r}. Its own status line makes the " + "merge the ratifying act, so the merge already discharged it — stamp it Accepted " + "and record which merge did so." + )