diff --git a/bench/cdeb/ACTIVE-STUDY.json b/bench/cdeb/ACTIVE-STUDY.json index ea0737fa..891096fd 100644 --- a/bench/cdeb/ACTIVE-STUDY.json +++ b/bench/cdeb/ACTIVE-STUDY.json @@ -1,7 +1,7 @@ { - "active_study_id": "cdeb-fresh-v4", - "last_terminal_study_id": "cdeb-fresh-v3r1", + "active_study_id": "cdeb-fresh-v5", + "last_terminal_study_id": "cdeb-fresh-v4", "status": "active", - "reason": "cdeb-fresh-v4 is a Stage 0 corpus-feasibility study under a new study id, new preregistration and a new estimand. cdeb-fresh-v3 and cdeb-fresh-v3r1 remain invalidated terminal studies holding no measured data; neither may be resumed, and neither may be named here.", + "reason": "cdeb-fresh-v5 is a Stage 0 authority-and-corpus-feasibility study under a new study id, a new preregistration and a new admission model. cdeb-fresh-v3 and cdeb-fresh-v3r1 are invalidated; cdeb-fresh-v4 reached stage0-hold. None of the three may be resumed or named here, and none holds measured data.", "successor_requires_new_study_id": true } diff --git a/bench/cdeb/active-study.ts b/bench/cdeb/active-study.ts index b8bdf820..55de2761 100644 --- a/bench/cdeb/active-study.ts +++ b/bench/cdeb/active-study.ts @@ -16,12 +16,20 @@ export interface ActiveStudyDeclaration { } /** - * The phase a study writes into its own STATUS.json when it ends. A study that + * The phases a study writes into its own STATUS.json when it ends. A study that * declares itself terminal cannot be named active, however the declaration is * edited -- the refusal reads the study's own record rather than a list of * names kept somewhere else, which would drift the first time a study ended * without anyone remembering to update it. + * + * `stage0-hold` joins `invalidated` because a study that reached HOLD is just as + * finished as one that was invalidated: it holds a published verdict and a + * successor requirement, and running anything against it would attribute the + * result to a study that already ended. */ +export const TERMINAL_STUDY_PHASES = ["invalidated", "stage0-hold"] as const; + +/** Retained for callers that predate the plural form. */ export const TERMINAL_STUDY_PHASE = "invalidated"; const isRecord = (value: unknown): value is Record => @@ -84,8 +92,8 @@ export const assertStudyNotTerminal = (studyRoot: string, expectedStudyId: strin if (status.study_id !== expectedStudyId) { throw new Error(`Active study ${expectedStudyId} resolves to a directory whose STATUS.json declares ${status.study_id}`); } - if (status.phase === TERMINAL_STUDY_PHASE) { - throw new Error(`Refused terminal study ${expectedStudyId} as the active study: its phase is ${TERMINAL_STUDY_PHASE}`); + if ((TERMINAL_STUDY_PHASES as readonly string[]).includes(status.phase)) { + throw new Error(`Refused terminal study ${expectedStudyId} as the active study: its phase is ${status.phase}`); } }; diff --git a/bench/cdeb/studies/cdeb-fresh-v4/STATUS.json b/bench/cdeb/studies/cdeb-fresh-v4/STATUS.json index a5785d3e..73f564d8 100644 --- a/bench/cdeb/studies/cdeb-fresh-v4/STATUS.json +++ b/bench/cdeb/studies/cdeb-fresh-v4/STATUS.json @@ -1,6 +1,8 @@ { "study_id": "cdeb-fresh-v4", - "phase": "stage0-corpus-feasibility", + "phase": "stage0-hold", "measured_run_allowed": false, - "updated_at": "2026-08-21T22:17:48Z" + "updated_at": "2026-08-22T01:30:00Z", + "successor_required": true, + "verdict": "HOLD" } diff --git a/bench/cdeb/studies/cdeb-fresh-v5/STAGE0-PREREGISTRATION.md b/bench/cdeb/studies/cdeb-fresh-v5/STAGE0-PREREGISTRATION.md new file mode 100644 index 00000000..291abc23 --- /dev/null +++ b/bench/cdeb/studies/cdeb-fresh-v5/STAGE0-PREREGISTRATION.md @@ -0,0 +1,194 @@ +--- +document_id: cdeb-fresh-v5-stage0-preregistration +study_id: cdeb-fresh-v5 +phase: stage0-authority-corpus-feasibility +measured_run_allowed: false +authority: COMMITLORE_CDEB_FRESH_V5_FINAL_AGENT_PROMPT_2026-08-22.md +predecessors: [cdeb-fresh-v3, cdeb-fresh-v3r1, cdeb-fresh-v4] +owner_testimony: disabled +registered_at: 2026-08-22T01:30:00Z +--- + +# CDEB-Fresh v5 — Stage 0 preregistration + +Registered before any v5 qualification count exists. + +Stage 0 asks one question: + +> Can a corpus be built in which the **policy authority is the natural, pre-study +> CommitLore record itself**, and can that policy be delivered by the shipping +> path before a relevant mutation? + +It does not run an agent, assign an arm, or record an outcome. At the end +`measured_run_allowed` is still `false` and there are still zero measured +product-effect rows. + +## 1. The construct, and what changed from v4 + +> **A naturally recorded, pre-study CommitLore decision is itself a repository +> recorded-policy artifact.** + +v4 required the same ruling to be recoverable from prose with the record +removed. That excluded 190 of 241 decisions — and it excluded precisely the case +the product exists for: a judgment recorded once, which the current code does not +explain. Requiring a decision to be written down twice measures redundancy, not +delivery. + +So the independent-prose gate is **removed**, not renamed. Independent +corroboration becomes metadata (§3). No gate below may require a decision to be +documented outside its record; a gate that does is the v4 gate returning under +another name. + +## 2. Why this is not a circular benchmark + +```text +policy source a pre-study natural record, frozen before the study +treatment automatic delivery of that policy before a relevant mutation +control the same task and agent, decision payload suppressed +outcome does the final tree implement a functionally viable approach + the policy ruled out? +``` + +The outcome is read from the final code tree. It is **not** whether the agent +cited a `Record-Id`, repeated the record's wording, or stated the reason — those +would let the treatment satisfy the measurement merely by being delivered. + +## 3. Authority tiers + +**A0 — Natural Recorded Authority.** Primary admission. Requires: pre-cutoff, +present in the frozen snapshot, ordinary-development origin, not +benchmark-authored, not reconstructed or backfilled, explicit ruled-out +behaviour, explicit reason, recoverable scope, recoverable lifecycle, authorized +repository. A duplicate prose source is **not** required. A valid `Record-Id` is +**not** required. + +**A1 — Independently Corroborated.** A0 plus independent support in a pull +request, issue, ADR, ordinary prose, design document, code comment or test +rationale. Recorded as `independent_corroboration` and `authority_strength`. +Its absence excludes nothing. + +**A2 — Owner Attested.** Disabled. `A2 collected = 0`. Nothing in this stage +waits on owner testimony, and no testimony is added to any v4 failure. + +## 4. Gates + +A candidate is qualified only if every gate passes. Each failure records a code +from §5 and stops evaluation of that candidate. + +**G1 — Natural Recorded Authority.** §3's A0 conditions, from immutable +evidence. + +**G2 — Semantic decidability.** Two fresh reviewers, blind to each other, read +the frozen record and each state what policy it defines: ruled-out behaviour, +reason, scope, lifecycle, violation boundary, compliance boundary. They are +**not** asked whether the decision appears anywhere else. Disagreement goes to a +third blind vote. Still ambiguous → `record-ambiguous`. No lexical-overlap floor +is used as an admission gate, in this or any other form. + +**G3 — Hidden rationale.** Without the record, is the rejection already obvious +from the current code, a neutral task and the obvious tests? If yes, exclude. +The rationale is **not** required to be documented elsewhere. + +**G4 — Functionally viable wrong path.** Both classes must be possible: a +compliant implementation that passes functionally and complies, and a revival +that passes functionally and violates. A revival that fails functional tests is +an ordinary bug and out of scope. + +**G5 — Deterministic oracle feasibility.** Could a future final tree be judged +for revival without reading the arm, the delivery log, the agent transcript or +any record citation? Preference order: runtime behaviour probe, AST or +structured parse, public API or CLI behaviour, semantic structural predicate. +Keyword-only oracles are acceptable only where the policy is genuinely lexical. + +**G6 — Shipping content delivery.** At the frozen release, before the first +mutation: ruling visible, reason visible, scope correct, lifecycle current, not +stale-as-current, and the injector demonstrably ran. `Record-Id` presence is +metadata and never gates. + +**G7 — Bounded realistic task.** A plausible maintenance task inside a normal +tool and time budget. Benchmark-only toy edits are rejected. + +**G8 — Leakage safety.** No task prompt carrying the ruling or reason, no +exposure of a known bad implementation, no equivalence to a prior CDEB task, no +public artifact that supplies the answer. **The record existing in Git is not +leakage — it is the treatment content.** + +## 5. Exclusion codes + +```text +post-cutoff benchmark-authored +backfilled-or-reconstructed reason-not-explicit +scope-unresolvable lifecycle-unresolvable +record-ambiguous reason-obvious-from-code +wrong-path-not-functionally-viable +oracle-not-deterministic shipping-content-not-observable +task-not-bounded leakage-risk +prior-benchmark-task-equivalent +``` + +`missing-record-id` and `insufficient-provenance` are **not** codes here and must +not become codes. + +## 6. Task-author firewall + +A task author may see the base tree, a neutral maintenance need, functional +acceptance criteria and the allowed scope. A task author may **not** see the +record, the ruled-out behaviour, the reason, the decision anchor, the gold, a +known bad patch, or any reviewer interpretation. This firewall is the core of +v5's anti-circularity and is checked executably where practical. + +## 7. GO / HOLD + +Registered before the census runs, taken unchanged from the owner's decision: + +```text +eligible repository final A0-qualified >= 8 + +GO requires all of: + eligible repositories >= 3 + total final A0-qualified >= 36 + delivery observability demonstrated for identified and id-less decisions + where each is present + no unresolved integrity blocker +Otherwise HOLD. +``` + +After Stage 0: 4 pass → four-repository set; 3 pass → three-repository set; +≤2 pass → HOLD. Repository selection completes before any treatment outcome +exists, and selecting repositories after seeing an ON/OFF result is forbidden. + +**36 is a feasibility floor, not a sample size.** It reserves at least 12 +candidates for a pilot and leaves at least 24 distinct candidates for a possible +confirmatory study. The final N comes from a separately frozen power analysis +after the pilot. + +Thresholds do not move after counts appear. On HOLD: no threshold relaxation, no +synthetic records, no post-hoc repository cherry-picking, no owner testimony. + +## 8. Disclosure — what was already visible + +v4's counts are public and known to this document's author: 241 decisions +(gitseed 104, agent-operator-score 59, logic-pro-mcp 43, agent-control-plane 35), +143 identified and 98 id-less, 154 of 207 delivered. Those are pre-gate counts +and v4 qualification outcomes under a discarded rule; none is imported, and no +threshold here is calibrated against them. The disclosure exists so a reader can +judge the thresholds knowing what their author knew. + +## 9. Forbidden in Stage 0 + +```text +pilot runs ON/OFF agent runs randomization +outcome rows effect sizes significance tests +README metrics product claims owner testimony +importing any v4 qualification row, reviewer verdict or correspondence score +``` + +## 10. Termination + +Stage 0 ends with a report and stops, GO or HOLD. A successor requires a new +confirmatory PRD, a new preregistration, a fixed repository set and separate +owner approval. + +```text +CDEB-FRESH V5 STAGE 0 COMPLETE — PRODUCT-EFFECT MEASUREMENT NOT STARTED +``` diff --git a/bench/cdeb/studies/cdeb-fresh-v5/STATUS.json b/bench/cdeb/studies/cdeb-fresh-v5/STATUS.json new file mode 100644 index 00000000..7b3466df --- /dev/null +++ b/bench/cdeb/studies/cdeb-fresh-v5/STATUS.json @@ -0,0 +1,6 @@ +{ + "study_id": "cdeb-fresh-v5", + "phase": "stage0-authority-corpus-feasibility", + "measured_run_allowed": false, + "updated_at": "2026-08-22T01:30:00Z" +} diff --git a/bench/cdeb/studies/cdeb-fresh-v5/authority-policy.json b/bench/cdeb/studies/cdeb-fresh-v5/authority-policy.json new file mode 100644 index 00000000..eb63ec35 --- /dev/null +++ b/bench/cdeb/studies/cdeb-fresh-v5/authority-policy.json @@ -0,0 +1,55 @@ +{ + "schema_version": 1, + "study_id": "cdeb-fresh-v5", + "registered_at": "2026-08-22T01:30:00Z", + "construct": "A naturally recorded, pre-study CommitLore decision is itself a repository recorded-policy artifact.", + "why_this_replaces_v4": "v4 admitted a candidate only when the same ruling could be recovered from prose with the record removed. That excluded 190 of 241 decisions, and it excluded exactly the case the product exists for: a judgment recorded once, in the record, that the current code does not explain. Requiring the decision to be documented twice makes the benchmark measure redundancy rather than delivery.", + "tiers": { + "A0": { + "name": "Natural Recorded Authority", + "role": "primary admission", + "required": [ + "created before the study cutoff", + "record bytes present in the frozen snapshot", + "arose in ordinary development", + "not benchmark-authored", + "not reconstructed or backfilled", + "explicit ruled-out behaviour", + "explicit reason", + "scope recoverable", + "lifecycle recoverable", + "authorized repository" + ], + "not_required": [ + "a duplicate prose source", + "a valid Record-Id", + "independent corroboration" + ] + }, + "A1": { + "name": "Independently Corroborated", + "role": "evidence-strength metadata only", + "definition": "A0 plus independent support in a pull request, issue, ADR, ordinary prose, design document, code comment or test rationale", + "on_absence": "independent_corroboration is false, authority remains A0, and the candidate is not excluded" + }, + "A2": { + "name": "Owner Attested", + "role": "disabled for v5", + "collected": 0, + "why": "v4's counts are already public, so testimony added now could be aimed at specific candidates. The owner disabled it, and nothing in this stage may wait on it." + } + }, + "anti_circularity": { + "policy_source": "pre-study natural CommitLore record", + "treatment": "automatic delivery of that policy before a relevant mutation", + "control": "the same task and agent with the model-visible decision payload suppressed", + "outcome": "whether the final code tree implements a functionally viable approach the policy ruled out", + "forbidden_outcomes": [ + "whether the agent mentioned a Record-Id", + "whether the agent repeated the record's wording", + "whether the agent stated the reason" + ], + "why_it_holds": "the policy source is frozen before the study and the outcome is read from the final tree, so nothing the treatment says can satisfy the measurement by being said." + }, + "forbidden_reintroduction": "No gate in this study may require a decision to be documented outside its record. A gate that does so under another name is the v4 gate returning, whatever it is called." +} diff --git a/bench/cdeb/studies/cdeb-fresh-v5/deviations.jsonl b/bench/cdeb/studies/cdeb-fresh-v5/deviations.jsonl new file mode 100644 index 00000000..d4f38239 --- /dev/null +++ b/bench/cdeb/studies/cdeb-fresh-v5/deviations.jsonl @@ -0,0 +1 @@ +{"deviation_id": "CDEB-V5-SNAPSHOT-REUSE-OVER-RESNAPSHOT", "recorded_at": "2026-08-22T01:30:00Z", "kind": "named-deviation-from-default-preference", "basis": {"instruction": "the Stage 0 prompt prefers fresh v5 snapshots and permits reuse where materially safer, documented", "adopted": "reuse the sealed bundles frozen at 2026-08-20T22:08:19Z; the v5 study cutoff is that instant", "measurement": {"gitseed": {"new_commits_since_freeze": 0, "new_ruled_out_lines": 0}, "agent-operator-score": {"new_commits_since_freeze": 90, "new_ruled_out_lines": 0}, "logic-pro-mcp": {"new_commits_since_freeze": 72, "new_ruled_out_lines": 0}, "agent-control-plane": {"new_commits_since_freeze": 5, "new_ruled_out_lines": 14, "window": "2026-08-21T06:46 .. 2026-08-21T18:38"}}, "what_a_fresh_snapshot_would_add": "14 decisions, all of them in agent-control-plane, all authored on 2026-08-21 \u2014 inside the window in which v4's corpus shortfall was known. The other two moved repositories contributed 0 decisions across 162 commits, so a fresh snapshot would import the contaminated set and nothing else.", "content_assessment": "the 14 read as ordinary development \u2014 SQLite DDL migration, retry-rule semantics, a permit brand type, vitest signal handling \u2014 with nothing about CDEB in them. They are probably natural. They cannot be shown to be natural mechanically, and their timing cannot be undone."}, "closed_alternatives": ["re-snapshotting and admitting the 14, which adds candidates authored during the study to the repository sitting nearest the eligibility floor", "re-snapshotting and excluding the 14 by date, which is the same corpus as reuse with more moving parts and a cutoff that has to be defended anyway", "asking the owner to attest that the 14 are natural, which is owner testimony and is disabled"], "measured_data_exists": false, "reason": "The direction of this choice is against GO: it takes the smaller, older corpus and gives up the only candidates a fresh snapshot would have added, in the repository where they would have counted most. If v5 still reaches GO the result is stronger for it; if v5 holds, the 14 are reported by name and count so the owner can order a deliberate re-snapshot rather than have one smuggled in here."} diff --git a/bench/cdeb/studies/cdeb-fresh-v5/feasibility/.gitkeep b/bench/cdeb/studies/cdeb-fresh-v5/feasibility/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/bench/cdeb/studies/cdeb-fresh-v5/study.json b/bench/cdeb/studies/cdeb-fresh-v5/study.json new file mode 100644 index 00000000..9ca1a093 --- /dev/null +++ b/bench/cdeb/studies/cdeb-fresh-v5/study.json @@ -0,0 +1,21 @@ +{ + "study_id": "cdeb-fresh-v5", + "schema_version": 1, + "phase": "stage0-authority-corpus-feasibility", + "measured_run_allowed": false, + "construct": "A naturally recorded, pre-study CommitLore decision is itself a repository recorded-policy artifact.", + "primary_admission_authority": "A0", + "owner_testimony": "disabled", + "record_id_required": false, + "independent_corroboration_required": false, + "predecessors": ["cdeb-fresh-v3", "cdeb-fresh-v3r1", "cdeb-fresh-v4"], + "predecessor_status": "v3 and v3r1 invalidated; v4 stage0-hold. None resumable, no measured data in any.", + "predecessor_artifact_reuse": "infrastructure code only, after verification; no qualification outcome, reviewer verdict, task, gold, oracle, trajectory or result row", + "product_release_tag": "v1.2.0", + "product_release_commit": "90a8b212e1db70cccf69fbf48415b9c036b2d854", + "product_dist_sha256": "318e16612206ae0aa3732033127b2937276ce2f142872c33a91ec04a33133b91", + "study_cutoff": "2026-08-20T22:08:19Z", + "stage0_survey_repositories": ["gitseed", "agent-operator-score", "logic-pro-mcp", "agent-control-plane"], + "fixed_repository_set": null, + "created_at": "2026-08-22T01:30:00Z" +} diff --git a/test/cdeb-terminal-hardening.test.ts b/test/cdeb-terminal-hardening.test.ts index 86f5b669..ff57398b 100644 --- a/test/cdeb-terminal-hardening.test.ts +++ b/test/cdeb-terminal-hardening.test.ts @@ -123,9 +123,12 @@ describe('CDEB terminal hardening', () => { const successor = readFileSync(join(STUDY, 'SUCCESSOR.md'), 'utf8'); expect(prd).toContain('status: terminal-preserved'); expect(prd).toContain('must not be used to resume `cdeb-fresh-v3r1`'); - expect(active).toMatchObject({ last_terminal_study_id: 'cdeb-fresh-v3r1', successor_requires_new_study_id: true }); - expect(active.active_study_id).not.toBe('cdeb-fresh-v3r1'); - expect(active.active_study_id).not.toBe('cdeb-fresh-v3'); + expect(active).toMatchObject({ successor_requires_new_study_id: true }); + // Whichever study holds the slot, no ended one may: the declaration names a + // successor and the resolver refuses the rest from their own status files. + for (const ended of ['cdeb-fresh-v3', 'cdeb-fresh-v3r1', 'cdeb-fresh-v4']) { + expect(active.active_study_id).not.toBe(ended); + } expect(status).toMatchObject({ phase: 'invalidated', measured_run_allowed: false }); expect(successor).toContain('new study id'); expect(successor).toContain('new preregistration'); diff --git a/test/cdeb-v4-stage0-governance.test.ts b/test/cdeb-v4-stage0-governance.test.ts index 6730c5c2..cf277d87 100644 --- a/test/cdeb-v4-stage0-governance.test.ts +++ b/test/cdeb-v4-stage0-governance.test.ts @@ -76,11 +76,14 @@ describe('CDEB v4 Stage 0 governance', () => { expect(() => resolveActiveStudyRoot(unknownStatus)).toThrow(/Invalid active-study declaration/); }); - it('resolves cdeb-fresh-v4 as the active study with the measured run still shut', () => { - expect(resolveActiveStudyRoot(CDEB_ROOT)).toBe(V4); + it('has handed the active slot on and cannot take it back', () => { + // v4 reached HOLD and a successor now holds the slot. What has to stay true + // is not that v4 is active -- it is that v4 can never be active again while + // its own status says it ended. + expect(resolveActiveStudyRoot(CDEB_ROOT)).not.toBe(V4); const status = readJson(join(V4, 'STATUS.json')); const study = readJson(join(V4, 'study.json')); - expect(status).toMatchObject({ study_id: 'cdeb-fresh-v4', phase: 'stage0-corpus-feasibility', measured_run_allowed: false }); + expect(status).toMatchObject({ study_id: 'cdeb-fresh-v4', phase: 'stage0-hold', measured_run_allowed: false, verdict: 'HOLD' }); expect(study).toMatchObject({ study_id: 'cdeb-fresh-v4', measured_run_allowed: false, record_id_required: false }); expect(study.predecessor_artifact_reuse).toBe('none'); expect(() => assertMeasuredRunAuthorized(V4)).toThrow(/measured_run_allowed is not true/); diff --git a/test/cdeb-v5-governance.test.ts b/test/cdeb-v5-governance.test.ts new file mode 100644 index 00000000..45f7a751 --- /dev/null +++ b/test/cdeb-v5-governance.test.ts @@ -0,0 +1,130 @@ +/** CDEB-Fresh v5 Stage 0 governance: a HOLD is as terminal as an invalidation. */ + +import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { + TERMINAL_STUDY_PHASES, + assertMeasuredRunAuthorized, + resolveActiveStudyRoot, +} from '../bench/cdeb/active-study.js'; + +const HERE = resolve(fileURLToPath(new URL('.', import.meta.url))); +const ROOT = resolve(HERE, '..'); +const CDEB_ROOT = join(ROOT, 'bench', 'cdeb'); +const V5 = join(CDEB_ROOT, 'studies', 'cdeb-fresh-v5'); + +const readJson = (path: string): Record => + JSON.parse(readFileSync(path, 'utf8')) as Record; + +const cdebRootNaming = (studyId: string): string => { + const root = mkdtempSync(join(tmpdir(), 'cdeb-v5-active-')); + mkdirSync(join(root, 'studies'), { recursive: true }); + for (const study of ['cdeb-fresh-v3', 'cdeb-fresh-v3r1', 'cdeb-fresh-v4', 'cdeb-fresh-v5']) { + const source = join(CDEB_ROOT, 'studies', study, 'STATUS.json'); + if (!existsSync(source)) continue; + mkdirSync(join(root, 'studies', study), { recursive: true }); + cpSync(source, join(root, 'studies', study, 'STATUS.json')); + } + writeFileSync( + join(root, 'ACTIVE-STUDY.json'), + `${JSON.stringify({ + active_study_id: studyId, + last_terminal_study_id: 'cdeb-fresh-v4', + status: 'active', + reason: 'test declaration', + successor_requires_new_study_id: true, + }, null, 2)}\n`, + ); + return root; +}; + +describe('CDEB v5 Stage 0 governance', () => { + it('refuses every ended predecessor as the active study, HOLD included', () => { + // v4 reached a verdict rather than being invalidated. Running anything + // against it would attribute the result to a study that already ended, so + // the two endings are treated the same. + expect([...TERMINAL_STUDY_PHASES]).toEqual(['invalidated', 'stage0-hold']); + for (const ended of ['cdeb-fresh-v3', 'cdeb-fresh-v3r1', 'cdeb-fresh-v4']) { + expect(() => resolveActiveStudyRoot(cdebRootNaming(ended))).toThrow( + new RegExp(`Refused terminal study ${ended} as the active study`), + ); + } + }); + + it('seals v4 at HOLD with its results intact', () => { + const status = readJson(join(CDEB_ROOT, 'studies', 'cdeb-fresh-v4', 'STATUS.json')); + expect(status).toMatchObject({ + study_id: 'cdeb-fresh-v4', + phase: 'stage0-hold', + measured_run_allowed: false, + successor_required: true, + verdict: 'HOLD', + }); + // The verdict and its evidence stay readable; sealing is not deletion. + const summary = readJson(join(CDEB_ROOT, 'studies', 'cdeb-fresh-v4', 'feasibility', 'qualification-summary.json')); + expect((summary.verdict as Record).verdict).toBe('HOLD'); + expect(summary.measured_product_effect_rows).toBe(0); + expect(existsSync(join(CDEB_ROOT, 'studies', 'cdeb-fresh-v4', 'feasibility', 'RESULT.md'))).toBe(true); + expect(existsSync(join(CDEB_ROOT, 'studies', 'cdeb-fresh-v4', 'feasibility', 'adversarial-review.md'))).toBe(true); + }); + + it('resolves v5 as active with the measured run still shut', () => { + expect(resolveActiveStudyRoot(CDEB_ROOT)).toBe(V5); + const study = readJson(join(V5, 'study.json')); + const status = readJson(join(V5, 'STATUS.json')); + expect(status).toMatchObject({ study_id: 'cdeb-fresh-v5', measured_run_allowed: false }); + expect(study).toMatchObject({ + study_id: 'cdeb-fresh-v5', + measured_run_allowed: false, + record_id_required: false, + independent_corroboration_required: false, + owner_testimony: 'disabled', + }); + expect(() => assertMeasuredRunAuthorized(V5)).toThrow(/measured_run_allowed is not true/); + }); + + it('registers A0 as primary admission and keeps corroboration out of it', () => { + const policy = readJson(join(V5, 'authority-policy.json')); + const tiers = policy.tiers as Record>; + expect(tiers.A0.role).toBe('primary admission'); + expect(tiers.A0.not_required).toEqual([ + 'a duplicate prose source', + 'a valid Record-Id', + 'independent corroboration', + ]); + expect(tiers.A1.role).toBe('evidence-strength metadata only'); + expect(tiers.A2.role).toBe('disabled for v5'); + expect(tiers.A2.collected).toBe(0); + const anti = policy.anti_circularity as Record; + expect(anti.forbidden_outcomes).toEqual([ + 'whether the agent mentioned a Record-Id', + "whether the agent repeated the record's wording", + 'whether the agent stated the reason', + ]); + }); + + it('registers the owner thresholds and forbids the v4 gate returning under a new name', () => { + const prereg = readFileSync(join(V5, 'STAGE0-PREREGISTRATION.md'), 'utf8'); + expect(prereg).toContain('eligible repository final A0-qualified >= 8'); + expect(prereg).toContain('eligible repositories >= 3'); + expect(prereg).toContain('total final A0-qualified >= 36'); + expect(prereg).toContain('`missing-record-id` and `insufficient-provenance` are **not** codes here'); + expect(prereg).toContain('36 is a feasibility floor, not a sample size'); + // The record being in Git is the treatment, not a leak — stating the + // opposite would rebuild v4's gate inside G8. + expect(prereg).toContain('The record existing in Git is not\nleakage — it is the treatment content.'); + const policy = readJson(join(V5, 'authority-policy.json')); + expect(String(policy.forbidden_reintroduction)).toMatch(/documented outside its record/); + }); + + it('creates no measured-run directory under the Stage 0 study', () => { + for (const forbidden of ['tasks', 'gold', 'oracles', 'pilot', 'rows', 'randomization']) { + expect(existsSync(join(V5, forbidden))).toBe(false); + } + }); +});