diff --git a/dag/gunbc/ci/ci_spec.dag b/dag/gunbc/ci/ci_spec.dag index 0c185e23454..0378131faec 100644 --- a/dag/gunbc/ci/ci_spec.dag +++ b/dag/gunbc/ci/ci_spec.dag @@ -1569,9 +1569,11 @@ fn gunbc_ci_deploy_invoke(stage: DeployStage) -> String { // being green on a merge candidate IS the statement that every path this invoke would write is // already at its fixed point -- hence no git diff, and no second heal. // -// TWO BOUNDARIES, STATED SO THEY ARE NOT LATER DISCOVERED AS SURPRISES. The phase covers the -// REGISTRY, roughly 35 artifacts including DESIGN.md, ROADMAP.md and the workflow emissions; the -// stage0 mirror population is the separate RegenPhase. A generated `.rs` outside both -- +// TWO POPULATION ARMS, STATED SO THEY ARE NOT LATER DISCOVERED AS SURPRISES. The +// GeneratedArtifactPhase now issues ONE verdict over the REGISTRY (roughly 35 artifacts including +// DESIGN.md, ROADMAP.md and the workflow emissions) AND the stage0 mirror population formerly +// adjudicated by a sibling RegenPhase. The component diagnostics remain separate and located; only +// their independently-green terminal outcomes are gone. A generated `.rs` outside both -- // `gunbc_rust_source_type_bindings.rs` on #9949 -- is in neither, which is why an authored `.dag` // landed there without its projection and merged green. That is a real coverage gap about a THIRD // population and it is not this loop's: `main_wet` writes the registry and nothing else, so the diff --git a/dag/gunbc/required_ci_phase_roster.dag b/dag/gunbc/required_ci_phase_roster.dag index 6939c427267..e6e086be083 100644 --- a/dag/gunbc/required_ci_phase_roster.dag +++ b/dag/gunbc/required_ci_phase_roster.dag @@ -41,7 +41,9 @@ fn required_ci_lane_name(l: RequiredCiLane) -> String { // its CLI word and lane -- construction, not a list a phase can be omitted from. Four phases are // the post-bankruptcy roster (gunbc.rung_drop required_gate_bankruptcy, 2026-08-29), with the // generated-artifact wall restored after its declared population produced a real drift incident: -// parse, namespace-wave-admission, regen, generated-artifact, floor. +// parse, namespace-wave-admission, generated-artifact, floor. `generated-artifact` is ONE phase +// over both declared generated populations below. The former sibling `regen` phase was removed +// when its independently-green verdict hid a drift in the stage0-mirror half for five hours. // // WALL-CLOCK PRICE OF THIS WIDENING, recorded here because `gunbc.rung_drop` // `required_gate_bankruptcy` requires every static-roster widening to be priced on the roster's @@ -56,16 +58,51 @@ fn required_ci_lane_name(l: RequiredCiLane) -> String { type RequiredCiPhase = ParsePhase | NamespaceWaveAdmissionPhase - | RegenPhase | GeneratedArtifactPhase | RegenFixedPointPhase | FloorPhase +// TWO NAMED ARMS, NEVER A WIDENED PATH PREDICATE. A path that neither authority owns is in +// neither arm; inability to produce either arm refuses GeneratedArtifactPhase. The registry arm +// is gunbc.generated_artifact committed_generated_artifacts. The mirror arm is +// v1.compiler.emit_rust emit_emitted_population_manifest joined against the committed stage0 +// source directory. The required host runs both adjudicators before issuing one terminal verdict. +type DeclaredGeneratedArtifactPopulation + = RegistryProjectionPopulation + | Stage0MirrorPopulation + +data declared_generated_artifact_populations: List = [ + RegistryProjectionPopulation, + Stage0MirrorPopulation, +] + +// Both component verdicts survive the fold. This is deliberately not `Accepted | Refused` with a +// single cause: when both populations refuse, both precise causes must reach the terminal report. +// `PopulationRefused` also covers a drift; the located per-artifact diagnosis remains owned by the +// component adjudicator and is carried here without being reclassified. +type GeneratedArtifactPopulationAdjudication + = GeneratedArtifactPopulationClean + | GeneratedArtifactPopulationRefused + +type WholeGeneratedArtifactAdjudication { + registry_projections: GeneratedArtifactPopulationAdjudication + stage0_mirrors: GeneratedArtifactPopulationAdjudication +} + +fn whole_generated_artifact_adjudication_clean(a: WholeGeneratedArtifactAdjudication) -> Bool { + match a.registry_projections { + GeneratedArtifactPopulationRefused => false + GeneratedArtifactPopulationClean => match a.stage0_mirrors { + GeneratedArtifactPopulationRefused => false + GeneratedArtifactPopulationClean => true + } + } +} + fn required_ci_phase_name(p: RequiredCiPhase) -> String { match p { ParsePhase => "parse" NamespaceWaveAdmissionPhase => "namespace-wave-admission" - RegenPhase => "regen" GeneratedArtifactPhase => "generated-artifact" RegenFixedPointPhase => "regen-fixed-point" FloorPhase => "floor" @@ -78,7 +115,6 @@ fn required_ci_phase_lane(p: RequiredCiPhase) -> RequiredCiLane { match p { ParsePhase => WitnessesLane NamespaceWaveAdmissionPhase => WitnessesLane - RegenPhase => BuildLane GeneratedArtifactPhase => BuildLane RegenFixedPointPhase => BuildLane FloorPhase => WitnessesLane @@ -92,7 +128,6 @@ fn required_ci_phase_lane(p: RequiredCiPhase) -> RequiredCiLane { data required_ci_phases: List = [ ParsePhase, NamespaceWaveAdmissionPhase, - RegenPhase, GeneratedArtifactPhase, RegenFixedPointPhase, FloorPhase, diff --git a/dag/test/claim/whole_generated_artifact_adjudication_test.dag b/dag/test/claim/whole_generated_artifact_adjudication_test.dag new file mode 100644 index 00000000000..acb6bc7bfa0 --- /dev/null +++ b/dag/test/claim/whole_generated_artifact_adjudication_test.dag @@ -0,0 +1,35 @@ +module test.claim.whole_generated_artifact_adjudication_test + +import gunbc.required_ci_phase_roster { + GeneratedArtifactPopulationClean, + GeneratedArtifactPopulationRefused, + WholeGeneratedArtifactAdjudication, + whole_generated_artifact_adjudication_clean +} + +// PERMANENT COMPOSITE REDS, one for each direction of the join. The first is the #10402 shape: +// registry projections all matched while v1_compiler_infer.rs drifted. The second is the founding +// receipt of green_reported_over_a_population_the_instrument_does_not_own: mirrors all matched +// while docs/design-failure-modes.md was stale. Either component refusing makes the ONE outcome +// refuse; neither can lend its green to the other. +test fn registry_green_does_not_cover_a_drifted_stage0_mirror() -> Bool { + !whole_generated_artifact_adjudication_clean(a: WholeGeneratedArtifactAdjudication { + registry_projections: GeneratedArtifactPopulationClean, + stage0_mirrors: GeneratedArtifactPopulationRefused + }) +} + +test fn mirror_green_does_not_cover_a_stale_registry_projection() -> Bool { + !whole_generated_artifact_adjudication_clean(a: WholeGeneratedArtifactAdjudication { + registry_projections: GeneratedArtifactPopulationRefused, + stage0_mirrors: GeneratedArtifactPopulationClean + }) +} + +// ACCEPTED POSITIVE CONTROL. A compositor that refused every input would satisfy both reds. +test fn both_declared_populations_clean_is_the_only_green() -> Bool { + whole_generated_artifact_adjudication_clean(a: WholeGeneratedArtifactAdjudication { + registry_projections: GeneratedArtifactPopulationClean, + stage0_mirrors: GeneratedArtifactPopulationClean + }) +} diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 5f7a813d03c..7ad78eb0e7b 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -476,8 +476,9 @@ fn run() -> Result { // refuses when planned, executed and terminal identity counts disagree, so a silently short // roster cannot report as a pass. // THE COMPOSED CI RUN — one process per LANE. The roster is five phases: the .dag parse - // sweep, namespace wave admission, the regen first-generation comparison, - // generated-artifact drift and the witness floor. `--required-lane` selects which of them + // sweep, namespace wave admission, one generated-artifact comparison over registry + // projections and stage0 mirrors, its fixed-point comparison, and the witness floor. + // `--required-lane` selects which of them // this process owns; with no lane, it owns all five, which is what a local run wants. // // WHAT IT IS AND IS NOT. Sequencing a program's phases is the program's job (DESIGN §3: the @@ -712,16 +713,26 @@ fn run() -> Result { ran.push("namespace-wave-admission"); } - // PHASE 2 — regen first generation: the emitted mirrors against what is committed. - if required_ci_phase_selected(RequiredCiPhase::Regen, required_ci_lane) { - eprintln!("required-ci: phase regen (first generation vs committed)"); + // PHASE — ONE adjudication over BOTH declared generated-artifact populations: every + // emitted stage0 mirror, and every committed registry projection. Neither half can report + // a terminal green: the phase records its one verdict only after both adjudicators ran. + // This closes the disjoint-denominator failure where registry=38/38 printed green while a + // stale v1_compiler_infer.rs made the sibling regen phase red. The registry boundary is + // read-only, so it cannot manufacture its own green by installing generated bytes. + if required_ci_phase_selected(RequiredCiPhase::GeneratedArtifact, required_ci_lane) { + eprintln!( + "required-ci: phase generated-artifact (registry projections + stage0 mirrors)" + ); + let failures_before = phase_failures.len(); + + // MIRROR HALF. Production still precedes adjudication so a missing/stale mirror leaves + // the candidate needed to repair it. A refusal remains distinct from a mismatch, but + // both are failures of this one generated-artifact phase. match v1_compiler::cli_run::run_required_regen( ®en_candidate_dir, ®en_receipt_path, ) { Ok(outcome) => { - // Read through accessors, and print `unmeasured` rather than a plausible - // default when the pass built the wrong variant (#8650's shape). let fge = outcome .receipt .first_generation_equal() @@ -729,38 +740,33 @@ fn run() -> Result { .unwrap_or_else(|| "unmeasured".to_string()); let candidate = outcome.receipt.candidate_artifact().unwrap_or("unmeasured"); eprintln!( - "required-ci: regen first_generation_equal={fge} candidate={candidate}" + "required-ci: generated-artifact population=stage0-mirrors \ + first_generation_equal={fge} candidate={candidate}" ); for failure in &outcome.failures { - eprintln!("required-ci: regen FAIL {failure}"); + eprintln!( + "required-ci: generated-artifact population=stage0-mirrors FAIL {failure}" + ); } if !outcome.failures.is_empty() { - phase_failures - .push(format!("regen ({} failure(s))", outcome.failures.len())); + phase_failures.push(format!( + "generated-artifact stage0-mirrors ({} failure(s))", + outcome.failures.len() + )); } } Err(e) => { - // A REFUSAL IS NOT A MISMATCH: nothing was emitted, so there is no comparison - // to report, and the refusal is named rather than folded into a drift verdict. - eprintln!("required-ci: regen refused: {e}"); - phase_failures.push(format!("regen refused: {e}")); + eprintln!( + "required-ci: generated-artifact population=stage0-mirrors REFUSED {e}" + ); + phase_failures.push(format!("generated-artifact stage0-mirrors refused: {e}")); } } - ran.push("regen"); - } - // PHASE — every committed generated projection against the bytes derived from its .dag - // authority. This is deliberately not `--required-regen`: that phase's subject is the - // stage0 mirror population, while this boundary asks the generated-artifact registry for - // its whole roster, including DESIGN.md, docs/design-failure-modes.md and - // docs/design-rung-drops.md. The boundary is read-only, so it cannot manufacture - // its own green by installing generated bytes. - if required_ci_phase_selected(RequiredCiPhase::GeneratedArtifact, required_ci_lane) { - eprintln!( - "required-ci: phase generated-artifact (every committed projection vs its authority)" - ); + // REGISTRY HALF. Run even after a mirror refusal so the one outcome names the complete + // population rather than allowing the first failure to hide the second. + let registry_failures_before = phase_failures.len(); let outcome = v1_compiler::cli_run::run_generated_artifact_boundary(&source_roots); - let failures_before = phase_failures.len(); match &outcome { v1_compiler::cli_run::GeneratedArtifactBoundaryOutcome::CarrierRefused { cause, @@ -777,7 +783,8 @@ fn run() -> Result { } => { let divergent = v1_compiler::cli_run::boundary_divergent(&outcome); eprintln!( - "required-ci: generated-artifact rostered={} adjudicated={} matches={} \ + "required-ci: generated-artifact population=registry-projections \ + rostered={} adjudicated={} matches={} \ drifted={} absent={} unadjudicated={}", artifacts.len() + unadjudicated.len(), artifacts.len(), @@ -829,7 +836,7 @@ fn run() -> Result { } } if !v1_compiler::cli_run::boundary_is_clean(&outcome) - && phase_failures.len() == failures_before + && phase_failures.len() == registry_failures_before { eprintln!( "required-ci: generated-artifact REFUSED — the outcome is not clean and no \ @@ -837,6 +844,15 @@ fn run() -> Result { ); phase_failures.push("generated-artifact (not clean, unnamed cause)".to_string()); } + if phase_failures.len() == failures_before { + eprintln!( + "required-ci: generated-artifact OK populations=registry-projections,stage0-mirrors" + ); + } else { + eprintln!( + "required-ci: generated-artifact FAIL populations=registry-projections,stage0-mirrors" + ); + } ran.push("generated-artifact"); } @@ -901,7 +917,8 @@ fn run() -> Result { // v2 ENTRY. Measured cost +135s against the floor's ~30-40 minutes. // // IT NO LONGER SHARES A JOB WITH THE FLOOR AT ALL (operator ruling, 2026-08-25). - // The phase sits in the `build` lane beside regen; the floor is the other job, and + // The phase sits in the `build` lane beside generated-artifact, whose mirror half writes + // its receipt; the floor is the other job, and // the two run in parallel. The old paragraph here weighed whether to make this an // early PREREQUISITE of the floor -- an exit before it -- and that question is now // moot rather than answered: a phase in another job cannot precondition this one, @@ -1464,7 +1481,6 @@ impl RequiredCiLane { enum RequiredCiPhase { Parse, NamespaceWaveAdmission, - Regen, GeneratedArtifact, RegenFixedPoint, Floor, @@ -1476,7 +1492,6 @@ impl RequiredCiPhase { RequiredCiPhase::Parse => "parse", RequiredCiPhase::NamespaceWaveAdmission => "namespace-wave-admission", RequiredCiPhase::RegenFixedPoint => "regen-fixed-point", - RequiredCiPhase::Regen => "regen", RequiredCiPhase::GeneratedArtifact => "generated-artifact", RequiredCiPhase::Floor => "floor", } @@ -1496,10 +1511,10 @@ impl RequiredCiPhase { // lane boundary between them would mean parsing the corpus twice to answer a // question one parse already reached. RequiredCiPhase::NamespaceWaveAdmission => RequiredCiLane::Witnesses, - RequiredCiPhase::Regen => RequiredCiLane::Build, RequiredCiPhase::GeneratedArtifact => RequiredCiLane::Build, - // THE FIXED POINT RIDES WITH REGEN BY NECESSITY, NOT PREFERENCE. It reads the receipt - // the regen phase wrote at `target/stage0-regen-receipt.json`, and target/ does not + // THE FIXED POINT RIDES WITH GENERATED-ARTIFACT BY NECESSITY, NOT PREFERENCE. It reads + // the receipt that phase's stage0-mirror adjudicator wrote at + // `target/stage0-regen-receipt.json`, and target/ does not // survive checkout, so a lane boundary between them would leave it with no prior // measurement to reference and nothing to compare. RequiredCiPhase::RegenFixedPoint => RequiredCiLane::Build, @@ -1508,13 +1523,14 @@ impl RequiredCiPhase { } } -// THE REQUIRED GATE IS SIX PHASES. Four are the 2026-08-29 compiler-floor bankruptcy roster; +// THE REQUIRED GATE IS FIVE PHASES. Four are the 2026-08-29 compiler-floor bankruptcy roster; // generated-artifact returned after its declared exposure produced a real stale projection on -// main. The other three removed phases remain outside required CI and inside the declared drop. -const REQUIRED_CI_PHASES: [RequiredCiPhase; 6] = [ +// main, and now also owns the former regen phase's stage0-mirror population. Keeping two phase +// identities would preserve the independently-green outcomes this composition removes. The other +// three removed phases remain outside required CI and inside the declared drop. +const REQUIRED_CI_PHASES: [RequiredCiPhase; 5] = [ RequiredCiPhase::Parse, RequiredCiPhase::NamespaceWaveAdmission, - RequiredCiPhase::Regen, RequiredCiPhase::GeneratedArtifact, RequiredCiPhase::RegenFixedPoint, RequiredCiPhase::Floor, @@ -1529,10 +1545,9 @@ const PHASE_ROSTER_AUTHORITY_MODULE: &str = "gunbc.required_ci_phase_roster"; const PHASE_ROSTER_AUTHORITY_DECL: &str = "RequiredCiPhase"; /// Every phase this binary realizes, in the authority's own variant spelling. -const PHASE_ROSTER_VARIANT_LABELS: [&str; 6] = [ +const PHASE_ROSTER_VARIANT_LABELS: [&str; 5] = [ "ParsePhase", "NamespaceWaveAdmissionPhase", - "RegenPhase", "GeneratedArtifactPhase", "RegenFixedPointPhase", "FloorPhase",