diff --git a/AGENTS.md b/AGENTS.md index acd965ab..957eaead 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -227,54 +227,76 @@ alias, flag, suite, and document is something that has to be kept true forever. Prefer removing to adding, and prefer shrinking a gap by deletion (costs nothing) over closing it by addition (costs forever). -**2. Master the little details and the big ones handle themselves.** -This is not a metaphor about care — it is a claim about causation. Large -architectural failures in this repo have consistently decomposed into small, +**2. Don't build a ratchet to guard a ratchet.** +The corollary of 1, and the one this repository paid for: when the fix for an +enforcement mechanism is another enforcement mechanism, the smell is not +insufficient enforcement — it is that the thing being policed should not exist. +A membership ratchet needed a reachability ratchet to mean anything; what +actually shrank the gap was deleting four suites nobody called, which cost +nothing and needed no pin at all. Look downstream of where the pain is felt. + +**3. Master the little details and the big ones handle themselves.** +Not a metaphor about care — a claim about causation. Large architectural +failures in this repo have consistently decomposed into small, individually-boring divergences that nobody closed. Get the small ones exactly right and the large ones do not form. -**3. A mechanism whose failure state is indistinguishable from its success +**4. A mechanism whose failure state is indistinguishable from its success state is not a guarantee.** -The dominant defect class here: a curated suite no gate invokes, a pin never -observed failing, a digest nothing re-derives, a verdict on an unmerged -branch, a decision recorded only in conversation. All of them look exactly -like working. **Red before green** — if you cannot make a new pin fail on -purpose, you have not shipped a guarantee, you have shipped a story about one. +The dominant defect class in this repository: a curated suite no gate invokes, +a pin never observed failing, a digest nothing re-derives, a verdict on an +unmerged branch, a predicate that passes vacuously on the input it was written +for, a decision recorded only in conversation. Every one of them looks exactly +like working. +**So design the failure to look different, and prove that it does.** Red before +green: if you cannot make a new pin fail on purpose, you have not shipped a +guarantee, you have shipped a story about one. -**4. Prefer a record that contradicts nothing to a record that impresses.** +**5. Prefer a record that contradicts nothing to a record that impresses.** A document, docstring, banner, or status field that disagrees with the code at a load-bearing point is worse than its absence, because it converts *"I should check"* into *"I already checked."* Recorded prose with no re-derivable artifact is testimony, not evidence. When a record and reality diverge, that is a defect with the same severity as a wrong answer. -**5. Consecutive unclear problems are a signal to stop and look wider.** +**6. Consecutive unclear problems are a signal to stop and look wider.** One surprising failure is a bug. Several in a row that are not individually obvious means the frame is wrong. Stop patching, step back, and ask what the failures have in common before writing another fix. Almost always the answer is that there is too much going on, not too little. -**6. Be quick to be humble about mistakes, and quick to investigate yourself -for them. Always.** -When a result is challenged, check your own work before defending it. When you -were wrong, say so plainly, fix it, and **record the error next to the fix** -rather than overwriting the history so that the tree looks clean. An arc's -self-correction chain is the strongest evidence it has; deleting a mistake -deletes that evidence. +**7. Investigate yourself first.** +When a result is challenged, turn the standard inward before defending outward +— not because it is polite, but because it is where the error usually is. The +H-13 refcount fix was wrong on a claim stated confidently, and its tests passed +only because they mirrored a call pattern production no longer used. Two +adopted rulings were about to be recorded stronger than they were made: R-11 B +authorizes no gate, and R-8 C's floor had no number. All three were caught by +re-reading my own work against the source, none by anyone disagreeing with me. +Then say so plainly, fix it, and **record the error next to the fix** rather +than tidying the history. An arc's self-correction chain is its strongest +evidence; deleting a mistake deletes that evidence. -**7. Do not cut corners because the plan is large.** +**8. Do not cut corners because the plan is large.** Volume of work is not a licence for lower diligence — it is the condition that makes diligence load-bearing. Meticulousness is cheapest exactly when it feels most expensive. -**8. Fix what you find, when you find it.** +**9. Fix what you find, when you find it.** Do not leave a discovered defect unfixed and unrecorded on the grounds that it -is off-topic. The two exceptions, both explicit: work a later planned item is -*expected* to fix (say so, and name the item), and work that requires an -authority you do not hold (say so, and prepare it so the decision costs one -word). +is off-topic. Two exceptions, both explicit: work a later planned item is +*expected* to fix (say so, and name the item), and work requiring an authority +you do not hold (say so, and prepare it so the decision costs one word). -**9. Let the tree settle, then run the gate once.** +**10. A closed issue is not evidence the problem is gone.** +It is evidence that someone decided to stop tracking it. The closure is a +governance artifact; the condition it describes is a fact about the system, and +the two drift. G-7 was CLOSED and REOPENED-PARTLY inside a single arc — the +closure was accurate about *membership* and manufactured confidence about +*execution*. H-13 was closed with a fix that did not work. Before trusting a +closed item, a merged PR, or a passing suite, ask what it actually observed. + +**11. Let the tree settle, then run the gate once.** Never mutate the working tree while a validation run is in flight — the result describes a tree that no longer exists, and you will not notice. Land all edits, let it settle, run once, read the whole output. diff --git a/core/cli_test.py b/core/cli_test.py index af52cc97..a3a57e5c 100644 --- a/core/cli_test.py +++ b/core/cli_test.py @@ -129,6 +129,16 @@ TEST_SUITES: dict[str, tuple[str, ...]] = { # It deliberately does NOT decide which of the 19 belong on the gate — # that costs gate time and is Shay's call. <1s. "tests/test_suite_reachability.py", + # G-23 — DOMAIN_PACKS and the pack manifests state one fact twice, and + # only one direction was checked. domain_contract_predicates P3 validates + # domain_id -> a known domain; NOTHING validated DOMAIN_PACKS -> the + # manifest agrees, and P3 passes VACUOUSLY on a manifest with no + # domain_id at all. Measured 7 of 9 bound, 0 contradictory, 2 absent — + # both in philosophy_theology, a band queued to earn SERVE behind R-8. + # The existing predicate tests run only on synthetic tmp_path manifests + # and sit in no curated suite, so they could not have caught it. + # Reads 30 small JSON files, <1s. + "tests/test_domain_pack_binding.py", # G-22 — CLAIMS.md is a PUBLISHED capability artifact, deterministically # regenerable from the capability ledger + PINNED_SHAS. It published a # superseded evidence digest for deduction_serve_v1 for two days after diff --git a/docs/assessment/30-gap-register.md b/docs/assessment/30-gap-register.md index c12cd176..e17f1875 100644 --- a/docs/assessment/30-gap-register.md +++ b/docs/assessment/30-gap-register.md @@ -165,6 +165,13 @@ Own `use_salience`, the two underived constants, the self-narrowing budget feedb **The mechanism, and why it is G-7's story again:** one commit updated the lane, its verifier and its test, and missed one downstream artifact. The pin that guards that artifact — `tests/test_claims_md_is_current.py` — **exists and is an orphan in no curated suite**, so nothing surfaced it until this arc ran the full tree once. Registered on the gate as part of the fix. - **G-21 · The math reader decides 1.0% of `holdout_dev/v1`** *(new, 2026-07-28)* — measured while building the §5 corpus: `parse_and_solve` returns a selected graph for **5 of 500** held-out cases, all carrying the same relational skeleton (`compare_multiplicative`); on the public lane, 24/150. This is a sharper measurement of the comprehension frontier than G-3's construction count, on the corpus the project already treats as its held-out standard, and it is why ADR-0252 §5.1's four-structure corpus is not extractable today. Distinct from G-3 (which counts *constructions* the general reader admits); this counts *cases decided* on a standing eval corpus. **Authority:** the widening program (G-3) + a ruling on whether holdout decision-rate becomes a tracked lane metric. - **G-20 · The `refusal_reason` materialisation** — typed refusal evidence exists and is discarded at the public `str` boundary; the plumbing for materialisation already landed. Cross-listed as H-3. **Authority:** small ADR (anticipated by the ADR-0024 chain). +- **G-23 · The domain↔pack binding is stated twice and checked in one direction** *(new, 2026-07-28; **pinned** the same day)* — `core/capability/domains.py`'s `DOMAIN_PACKS` declares domain→pack membership for the capability ledger; each `packs/data//manifest.json` independently declares `domain_id`. `domain_contract_predicates.py` **P3** validates `domain_id → a known ledger domain` and is the only binding check that existed. Nothing validated the reverse, and **P3 passes vacuously on a manifest with no `domain_id` at all** — the same shape as its own `test_pack_without_contract_reports_absent`. **Measured: 7 of 9 bound, 0 contradictory, 2 absent** — `en_core_cognition_v1` and `en_core_meta_v1`, both placed in `philosophy_theology`, which is one of the four bands queued to earn a SERVE license behind R-8. The domain whose license is next to be earned is the one whose binding exists in only one place. + + *Why nothing caught it:* `tests/test_domain_contract_predicates.py` exercises P1–P9 entirely against synthetic `tmp_path` manifests — never against the real nine — and is itself in **no curated suite**. A predicate proven correct on fabricated input and never run on the real input is this arc's defect class stated precisely: its success state and *"it never ran here"* are indistinguishable. + + *How it was found, recorded because the method matters:* an external assessment proposed collapsing the pack/domain machinery, reasoning explicitly from a directory listing. Every specific it gave was falsified by opening the files (see the 2026-07-28 triage in `31-hindrance-audit.md`) — but the seam it pointed at was real. Bad method, useful direction; both halves are worth recording. + + **Pinned, not fixed** — `tests/test_domain_pack_binding.py`, on the gate. Freezes the 2-pack gap, fails on contradiction always and on absence unless baselined, enforces both directions, pins the reverse direction (a pack may not self-assign a ledger domain — clean across all 30 packs today), and guards its own parse against vacuity. **Four sabotages observed red.** The fix is deliberately *not* here: whether `en_core_cognition_v1` is *a philosophy_theology domain pack* or merely *grouped under that domain for ledger accounting* is a ratification-adjacent content question. **Authority:** R-8 / PR-14, where that band's license is decided. --- diff --git a/docs/assessment/31-hindrance-audit.md b/docs/assessment/31-hindrance-audit.md index f4f7dad0..ace1f9b3 100644 --- a/docs/assessment/31-hindrance-audit.md +++ b/docs/assessment/31-hindrance-audit.md @@ -163,3 +163,12 @@ For symmetry with the Candidate Register's "considered and not registered" — h - **The five unreconciled articulations** — dissolved by the taxonomy (D1), not a standing hindrance; the residue is one stale Draft banner (folded into H-8's amendment batch). - **Flag-gated conservatism itself** — seventeen dark flags is not inherently hindrance; *unregistered* darkness is (G-8). The posture may be exactly right; the register exists so that judgment can be made deliberately. - **Six proposals from two external assessments (2026-07-28)** — each verified against code and cleared, with the refuting file named: **EpistemicState compression** (the "unused" states are the vault's refuse-forcing states and the serving path's `UNVERIFIED_POSSIBLE` — `vault/store.py:99-103`, `chat/runtime.py:1329`; ADR-0206 already partitions the taxonomy per-state in `core/response_governance/policy.py:64-100`); **an EpistemicState×NormativeClearance product type** (forbidding DECODED+SUPPRESSED at the type level erases the refusal's evidence — H-3's failure rebuilt as a type constraint; the coupling already exists as policy, where the legitimate combinations are decided); **PID attestation on `lived_life.json`** (a reboot-resumed life's writer PID is dead by definition, so the check refuses every legitimate resume, and PID reuse defeats attestation — the same pid-file pitfalls the lock's own comment derives against); **path-based cycle skipping in the inductive closure** (would refuse sound transitive derivations; see H-8e — the docstring, not the code, is the wrong side); **a depth-enrichment session cache** (the cost it targets is already `@lru_cache`d at the pack layer, `chat/pack_resolver.py:19`; measure first — the 0.22% Rust precedent); **a unified reasoning engine presented as refactor** (changes `operator_invocation` bytes → `trace_hash`, silently widens capability by letting compose see closure-derived triples, and reduces entailment to set membership one merge after ADR-0265 ratified denial representation — the underlying facts are real and are recorded as design considerations under H-4/H-14). Triage rule kept from this round: **no external lead folds in without the one-more-file check** — every rejected proposal above failed on a file its author did not open. +- **Six architectural proposals from a third external assessment (2026-07-28), whose author states the method up front: *"I have enough from the directory and CLAIMS.md to reason seriously."*** That is the method `AGENTS.md` protocol rule 1 forbids, and the hedges mark where it broke — *"almost certainly a nested conditional or a big dict," "likely doing both," "may be operating at a resolution."* **One folded in (G-23); five falsified by opening the file:** + 1. ***`conftest.py` is "almost certainly the most dangerous file in the repo"; suite-membership logic has accumulated in it.*** **False.** It holds two autouse engine-state isolation fixtures, a QUARANTINE nodeid registry, a SLOW_FILES/SLOW_TESTS registry, `_is_slow`, and `pytest_collection_modifyitems` — which *stamps markers and never skips* (`"classification ONLY, never skipped here"`). Zero suite membership, zero gate-tier assignment, zero conditional inclusion. The 12KB is two data tables with per-entry justifications: already the declared-table pattern the proposal asks for. + 2. ***`cli_test.py` manages suite topology "through what is almost certainly a nested conditional or a big dict."*** **A flat dict — 17 keys, all tuple values, zero conditionals in the module body.** 463 lines, 171 of them comments (37%). The *surviving* half of the point — gate tier is a property of which suites the shell invokes, not of the suite — is real; but it is already **declared and verified**: `test_suite_reachability.py` declares `GATE_SUITES` and asserts it against `pre-push`/`local-ci.sh` precisely so the pin cannot go vacuous. The claim that this "makes pin 3's ratchet trivially implementable" describes a pin that shipped this arc. + 3. ***`field/` + `algebra/` + `morphology/` are a triad a caller must compose correctly or bypass the ratification contract; give them a single `serve()`.*** **Not a triad.** `algebra/` has 72 non-test importers (a foundational library), `field/` 15, **`morphology/` 3 — and `morphology/` is 2 files, 97 lines total.** No `def serve(` exists anywhere in `core/`, `chat/` or `generate/`; the blessed path is `ChatRuntime`. Top-level directories look like peers regardless of size, which is the entire content of this proposal. + 4. ***The epistemic grade space is richer than the serve layer consumes; collapse it.*** **All 15 `EpistemicState` members have non-test consumers** (min 1, max 13). Nothing is unconsumed. Compression was already examined and cleared in the round above, on different evidence. + 5. ***Give each of the 260+ ADRs a machine-readable `constraints:` block.*** The count is **314**, and the class already exists in the better direction: `test_adr_status_governance.py` parses `core/config.py`'s bool flags *with their cited ADR numbers* and fails when a default-ON flag is governed by a `Proposed` ADR, with a non-vacuity guard. **The proposal inverts this the wrong way** — 314 ratified, immutable decision records each carrying live assertions is 314 new places that must be kept true, in the repository whose dominant defect class is records drifting from reality. Either the ADR is edited when code moves (breaking ratification) or the assertion goes stale (H-8, at 314×). Machine-readability belongs in the **checker that reads the ADR**, not in the ADR. + 6. ***The unifying proposal — a root `contracts.toml` holding every declared relationship — is "the highest-leverage single change."*** The *principle* is already this repository's (ADR-0263 Rule 5: declared in the table, not the call site) and is implemented in the very file the assessment cites: `core/epistemic_state.py` declares `GroundingSource` as a closed `Literal` plus a runtime-iterable `GROUNDING_SOURCES` frozenset, single-sourced across the runtime, the lane validator, the Workbench API coercion, and a build-time enum-coverage test under `workbench-ui/` — and its docstring records the bug that pattern caught (unregistered `deduction`/`curriculum` labels floored to `"none"`, *"recording a proved answer as ungrounded in a durable audit artifact"*). Centralizing four tables with four different authorities and lifecycles into one file adds a fifth copy that must agree with four generators; it is a ratchet to guard ratchets (standing philosophy 2). **Cleared as a proposal, kept as an observation:** nobody can currently enumerate *"which declared tables exist and what enforces each"* — that is one page, folded into PR-5, not a new mechanism. + + **What it got right, and it is worth saying:** proposal 1 pointed at the pack/domain seam. Every specific was wrong, and reading the code there anyway found **G-23** — a real, load-bearing, one-directional check on the band whose license is next to be earned. Bad method, useful direction. The triage rule survives unchanged and earned a second demonstration: *every rejected proposal above failed on a file its author did not open.* diff --git a/docs/assessment/50-execution-plan.md b/docs/assessment/50-execution-plan.md index 5ec7a2fb..416e708d 100644 --- a/docs/assessment/50-execution-plan.md +++ b/docs/assessment/50-execution-plan.md @@ -289,6 +289,8 @@ Promoted out of a parenthetical in PR-4, because a parenthetical is how fifteen ### PR-5 · The flag-default register · **M** · G-8/H-6 `docs/specs/flag_register.md`: all **28** default-off flags plus the 4 default-on — current default, deliberate-posture vs accumulated-hesitancy, **what evidence flips it**, and named profiles (one-shot / eval / continuous-life) per R-4. Declared in the table, not the call site (ADR-0263 Rule 5). A pin asserts the register lists exactly the flags `core/config.py` defines — so a new flag cannot land unregistered. Includes the F-6 accrual resolution from R-3 and the N-6 docstring correction. +**Plus one page this PR is the natural home for (from the 2026-07-28 triage, proposal 6).** The repository's governing pattern is *declared in the table, not the call site* (ADR-0263 Rule 5) and it is implemented in many places — `GROUNDING_SOURCES`, `DOMAIN_PACKS`, `TEST_SUITES`, `GATE_SUITES`, `CONTINUOUS_LIFE_CONFIG_FLAGS`, the quarantine/slow registries, `PINNED_SHAS`. **Nobody can currently enumerate them, or say what enforces each.** That is a one-page index in the flag register's own document — *table → where declared → what pin makes it true* — and explicitly **not** a `contracts.toml` that centralizes them into a fifth copy needing agreement with four generators. The index is a reader's aid; the tables stay where their authority lives. + ### PR-6 · Failing pins for three doctrine-level prohibitions · **M** · G-9 — **LANDED 2026-07-28** (a) no-approximate-recall — a pin that fails if a cosine/ANN ranker is substituted on the recall path; (b) a bypass pin — governance not merely working when called, but *unbypassable*; (c) safety-pack non-swappability mechanically enforced. Each demonstrated red-then-green; a law with a pin that cannot fail is a hollow gate. @@ -334,6 +336,8 @@ Shipped as ADR-0264 R5, discharged 2026-07-26. The 16-premise cap does not exist **Under R-8 C the deliverable is now specific:** two entries in `CAPABILITY_LEDGERS` — `unknown`-serving and `entailed`-serving licensed as **different capabilities on different evidence** — the mix rule declared in the table rather than at the call site (ADR-0263 Rule 5), an **ADR-0264 §5 amendment** recording the ruling, then the seal. The four bands license the thing they have actually demonstrated at 660 decisions (correct refusal); nothing licenses entailment on a maximum of 9 cases. +**It also carries G-23's decision, because it is the same band.** `DOMAIN_PACKS` places `en_core_cognition_v1` and `en_core_meta_v1` in `philosophy_theology`; neither manifest declares a `domain_id`, so `domain_contract_predicates` P3 has never run on them. The gap is pinned (`tests/test_domain_pack_binding.py`, on the gate, four sabotages red) and deliberately not fixed: whether those two are *philosophy_theology domain packs* or merely *grouped under it for ledger accounting* decides what the license actually covers. Sealing that band's ledger without answering it licenses a domain whose membership is asserted in one place and denied by silence in the other. + **One sub-question the ruling leaves open, stated rather than assumed.** The packet's recommendation was *"C, with a floor from A applied to the entailed capability only"* — and **N was never named.** Adopting C adopts the floor in principle; the number is not derivable from the evidence, because the whole point is that no band is near it (max entailed volume = 9). This PR must therefore either carry a proposed N with its consequence measured, or land the two capabilities with the entailed one explicitly unlicensable-pending-N. It may not quietly ship without a floor — that would license entailment on 9 cases through an omission, which is the exact outcome C exists to prevent. ### Track B · The fabrications and the widening — *gated, see §6* diff --git a/docs/assessment/README.md b/docs/assessment/README.md index 86d1b7aa..ec0bbac9 100644 --- a/docs/assessment/README.md +++ b/docs/assessment/README.md @@ -14,8 +14,8 @@ A read-only, evidence-bearing assessment of CORE's cognitive-cycle design versus | [`04-phase2-findings.md`](04-phase2-findings.md) | 2 | Stage-coverage audit; corrections to Phase 0; findings F-1…F-5 | | [`20-component-cards/`](20-component-cards/) | 3 | Eight component cards: the four zero-subsystem zones + always-on, derivation organs, surface selection, attention | | [`05-phase3-findings.md`](05-phase3-findings.md) | 3 | Corrections C-1…C-5; findings F-6…F-10; the consolidated Phase-4 seed list | -| [`30-gap-register.md`](30-gap-register.md) | 4 | **The live gap register** — 22 entries (G-21, G-22 added during execution), 4 tiers, each with evidence + deciding authority; **supersedes `docs/gaps.md` and the substrate-liveness ratchet** per R-7 | -| [`31-hindrance-audit.md`](31-hindrance-audit.md) | 4 | Fourteen hindrances with fitness verdicts and better homes; eleven candidates examined and cleared (five in Phase 4, six in the 2026-07-28 external-assessment triage) | +| [`30-gap-register.md`](30-gap-register.md) | 4 | **The live gap register** — 23 entries (G-21, G-22, G-23 added during execution), 4 tiers, each with evidence + deciding authority; **supersedes `docs/gaps.md` and the substrate-liveness ratchet** per R-7 | +| [`31-hindrance-audit.md`](31-hindrance-audit.md) | 4 | Fourteen hindrances with fitness verdicts and better homes; **seventeen candidates examined and cleared** — five in Phase 4, six in the first 2026-07-28 external-assessment triage, six in the second (of which one, the pack/domain seam, survived as G-23) | | [`40-assessment.md`](40-assessment.md) | 5 | The synthesis | | [`50-execution-plan.md`](50-execution-plan.md) | 6 | **The execution plan** — five waves + five frontier tracks over every G/H entry, with the dependency gates and the risks. §0 carries **nine** corrections to the assessment found while sizing and executing it; **§2.1 carries the adopted ruling docket and its execution order** | | [`50-rulings.md`](50-rulings.md) | 6 | **The ruling packet** — R-1…R-14, each with evidence, options, a recommendation, and the exact diff that follows from each choice. **RULED 2026-07-28: twelve adopted, R-10 and R-14 stricken, §5 NO-GO ratified.** Wave 0's deliverable, and Wave 0 is closed | diff --git a/tests/test_domain_pack_binding.py b/tests/test_domain_pack_binding.py new file mode 100644 index 00000000..733f342f --- /dev/null +++ b/tests/test_domain_pack_binding.py @@ -0,0 +1,211 @@ +"""G-23 — `DOMAIN_PACKS` and the pack manifests state one fact in two places. + +**How this was found.** An external assessment proposed collapsing the +pack/domain machinery, reasoning from a directory listing. Every specific it +gave was wrong — ``core/protocol/`` is the trace protocol, not governance; +``core/capability`` is reporting surfaces; ``packs/domain_contract.py`` says of +itself *"intentionally read-only … does not promote capability status."* But it +pointed at the right seam, and reading the code there found a real one. + +**The seam.** ``core/capability/domains.py`` declares ``DOMAIN_PACKS`` — the +domain→pack membership the capability ledger reasons over. Each pack's +``packs/data//manifest.json`` independently declares ``domain_id``. Two +statements of one fact, and only one of the two directions is checked: + +* ``domain_contract_predicates.py`` **P3** validates ``domain_id`` → *a known + ledger domain*. It is the only binding check that exists. +* Nothing validates ``DOMAIN_PACKS`` → *the manifest agrees*. + +**P3 passes vacuously on a manifest with no ``domain_id`` at all** — the same +shape as its own ``test_pack_without_contract_reports_absent``. So a pack that +``DOMAIN_PACKS`` places in a domain, but whose manifest never says so, is +invisible to every check in the repository. Measured 2026-07-28: **7 of 9 bound, +0 contradictory, 2 absent** — ``en_core_cognition_v1`` and ``en_core_meta_v1``, +both placed in ``philosophy_theology``, which is one of the four bands queued to +earn a SERVE license behind R-8. The domain whose license is about to be earned +is the one whose binding exists in only one place. + +**And the existing tests could not have caught it.** +``tests/test_domain_contract_predicates.py`` exercises P1–P9 entirely against +synthetic ``tmp_path`` manifests — never against the real nine — and is itself +in no curated suite. A predicate proven correct on fabricated input, never run +on the real input, is this arc's defect class stated precisely: *its success +state and "it never ran here" are indistinguishable.* + +**What this pin does not do.** It does not add ``domain_id`` to the two +manifests. Whether ``en_core_cognition_v1`` is *a philosophy_theology domain +pack* or merely *grouped under that domain for ledger accounting* is a +ratification-adjacent content question, not an engineering one, and it is +routed to R-8 / PR-14 where that band's license is decided. This pin freezes +the state, fails on growth, and enforces both directions — so the gap is +visible and cannot widen while the decision is pending. +""" + +from __future__ import annotations + +import ast +import json +from pathlib import Path +from typing import Any + +_ROOT = Path(__file__).resolve().parents[1] +_DOMAINS_PY = _ROOT / "core" / "capability" / "domains.py" +_PACK_DATA = _ROOT / "packs" / "data" + +#: Packs that ``DOMAIN_PACKS`` places in a domain whose manifest declares no +#: ``domain_id``. This list may only SHRINK. A pack leaves it by gaining the +#: field, not by being deleted from here. +UNBOUND_BASELINE: frozenset[str] = frozenset( + { + "en_core_cognition_v1", + "en_core_meta_v1", + } +) + + +def _domain_packs() -> dict[str, tuple[str, ...]]: + """Read ``DOMAIN_PACKS`` without importing the package. + + ``core.capability`` imports ``chat`` imports ``generate`` imports numpy, so + a plain import drags the whole runtime in to read one dict. Executing the + module body is safe here: ``domains.py`` is pure data with no imports of + its own beyond ``__future__``. + """ + namespace: dict[str, Any] = {} + source = _DOMAINS_PY.read_text(encoding="utf-8") + exec(compile(ast.parse(source), str(_DOMAINS_PY), "exec"), namespace) # noqa: S102 + packs = namespace["DOMAIN_PACKS"] + assert isinstance(packs, dict), "DOMAIN_PACKS is not a dict" + return packs + + +def _manifest_domain_id(pack: str) -> str | None: + path = _PACK_DATA / pack / "manifest.json" + if not path.exists(): + return None + return json.loads(path.read_text(encoding="utf-8")).get("domain_id") + + +def test_parse_is_not_vacuous() -> None: + """A parse that silently matched nothing would make every pin below pass. + + That failure mode (N-1) has produced three false conclusions in this repo, + so it is asserted rather than assumed. + """ + packs = _domain_packs() + members = [p for members in packs.values() for p in members] + assert len(packs) >= 5, f"parsed only {len(packs)} domains from DOMAIN_PACKS" + assert len(members) >= 9, f"parsed only {len(members)} pack memberships" + assert list(_PACK_DATA.glob("*/manifest.json")), "no pack manifests found on disk" + + +def test_every_declared_pack_has_a_manifest() -> None: + """``DOMAIN_PACKS`` may not name a pack that does not exist on disk.""" + missing = sorted( + pack + for members in _domain_packs().values() + for pack in members + if not (_PACK_DATA / pack / "manifest.json").exists() + ) + assert not missing, ( + "DOMAIN_PACKS names packs with no manifest under packs/data: " f"{missing}" + ) + + +def test_manifest_domain_id_agrees_with_domain_packs() -> None: + """The forward direction P3 does not check: the table must match the pack. + + A *contradiction* is always a failure. An *absence* is a failure unless the + pack is in ``UNBOUND_BASELINE`` — which is a recorded gap with a named + owner (R-8 / PR-14), not a default. + """ + contradictions: list[str] = [] + undeclared_absences: list[str] = [] + + for domain, members in _domain_packs().items(): + for pack in members: + declared = _manifest_domain_id(pack) + if declared is None: + if pack not in UNBOUND_BASELINE: + undeclared_absences.append(f"{pack} (DOMAIN_PACKS: {domain})") + elif declared != domain: + contradictions.append( + f"{pack}: manifest says {declared!r}, DOMAIN_PACKS says {domain!r}" + ) + + assert not contradictions, ( + "pack manifests contradict DOMAIN_PACKS — one of the two is wrong and " + "the capability ledger reasons over the second:\n " + + "\n ".join(contradictions) + ) + assert not undeclared_absences, ( + "these packs are placed in a domain by DOMAIN_PACKS and declare no " + "domain_id, so P3 passes vacuously on them. Add domain_id to the " + "manifest, or add the pack to UNBOUND_BASELINE with the reason in your " + "commit message:\n " + "\n ".join(undeclared_absences) + ) + + +def test_no_manifest_claims_a_domain_the_table_does_not_grant() -> None: + """The reverse direction: a pack may not self-assign a ledger domain. + + Measured clean at 2026-07-28 across all 30 packs under ``packs/data``. It is + pinned because it is the direction a *content* edit would break — a new pack + manifest copying ``domain_id`` from a sibling is the obvious mistake, and it + would silently widen a domain the ledger licenses. + """ + granted = { + pack: domain for domain, members in _domain_packs().items() for pack in members + } + offenders: list[str] = [] + for manifest in sorted(_PACK_DATA.glob("*/manifest.json")): + declared = json.loads(manifest.read_text(encoding="utf-8")).get("domain_id") + if declared is None: + continue + pack = manifest.parent.name + if pack not in granted: + offenders.append(f"{pack}: claims {declared!r}, not in DOMAIN_PACKS at all") + elif granted[pack] != declared: + offenders.append( + f"{pack}: claims {declared!r}, DOMAIN_PACKS grants {granted[pack]!r}" + ) + assert not offenders, ( + "pack manifests claim ledger domains DOMAIN_PACKS does not grant:\n " + + "\n ".join(offenders) + ) + + +def test_unbound_baseline_has_no_stale_entries() -> None: + """Both directions — a pack that gains ``domain_id`` must leave the list. + + Same discipline as the suite-membership and reachability ratchets, for the + same reason: a baseline that keeps entries it no longer needs stops being a + measurement and becomes a story about one. That discipline has caught this + file's author twice. + """ + declared_packs = { + pack for members in _domain_packs().values() for pack in members + } + + vanished = sorted(p for p in UNBOUND_BASELINE if p not in declared_packs) + assert not vanished, ( + f"UNBOUND_BASELINE names packs DOMAIN_PACKS no longer places: {vanished}" + ) + + now_bound = sorted(p for p in UNBOUND_BASELINE if _manifest_domain_id(p) is not None) + assert not now_bound, ( + "these packs now declare domain_id (good) but are still listed as " + f"unbound — remove them from UNBOUND_BASELINE: {now_bound}" + ) + + +def test_recorded_gap_matches_the_measurement() -> None: + """Pin the number so the gap moving is a reviewed decision, not a drift. + + 2 of 9 at 2026-07-28. Shrinking it is the point, and it shrinks by a + ratification-adjacent content decision (R-8 / PR-14), not by an edit here. + """ + assert len(UNBOUND_BASELINE) <= 2, ( + f"unbound domain packs grew to {len(UNBOUND_BASELINE)} (was 2). The " + "ratchet only turns one way." + )