Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions dag/gunbc/ci/ci_spec.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 40 additions & 5 deletions dag/gunbc/required_ci_phase_roster.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DeclaredGeneratedArtifactPopulation> = [
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"
Expand All @@ -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
Expand All @@ -92,7 +128,6 @@ fn required_ci_phase_lane(p: RequiredCiPhase) -> RequiredCiLane {
data required_ci_phases: List<RequiredCiPhase> = [
ParsePhase,
NamespaceWaveAdmissionPhase,
RegenPhase,
GeneratedArtifactPhase,
RegenFixedPointPhase,
FloorPhase,
Expand Down
35 changes: 35 additions & 0 deletions dag/test/claim/whole_generated_artifact_adjudication_test.dag
Original file line number Diff line number Diff line change
@@ -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
})
}
99 changes: 57 additions & 42 deletions src/v1/stage0/src/bin/claim_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,9 @@ fn run() -> Result<ExitCode, ExitCode> {
// 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
Expand Down Expand Up @@ -712,55 +713,60 @@ fn run() -> Result<ExitCode, ExitCode> {
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(
&regen_candidate_dir,
&regen_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()
.map(|v| v.to_string())
.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,
Expand All @@ -777,7 +783,8 @@ fn run() -> Result<ExitCode, ExitCode> {
} => {
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(),
Expand Down Expand Up @@ -829,14 +836,23 @@ fn run() -> Result<ExitCode, ExitCode> {
}
}
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 \
branch above named why. Reporting the refusal rather than the silence"
);
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");
}

Expand Down Expand Up @@ -901,7 +917,8 @@ fn run() -> Result<ExitCode, ExitCode> {
// 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,
Expand Down Expand Up @@ -1464,7 +1481,6 @@ impl RequiredCiLane {
enum RequiredCiPhase {
Parse,
NamespaceWaveAdmission,
Regen,
GeneratedArtifact,
RegenFixedPoint,
Floor,
Expand All @@ -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",
}
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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",
Expand Down
Loading