From 4b44b155c4a7accf29df774a5032927d148b3525 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 2 Aug 2026 17:46:27 +0000 Subject: [PATCH 1/3] WIP: Design: emission admission + stage-aware pipeline (declared-change vs re --- dag/gunbc/bootstrap.dag | 6 +- dag/gunbc/emission_admission.dag | 241 +++++++++++ .../claim/emission_admission_witness_test.dag | 217 ++++++++++ ...n-admission-stage-aware-pipeline-design.md | 384 ++++++++++++++++++ 4 files changed, 846 insertions(+), 2 deletions(-) create mode 100644 dag/gunbc/emission_admission.dag create mode 100644 dag/test/claim/emission_admission_witness_test.dag create mode 100644 docs/plans/emission-admission-stage-aware-pipeline-design.md diff --git a/dag/gunbc/bootstrap.dag b/dag/gunbc/bootstrap.dag index 824204ff472..2db64bf8a47 100644 --- a/dag/gunbc/bootstrap.dag +++ b/dag/gunbc/bootstrap.dag @@ -1,5 +1,7 @@ module gunbc.bootstrap +import std.types { String, NonEmptyStr, Bool, Int, List, FilePath } +import std.content_hash { ContentHash } type CompilerStage = Tokenize @@ -37,7 +39,7 @@ type ArtifactKind | RenderedFiles type SourceEntry { - path: std.types.FilePath + path: FilePath content: String module_name: NonEmptyStr? } @@ -73,7 +75,7 @@ type FixedPointCheck { pass1_hash: ContentHash pass2_hash: ContentHash converged: Bool - diff_files: List + diff_files: List } type BootstrapResult diff --git a/dag/gunbc/emission_admission.dag b/dag/gunbc/emission_admission.dag new file mode 100644 index 00000000000..00564575efe --- /dev/null +++ b/dag/gunbc/emission_admission.dag @@ -0,0 +1,241 @@ +module gunbc.emission_admission + +import std.types { String, NonEmptyStr, Bool, Int, List, FilePath, CommitSha } +import std.content_hash { Fnv1a64Structural } +import gunbc.bootstrap { CompilerStage, Emit, Infer, ChangeClassification } +import gunbc.generated_artifact { GeneratedArtifact, CiYamlArtifact } +import gunbc.guarantee_measurement { + MeasurementRevision, + ContentFingerprint, + GitRevision, + GuaranteeTargetName, +} +import v2.std.algebra { any, contains, fold_list } +import v2.std.change { ChangeSet } + +data emission_admission_charter_note: String = "STAGE 1 OF THE EMISSION-ADMISSION SPINE (design docs/plans/emission-admission-stage-aware-pipeline-design.md): vocabulary, pure admit_emission_delta, and synthetic witnesses only. No PR derivation, no regen phase stamp, no warm-merge consumer — those are downstream stages. This module carries EmissionSurface, StageStamp, DeclaredChangeSet, EmissionDelta, and EmissionAdmissionVerdict so regen/floor/admission phases can share one fail-closed seam instead of collapsing to bytes_equal / bytes_differ. CompilerStage authority is gunbc.bootstrap; MeasurementRevision is reused from gunbc.guarantee_measurement; FrontierProbeStage is harness vocabulary and is NOT imported here." + +type PipelinePhase + = RegenPhase + | FloorPhase + | AdmissionPhase + +type EmissionSurface + = Stage0SeedFile { rel_path: FilePath } + | GeneratedArtifactSurface { artifact: GeneratedArtifact } + | TargetLanguageBundle { target: GuaranteeTargetName, module: NonEmptyStr } + | OrchestrationMedium { medium: NonEmptyStr } + +type StageContribution + = StageProduced { stage: CompilerStage } + | StageForwarded { stage: CompilerStage, from_prior_phase: PipelinePhase } + | UpstreamRefusalEmbedded { origin_stage: CompilerStage, diagnostic: NonEmptyStr } + +type StageStamp { + phase: PipelinePhase + contributions: List + substrate_revision: ContentFingerprint +} + +type DeclaredChangeEvidence + = DerivedFromAffectedSet { changes: ChangeSet } + | DerivedFromBootstrapPlan { change: ChangeClassification } + | AuthorExplicit { reason: NonEmptyStr } + +type DeclaredChangeSet { + subject_revision: MeasurementRevision + allowed_stage_moves: List + allowed_surface_moves: List + derivation_evidence: DeclaredChangeEvidence +} + +type EmissionDelta { + surface: EmissionSurface + before: ContentFingerprint + after: ContentFingerprint + stage_stamp: StageStamp + observed_at_phase: PipelinePhase +} + +type EmissionRegressionCause + = SurfaceMovedWithoutDeclaration + | StageAttributionMismatch + | UpstreamRefusalMaskedAtEmit + | BehavioralWitnessFailed + | DigestMismatchUndeclared + +type EmissionAdmissionVerdict + = AdmittedDeclared { + delta: EmissionDelta + matched_declaration: DeclaredChangeSet + } + | RefusedRegression { + delta: EmissionDelta + cause: EmissionRegressionCause + } + | RefusedUndeclaredDelta { + delta: EmissionDelta + missing_from_declaration: EmissionSurface + } + | RefusedStageMismatch { + delta: EmissionDelta + declared_stages: List + observed_stage: CompilerStage + } + | RefusedStaleSubstrate { + stamp: StageStamp + cause: NonEmptyStr + } + +type EmissionAdmissionVerdictFold { + admitted_declared: fn(EmissionDelta, DeclaredChangeSet) -> R + refused_regression: fn(EmissionDelta, EmissionRegressionCause) -> R + refused_undeclared: fn(EmissionDelta, EmissionSurface) -> R + refused_stage_mismatch: fn(EmissionDelta, List, CompilerStage) -> R + refused_stale_substrate: fn(StageStamp, NonEmptyStr) -> R +} + +fn emission_admission_verdict_fold( + v: EmissionAdmissionVerdict, + fold: EmissionAdmissionVerdictFold +) -> R { + match v { + AdmittedDeclared { delta: d, matched_declaration: m } => fold.admitted_declared(d, m) + RefusedRegression { delta: d, cause: c } => fold.refused_regression(d, c) + RefusedUndeclaredDelta { delta: d, missing_from_declaration: s } => fold.refused_undeclared(d, s) + RefusedStageMismatch { delta: d, declared_stages: ds, observed_stage: os } => + fold.refused_stage_mismatch(d, ds, os) + RefusedStaleSubstrate { stamp: s, cause: c } => fold.refused_stale_substrate(s, c) + } +} + +fn emission_admission_is_admitted(v: EmissionAdmissionVerdict) -> Bool { + emission_admission_verdict_fold( + v: v, + fold: EmissionAdmissionVerdictFold { + admitted_declared: fn(_, _) { true }, + refused_regression: fn(_, _) { false }, + refused_undeclared: fn(_, _) { false }, + refused_stage_mismatch: fn(_, _, _) { false }, + refused_stale_substrate: fn(_, _) { false }, + }, + ) +} + +fn emission_surface_eq(left: EmissionSurface, right: EmissionSurface) -> Bool { + match left { + Stage0SeedFile { rel_path: lp } => match right { + Stage0SeedFile { rel_path: rp } => lp == rp + _ => false + } + GeneratedArtifactSurface { artifact: la } => match right { + GeneratedArtifactSurface { artifact: ra } => la == ra + _ => false + } + TargetLanguageBundle { target: lt, module: lm } => match right { + TargetLanguageBundle { target: rt, module: rm } => (lt as String) == (rt as String) && (lm as String) == (rm as String) + _ => false + } + OrchestrationMedium { medium: lm } => match right { + OrchestrationMedium { medium: rm } => (lm as String) == (rm as String) + _ => false + } + } +} + +fn surface_allowed(surface: EmissionSurface, allowed: List) -> Bool { + any( + xs: allowed, + predicate: fn(s) { emission_surface_eq(left: surface, right: s) }, + ) +} + +fn stage_contribution_stage(c: StageContribution) -> CompilerStage { + match c { + StageProduced { stage: s } => s + StageForwarded { stage: s, from_prior_phase: _ } => s + UpstreamRefusalEmbedded { origin_stage: s, diagnostic: _ } => s + } +} + +fn stage_stamp_has_upstream_refusal(stamp: StageStamp) -> Bool { + any( + xs: stamp.contributions, + predicate: fn(c) { + match c { + UpstreamRefusalEmbedded { origin_stage: _, diagnostic: _ } => true + _ => false + } + }, + ) +} + +fn first_stage_mismatch( + stamp: StageStamp, + allowed: List +) -> CompilerStage? { + fold_list( + xs: stamp.contributions, + empty: Absent as CompilerStage?, + cons: fn(acc, c) { + match acc { + Present { value: _ } => acc + Absent => { + let stage = stage_contribution_stage(c: c) + if contains(xs: allowed, item: stage, eq: fn(l, r) { l == r }) { + Absent + } else { + Present { value: stage } + } + } + } + }, + ) +} + +fn content_fingerprint_eq(left: ContentFingerprint, right: ContentFingerprint) -> Bool { + match left { + ContentFingerprint { digest: ld } => match right { + ContentFingerprint { digest: rd } => (ld as String) == (rd as String) + } + } +} + +fn emission_delta_is_no_op(delta: EmissionDelta) -> Bool { + content_fingerprint_eq(left: delta.before, right: delta.after) +} + +fn upstream_refusal_masked(delta: EmissionDelta) -> Bool { + emission_delta_is_no_op(delta: delta) == false + && stage_stamp_has_upstream_refusal(stamp: delta.stage_stamp) +} + +fn admit_emission_delta( + delta: EmissionDelta, + declared: DeclaredChangeSet +) -> EmissionAdmissionVerdict { + if emission_delta_is_no_op(delta: delta) { + AdmittedDeclared { delta: delta, matched_declaration: declared } + } else if !surface_allowed(surface: delta.surface, allowed: declared.allowed_surface_moves) { + RefusedUndeclaredDelta { + delta: delta, + missing_from_declaration: delta.surface, + } + } else { + match first_stage_mismatch(stamp: delta.stage_stamp, allowed: declared.allowed_stage_moves) { + Present { value: observed } => RefusedStageMismatch { + delta: delta, + declared_stages: declared.allowed_stage_moves, + observed_stage: observed, + } + Absent => if upstream_refusal_masked(delta: delta) { + RefusedRegression { + delta: delta, + cause: UpstreamRefusalMaskedAtEmit, + } + } else { + AdmittedDeclared { delta: delta, matched_declaration: declared } + } + } + } +} diff --git a/dag/test/claim/emission_admission_witness_test.dag b/dag/test/claim/emission_admission_witness_test.dag new file mode 100644 index 00000000000..4f6d3fccb33 --- /dev/null +++ b/dag/test/claim/emission_admission_witness_test.dag @@ -0,0 +1,217 @@ +module test.claim.emission_admission_witness_test + +import std.types { String, NonEmptyStr, Bool, Int, List, FilePath, CommitSha } +import std.content_hash { Fnv1a64Structural, content_hash_atom } +import gunbc.bootstrap { CompilerStage, Emit, Infer } +import gunbc.generated_artifact { GeneratedArtifact, CiYamlArtifact } +import gunbc.emission_admission { + PipelinePhase, RegenPhase, FloorPhase, + EmissionSurface, Stage0SeedFile, GeneratedArtifactSurface, StageProduced, UpstreamRefusalEmbedded, + StageStamp, StageContribution, + DeclaredChangeSet, DeclaredChangeEvidence, AuthorExplicit, + EmissionDelta, + EmissionAdmissionVerdict, + AdmittedDeclared, RefusedRegression, RefusedUndeclaredDelta, RefusedStageMismatch, + UpstreamRefusalMaskedAtEmit, + EmissionAdmissionVerdictFold, emission_admission_verdict_fold, + emission_admission_is_admitted, + admit_emission_delta, +} +import gunbc.guarantee_measurement { + MeasurementRevision, ContentFingerprint, GitRevision, +} + +data emission_admission_witness_scope_note: String = "SYNTHETIC ROWS ONLY, by the Stage 1 charter: these witnesses prove admit_emission_delta discriminates declared moves from regressions using invented fingerprints and surfaces — no PR facts, no regen execution, no warm receipts. When Stage 2 lands declared_change_set_from_pr_facts, real rows execute through THIS vocabulary and these witnesses stay as permanent regression controls (design sec 7)." + +fn fp(tag: String) -> ContentFingerprint { + ContentFingerprint { digest: content_hash_atom(value: tag as NonEmptyStr) } +} + +fn synth_stamp(stage: CompilerStage, with_upstream_refusal: Bool) -> StageStamp { + let base = [ + StageProduced { stage: stage }, + ] + let contribs = if with_upstream_refusal { + concat( + base, + [UpstreamRefusalEmbedded { origin_stage: Infer, diagnostic: "CompilerError planted at emit" as NonEmptyStr }], + ) + } else { + base + } + StageStamp { + phase: RegenPhase, + contributions: contribs, + substrate_revision: fp(tag: "substrate-a"), + } +} + +fn synth_declared( + surfaces: List, + stages: List +) -> DeclaredChangeSet { + DeclaredChangeSet { + subject_revision: GitRevision { commit: "0000000000000000000000000000000000000000" as CommitSha }, + allowed_stage_moves: stages, + allowed_surface_moves: surfaces, + derivation_evidence: AuthorExplicit { reason: "synthetic witness fixture" as NonEmptyStr }, + } +} + +fn synth_delta( + surface: EmissionSurface, + before_tag: String, + after_tag: String, + stage: CompilerStage, + with_upstream_refusal: Bool +) -> EmissionDelta { + EmissionDelta { + surface: surface, + before: fp(tag: before_tag), + after: fp(tag: after_tag), + stage_stamp: synth_stamp(stage: stage, with_upstream_refusal: with_upstream_refusal), + observed_at_phase: RegenPhase, + } +} + +test fn no_op_delta_admits_even_without_surface_declaration() -> Bool { + let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } + let delta = synth_delta( + surface: surface, + before_tag: "same", + after_tag: "same", + stage: Emit, + with_upstream_refusal: false, + ) + let declared = synth_declared(surfaces: [], stages: []) + emission_admission_is_admitted(v: admit_emission_delta(delta: delta, declared: declared)) +} + +test fn declared_surface_and_stage_move_admits() -> Bool { + let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } + let declared = synth_declared( + surfaces: [surface], + stages: [Emit], + ) + let delta = synth_delta( + surface: surface, + before_tag: "before", + after_tag: "after", + stage: Emit, + with_upstream_refusal: false, + ) + match admit_emission_delta(delta: delta, declared: declared) { + AdmittedDeclared { delta: _, matched_declaration: _ } => true + RefusedRegression { delta: _, cause: _ } => false + RefusedUndeclaredDelta { delta: _, missing_from_declaration: _ } => false + RefusedStageMismatch { delta: _, declared_stages: _, observed_stage: _ } => false + RefusedStaleSubstrate { stamp: _, cause: _ } => false + } +} + +test fn undeclared_surface_move_refuses() -> Bool { + let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } + let declared = synth_declared(surfaces: [], stages: [Emit]) + let delta = synth_delta( + surface: surface, + before_tag: "before", + after_tag: "after", + stage: Emit, + with_upstream_refusal: false, + ) + match admit_emission_delta(delta: delta, declared: declared) { + AdmittedDeclared { delta: _, matched_declaration: _ } => false + RefusedRegression { delta: _, cause: _ } => false + RefusedUndeclaredDelta { delta: _, missing_from_declaration: s } => + match s { + GeneratedArtifactSurface { artifact: CiYamlArtifact } => true + _ => false + } + RefusedStageMismatch { delta: _, declared_stages: _, observed_stage: _ } => false + RefusedStaleSubstrate { stamp: _, cause: _ } => false + } +} + +test fn stage_mismatch_refuses_when_emit_not_declared() -> Bool { + let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } + let declared = synth_declared(surfaces: [surface], stages: [Infer]) + let delta = synth_delta( + surface: surface, + before_tag: "before", + after_tag: "after", + stage: Emit, + with_upstream_refusal: false, + ) + match admit_emission_delta(delta: delta, declared: declared) { + AdmittedDeclared { delta: _, matched_declaration: _ } => false + RefusedRegression { delta: _, cause: _ } => false + RefusedUndeclaredDelta { delta: _, missing_from_declaration: _ } => false + RefusedStageMismatch { delta: _, declared_stages: _, observed_stage: os } => os == Emit + RefusedStaleSubstrate { stamp: _, cause: _ } => false + } +} + +test fn upstream_refusal_with_byte_move_refuses_regression() -> Bool { + let surface = Stage0SeedFile { rel_path: "src/v1/stage0/src/lib.rs" as FilePath } + let declared = synth_declared(surfaces: [surface], stages: [Emit, Infer]) + let delta = synth_delta( + surface: surface, + before_tag: "before", + after_tag: "after", + stage: Emit, + with_upstream_refusal: true, + ) + match admit_emission_delta(delta: delta, declared: declared) { + AdmittedDeclared { delta: _, matched_declaration: _ } => false + RefusedRegression { delta: _, cause: c } => c == UpstreamRefusalMaskedAtEmit + RefusedUndeclaredDelta { delta: _, missing_from_declaration: _ } => false + RefusedStageMismatch { delta: _, declared_stages: _, observed_stage: _ } => false + RefusedStaleSubstrate { stamp: _, cause: _ } => false + } +} + +fn verdict_tag(v: EmissionAdmissionVerdict) -> Int { + emission_admission_verdict_fold( + v: v, + fold: EmissionAdmissionVerdictFold { + admitted_declared: fn(_, _) { 1 }, + refused_regression: fn(_, _) { 2 }, + refused_undeclared: fn(_, _) { 3 }, + refused_stage_mismatch: fn(_, _, _) { 4 }, + refused_stale_substrate: fn(_, _) { 5 }, + }, + ) +} + +test fn verdict_fold_discriminates_every_arm() -> Bool { + let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } + let declared = synth_declared(surfaces: [surface], stages: [Emit]) + let moving = synth_delta( + surface: surface, + before_tag: "b", + after_tag: "a", + stage: Emit, + with_upstream_refusal: false, + ) + let no_op = synth_delta( + surface: surface, + before_tag: "x", + after_tag: "x", + stage: Emit, + with_upstream_refusal: false, + ) + verdict_tag(v: admit_emission_delta(delta: moving, declared: declared)) == 1 + && verdict_tag( + v: admit_emission_delta( + delta: moving, + declared: synth_declared(surfaces: [], stages: [Emit]), + ), + ) == 3 + && verdict_tag( + v: admit_emission_delta( + delta: moving, + declared: synth_declared(surfaces: [surface], stages: [Infer]), + ), + ) == 4 + && verdict_tag(v: admit_emission_delta(delta: no_op, declared: synth_declared(surfaces: [], stages: []))) == 1 +} diff --git a/docs/plans/emission-admission-stage-aware-pipeline-design.md b/docs/plans/emission-admission-stage-aware-pipeline-design.md new file mode 100644 index 00000000000..59f7a9ff9a6 --- /dev/null +++ b/docs/plans/emission-admission-stage-aware-pipeline-design.md @@ -0,0 +1,384 @@ +# Emission admission + stage-aware pipeline (declared-change vs regression) + +**Status:** Stage 1 LANDED (vocabulary carrier + `admit_emission_delta` + synthetic witnesses). +Stages 2–6 remain design-only until separately dispatched. + +Work item: `node://adhoc-48a1f19c-1f8` (session calm-eagle-92). + +Parent context: v1-deletion lane (`still-bat-561`). This design unblocks honest +warm-merge admission, phased single-process CI, and self-host/regen gates that +today can only compare bytes — not classify *why* bytes changed. + +DESIGN refs: §2 (reuse existing stage/change authorities; no parallel ledger), +§3 (`gunbc.bootstrap.CompilerStage` is the stage authority; +`FrontierProbeStage` is harness vocabulary, not a second pipeline), +§4 (emission is the downstream half of the two-stage contract in +[compiler-guarantee-recovery-gap-analysis.md](compiler-guarantee-recovery-gap-analysis.md) +§1), §5 (undeclared emission delta refuses — never widens to rerun-everything or +accept-anyway), §4b (discriminating RED: regression vs declared-change controls +stay enrolled after walls land), §6 (model-before-implement; scaffolds name +dissolution triggers). + +Related: [five-minute-ci-gate-design.md](five-minute-ci-gate-design.md) +(`warm-merge-admission`, `phased-single-process-ci`) · +[seed-honesty-discharge-design.md](seed-honesty-discharge-design.md) · +[module-identity-storage-binding-design.md](module-identity-storage-binding-design.md) +(BothWays delta lens) · [post-zero-regen-gate-placement.md](post-zero-regen-gate-placement.md) · +`gunbc.bootstrap` · `gunbc.guarantee_measurement` · `v2.compiler.self_host` · +`gunbc.generated_artifact` + +--- + +## 0. One-sentence claim + +> Every emission delta is admitted only when a **declared change set** names the +> compiler stages and artifact surfaces that may move; anything else is a +> **regression** and refuses. The pipeline is **stage-aware**: each phase +> (regen, floor, admission) stamps which `CompilerStage` produced which artifact, +> and admission consumes those stamps — it does not re-derive the whole compiler +> from bytes alone. + +--- + +## 1. Displaced cost (§6) + +| Cost today | Mechanism | +|---|---| +| **Warm receipts cannot be trusted for merge** | `warm-merge-admission` can stamp resolve/materialization receipts, but has no typed rule for whether an emission delta was *expected* from the PR's source change. Stale-base and stale-roster refusals exist; *undeclared emitter drift* does not — so admission either reruns cold or risks admitting a regression. | +| **Phases pay duplicate prelude work** | `phased-single-process-ci` wants regen, floor, and admission on one substrate. Without stage-stamped verdicts, each phase must assume the prior phase might have silently changed upstream facts — so discovery/resolve/index rerun anyway (the roadmap's red_control: "silently reruns full discovery when prior phase already fixed those facts"). | +| **Byte gates cannot distinguish intent** | `generated_artifact_drift_gate`, `regen_verify`, and `self_host_realized_comparison` answer only *match / mismatch*. A PR that intentionally changes `05_emit` output and includes regen is indistinguishable from an emitter bug — both are "bytes differ." Review becomes the classifier (§5 specification-without-execution). | +| **Self-host flips lack stage attribution** | `frontier_probe_types` locates blockers at `ProbeStageAssemble` / `ProbeStageEmit` / `ProbeStageSemanticDerivation`, but that vocabulary is harness-facing. There is no durable receipt tying an emission delta to `CompilerStage::Emit` vs an upstream `Infer` refusal planted at emit — the `#7485` containment class. | +| **Guarantee paths stay unmeasured at emission** | `gunbc.guarantee_measurement` names `source→each-emission-target` as an independent path grain, but receipts today do not carry *declared vs regression* disposition — only observed verdict/refusal. Rung honesty (§4b) cannot rank emission-path regressions separately from declared emitter migrations. | + +--- + +## 2. The gap — what exists vs what is missing + +### 2.1 Authorities already correct (reuse, do not fork) + +| Fact | Authority | Live today? | +|---|---|---| +| Compiler pipeline stages | `gunbc.bootstrap.CompilerStage` (`Tokenize`…`Emit`) | ✓ `dag/gunbc/bootstrap.dag` | +| Stage input/output kinds | `gunbc.bootstrap.StageInput` / `StageOutput` / `ArtifactKind` | ✓ modeled, lightly exercised | +| Change → affected stages (bootstrap edits) | `gunbc.bootstrap.ChangeClassification.affects_stages` | ✓ modeled; not wired to CI/self-host | +| Field propagation per stage | `gunbc.bootstrap.FieldPropagation` / `TransformContract` | ✓ modeled; not executed | +| Source-graph change kinds | `v2.std.change.ChangeKind` + `ChangeSet` | ✓ consumed by `v2.lens.affected_set` | +| Emitted-byte digest | `v2.compiler.self_host.canonical_emitted_bytes_digest` | ✓ executed on self-host roster | +| Per-path guarantee receipts | `gunbc.guarantee_measurement.GuaranteeMeasurementReceipt` | ✓ Stage-0 vocabulary; no emission-admission join | +| Generated artifact registry | `gunbc.generated_artifact` + drift witnesses | ✓ byte fixed-point; no declared-change arm | +| Harness probe positions | `v2.compiler.self_host.frontier_probe_types.FrontierProbeStage` | ✓ distinct from `CompilerStage` by design | + +### 2.2 The violation (three coupled gaps) + +1. **No declared-change carrier.** PRs carry git diffs and affected-set projections, but nothing in-tree states *which compiler stages and emission surfaces are allowed to move* for this change. `ChangeClassification` in `gunbc.bootstrap` is bootstrap-authoring vocabulary only — not joined to pull-request admission. + +2. **No emission-admission verdict.** Consumers collapse to `bytes_equal` / `bytes_differ`. Missing arms: `AdmittedDeclared { stages, surfaces }`, `RefusedRegression { stage, surface, cause }`, `RefusedUndeclaredDelta`, `RefusedStageMismatch { declared, observed }`. The absorbing fallback class (§5): treating any mismatch as "rerun regen" or "rerun whole floor" without a counted, typed reason. + +3. **No stage-stamped phase pipeline.** Regen, floor, and admission are separate processes (separate `gunbc run` invocations, separate indices). `phased-single-process-ci` needs phase boundaries with *separate verdict stamps* on one shared materialization provider — but the stamp shape is unspecified. + +--- + +## 3. Scope + +### 3.1 In scope (this design) + +- Vocabulary and join contracts for **declared change**, **emission delta**, and **admission verdict**. +- **Stage-aware pipeline run** model: phases, stage stamps, artifact surfaces. +- Classification rules: declared-change vs regression (fail-closed). +- First consumers named with discriminating witnesses (design-level; implementation is staged). +- Explicit integration with `warm-merge-admission`, `phased-single-process-ci`, regen/self-host, and generated-artifact drift. + +### 3.2 Out of scope (named neighbors) + +| Neighbor | Why out | +|---|---| +| Implementing warm-merge-admission (#7522) | Downstream consumer; this design is its admission *ontology*. | +| Materialization substrate (#7534) | Shared cache keys are orthogonal; this design consumes materialization receipts, does not define them. | +| Full `ChangeClassification` → live bootstrap planner | Bootstrap authoring tool; only the *join pattern* is reused for PR declared-change. | +| Byte-identical self-host fixed point | Retired non-goal (DESIGN §7); admission uses behavioral + digest receipts, not byte identity as the sole oracle. | +| DDC / `ddc_reference_compiler` | [seed-honesty-discharge-design.md](seed-honesty-discharge-design.md) — consumes admission receipts, does not define them. | +| Guarantee ladder disposition derivation | `gunbc.guarantee_measurement` Stage 0 only; dispositions stay on the claims carrier. | + +### 3.3 Load-bearing files (escalate before edit under a stale brief) + +- `dag/gunbc/bootstrap.dag` — extend only via new exported types or new fns; do not repurpose `CompilerStage` variants. +- `src/v2/std/change.dag` — declared-change projection from `ChangeSet`. +- New module home (proposed): `gunbc.emission_admission` under `dag/gunbc/` (product/workflow layer, not `std/`). +- `src/v2/workflow/ci_floor_plan.dag` / `dag/gunbc/commit_workflow.dag` — phase enrollment (later PRs). +- `dag/tools/generated_artifact_gate.dag` — regen phase consumer (later PR). + +--- + +## 4. Target model + +### 4.1 Artifact surface (what emission admission governs) + +An **emission surface** is a typed target for bytes the compiler pipeline (or a +generated-artifact emitter) produces. Surfaces are enumerated — not free paths — +so admission cannot be bypassed via a novel string path. + +```dag +type EmissionSurface + = Stage0SeedFile { rel_path: FilePath } // GENERATED_STAGE0_FILES roster + | GeneratedArtifactSurface { artifact: GeneratedArtifact } + | TargetLanguageBundle { target: GuaranteeTargetName, module: QualifiedModule } + | OrchestrationMedium { medium: Symbol } // e.g. bash slice enrolled in ci_spec +``` + +**Authority:** surfaces derive from existing registries (`gunbc.generated_artifact`, +`gunbc.stage0_emit_model.generated_stage0_files`, self-host frontier module list, +`gunbc.guarantee_measurement.GuaranteeTargetName`). A path literal not reachable +from a registry row is `SurfaceUnknown` and refuses — never admitted by glob. + +### 4.2 Stage stamp (pipeline awareness) + +Each pipeline phase produces a **stage stamp** recording which `CompilerStage` +values contributed to an artifact (not just the last stage — emit can plant +upstream `CompilerError` nodes). + +```dag +type StageContribution + = StageProduced { stage: CompilerStage } + | StageForwarded { stage: CompilerStage, from_prior_phase: PipelinePhase } + | UpstreamRefusalEmbedded { origin_stage: CompilerStage, diagnostic: NonEmptyStr } + +type StageStamp { + phase: PipelinePhase + contributions: List + substrate_revision: ContentFingerprint // shared materialization identity +} + +type PipelinePhase + = RegenPhase + | FloorPhase + | AdmissionPhase +``` + +**Law:** `FrontierProbeStage` projects to `CompilerStage` via an explicit total +function `compiler_stage_from_probe` when probe receipts are consumed — never the +identity map. Harness positions (`Assemble`) are not compiler stages. + +### 4.3 Declared change set (author intent) + +A **declared change set** is the PR's claim about which stages and surfaces may +move. It is derived — not hand-authored per PR — from facts already in the tree: + +| Source | Projection | +|---|---| +| Git merge-base diff + `v2.lens.affected_set` | `ChangeSet` over module graph | +| `v2.std.change.RecomputePlan` / readiness layers | stages that must rerun vs preserve | +| Touch of emitter / translate / generated-artifact emit modules | `CompilerStage::Emit` (+ upstream if not lossless) | +| `gunbc.bootstrap.ChangeClassification` rows (when editing bootstrap types) | explicit `affects_stages` | +| Generated-artifact registry + diff | `EmissionSurface` rows for touched artifacts | + +```dag +type DeclaredChangeSet { + subject_revision: MeasurementRevision // guarantee_measurement vocabulary + allowed_stage_moves: List + allowed_surface_moves: List + derivation_evidence: DeclaredChangeEvidence // typed, located — not prose +} + +type DeclaredChangeEvidence + = DerivedFromAffectedSet { changes: ChangeSet } + | DerivedFromBootstrapPlan { change: ChangeClassification } + | AuthorExplicit { reason: NonEmptyStr } // rare; counted; never default +``` + +**Fail-closed:** `DeclaredChangeUnknown` when affected-set refuses, selection +fails closed, or a touched path is outside modeled surfaces. Unknown ⇒ no +admission — not "allow and rerun." + +### 4.4 Emission delta (observed fact) + +```dag +type EmissionDelta { + surface: EmissionSurface + before: ContentFingerprint + after: ContentFingerprint + stage_stamp: StageStamp + observed_at_phase: PipelinePhase +} +``` + +Deltas are observed by execution — regen compare, drift gate, self-host +`canonical_emitted_bytes_digest`, warm receipt replay. Numeric literals copied +from the current tree are not oracles (DESIGN §5 merge-blocking test rule). + +### 4.5 Admission verdict (the seam) + +```dag +type EmissionAdmissionVerdict + = AdmittedDeclared { + delta: EmissionDelta + matched_declaration: DeclaredChangeSet + } + | RefusedRegression { + delta: EmissionDelta + cause: EmissionRegressionCause + } + | RefusedUndeclaredDelta { + delta: EmissionDelta + missing_from_declaration: EmissionSurface + } + | RefusedStageMismatch { + delta: EmissionDelta + declared_stages: List + observed_stage: CompilerStage + } + | RefusedStaleSubstrate { + stamp: StageStamp + cause: NonEmptyStr + } + +type EmissionRegressionCause + = SurfaceMovedWithoutDeclaration + | StageAttributionMismatch + | UpstreamRefusalMaskedAtEmit + | BehavioralWitnessFailed + | DigestMismatchUndeclared +``` + +**Core decision** (pure, total on well-formed inputs): + +``` +admit_emission_delta(delta, declared) -> + if delta.before == delta.after -> AdmittedDeclared (no-op move) + else if !surface_allowed(delta.surface, declared) -> RefusedUndeclaredDelta + else if !stage_allowed(delta.stage_stamp, declared) -> RefusedStageMismatch + else if upstream_refusal_masked(delta) -> RefusedRegression + else -> AdmittedDeclared +``` + +No arm widens: missing declaration never triggers regen-all; mismatch never +falls through to green. + +--- + +## 5. Stage-aware pipeline (phased single process) + +### 5.1 End-to-end shape + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ One initialized process (shared materialization provider) │ +│ │ +│ RegenPhase │ +│ ├─ compute DeclaredChangeSet from PR facts │ +│ ├─ run regen emitters → EmissionDelta[] │ +│ ├─ StageStamp per delta (Emit + forwarded upstream) │ +│ └─ RegenVerdict = fold admit_emission_delta │ +│ │ │ +│ ▼ (RegenVerdict must be Admitted* or Refused — blocks) │ +│ FloorPhase │ +│ ├─ reuse substrate_revision + declared set │ +│ ├─ witness / compile / native runs │ +│ └─ FloorVerdict (separate stamp; may not rerun resolve) │ +│ │ │ +│ ▼ │ +│ AdmissionPhase (merge / warm-merge-admission) │ +│ ├─ validate warm receipts against declared + stamps │ +│ └─ AdmissionVerdict → merge gate │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Ordering law (from roadmap):** regen still gates floor. Admission may consume +warm receipts only when `substrate_revision` and `DeclaredChangeSet` match the +stamping run. + +### 5.2 Prelude duplication metric + +First slice for `phased-single-process-ci` (roadmap handback): attribute time +spent in resolve, index construction, and discovery per phase with and without +stamps. **RED control:** if phase *N+1* repeats resolve whose inputs are +unchanged per stamp, count > 0 ⇒ regression refusal (the roadmap red_control, +made executable). + +--- + +## 6. Consumer mapping + +| Consumer | Declared-change source | Regression signal | +|---|---|---| +| `warm-merge-admission` | PR affected set + stamped receipts at merge base | Receipt replay produces undeclared `EmissionDelta` | +| `generated_artifact_drift_gate` | diff touches artifact authority modules | `artifact_generate` ≠ committed without `allowed_surface_moves` | +| `regen_stage0 --verify` | diff touches `src/v2/compiler/*emit*` or frontier SelfEmitted rows | seed file digest mismatch undeclared | +| `self_host_realized_comparison` | frontier row flip / emitter edit in PR | behavioral witness fails with declared-only digest move | +| `ci_merge_admission_emit` | gate roster + floor stamp (existing) joined with admission verdict | stale-base / stale-roster (existing) + new undeclared-delta arm | +| `gunbc.guarantee_measurement` | probe harness declares subject revision | `ExecutionDiverged` on emission path with clean compile admission | + +--- + +## 7. Witness / RED discipline (§4b, §5) + +Each verdict arm gets a discriminating pair: + +| Arm | GREEN control | RED control | +|---|---|---| +| `AdmittedDeclared` | emitter edit in PR + regen committed + `allowed_surface_moves` contains surface | same edit without declaration ⇒ `RefusedUndeclaredDelta` | +| `RefusedRegression` | planted emitter bug without source change ⇒ refusal | declaration cannot green a behavioral failure | +| `RefusedStageMismatch` | declared `Infer`-only change but emit digest moves | mismatch stage attribution | +| `RefusedStaleSubstrate` | warm receipt from prior `substrate_revision` | replay after index rebuild without restamp | +| Prelude duplication | phase 2 skips resolve when stamp says preserved | forced re-resolve ⇒ counted refusal | + +Probes that go green when a wall lands **flip to permanent regression controls** — +they do not retire (§4b dissolution-on rule). + +--- + +## 8. Staged plan (design only — implementation is separate PRs) + +| Stage | Deliverable | Depends on | +|---|---|---| +| **0 — this document** | Sign-ready design + operator review | — | +| **1 — vocabulary carrier** | `gunbc.emission_admission` types + pure `admit_emission_delta` + unit witnesses on synthetic deltas | Stage 0 sign-off | +| **2 — declared-change derivation** | `declared_change_set_from_pr_facts` wired to affected-set; refusal when selection fails | Stage 1 | +| **3 — regen phase stamp** | `regen_stage0` / `generated_artifact_gate` emit `StageStamp` + deltas; regen refuses undeclared | Stage 2 | +| **4 — warm-merge join** | #7522 admits from warm receipts only with matching stamps + declaration | Stage 3 | +| **5 — phased single process** | `phased-single-process-ci` shares substrate; prelude duplication witness | Stage 4 | +| **6 — guarantee path rows** | emission-path measurement receipts carry admission verdict projection | Stage 1 (can parallel 3–5) | + +**Dissolution triggers (scaffolds):** + +- Stage 1: delete any interim `bytes_differ` only branch in merge admission once + verdict arm executes in CI. +- Stage 3: retire `regen_divergence_count` prose-oracle when structured + `EmissionDelta` list is the transport (extends regen ratchet retirement). +- Stage 5: delete duplicate `process_shared_index` construction in per-phase + entrypoints when stamp hit rate is receipted ≥ target on representative PRs. + +--- + +## 9. Open questions (operator sign-off) + +1. **AuthorExplicit declaration:** should human override ever admit a delta without + affected-set evidence, or is that always a §5 escape hatch to refuse? + +2. **Behavioral vs digest on self-host:** for `SelfEmitted` modules, is digest + match alone ever sufficient, or must `AdmittedDeclared` always include a + behavioral witness when the surface is `Stage0SeedFile`? + +3. **Frontier scaffold rows:** do counted `SeedRetained` / `UnpinnedFrontier` rows + automatically widen `allowed_surface_moves`, or does each row carry an explicit + `DeclaredChangeTemplate`? + +4. **Registry completeness:** is `SurfaceUnknown` a hard refusal on the merge path, + or a counted `FrontierAccepted` disposition until the generated-artifact + registry covers all emit surfaces? + +5. **Relationship to `BothWays`:** [module-identity-storage-binding-design.md](module-identity-storage-binding-design.md) + intentional semantic edits compare `R1` vs `R2`, not authority vs seed — does + emission admission treat quotient-delta lawful edits as always `AdmittedDeclared` + when the six lens laws pass, even if digest moves? + +--- + +## 10. Sign-off bar + +This design is ready for operator review when: + +- [ ] §4 types are agreed as the single admission authority (no parallel byte gate semantics). +- [ ] Stage 1–3 sequencing is accepted relative to #7522 / `phased-single-process-ci`. +- [ ] Open questions §9 have recorded verdicts (even if "defer with scaffold row"). +- [ ] Parent lane (`still-bat-561`) confirms this unblocks v1-deletion emission gates + without contradicting seed-honesty / real-fixpoint milestones. From d17e050a8b5a99075a0ed067ce8c91c54f5d12cb Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 2 Aug 2026 17:49:14 +0000 Subject: [PATCH 2/3] Design: emission admission + stage-aware pipeline (design-only). Rewrite the plan around existing emitter_producer_provenance machinery, declare the two new pieces (declared-change vs regression and generation lineage), register the doc graph bind, and drop premature carrier code. Co-authored-by: Cursor --- dag/gunbc/bootstrap.dag | 6 +- dag/gunbc/doc_graph_roots.dag | 7 + dag/gunbc/emission_admission.dag | 241 -------- .../claim/emission_admission_witness_test.dag | 217 -------- ...n-admission-stage-aware-pipeline-design.md | 518 +++++++++--------- 5 files changed, 254 insertions(+), 735 deletions(-) delete mode 100644 dag/gunbc/emission_admission.dag delete mode 100644 dag/test/claim/emission_admission_witness_test.dag diff --git a/dag/gunbc/bootstrap.dag b/dag/gunbc/bootstrap.dag index 2db64bf8a47..824204ff472 100644 --- a/dag/gunbc/bootstrap.dag +++ b/dag/gunbc/bootstrap.dag @@ -1,7 +1,5 @@ module gunbc.bootstrap -import std.types { String, NonEmptyStr, Bool, Int, List, FilePath } -import std.content_hash { ContentHash } type CompilerStage = Tokenize @@ -39,7 +37,7 @@ type ArtifactKind | RenderedFiles type SourceEntry { - path: FilePath + path: std.types.FilePath content: String module_name: NonEmptyStr? } @@ -75,7 +73,7 @@ type FixedPointCheck { pass1_hash: ContentHash pass2_hash: ContentHash converged: Bool - diff_files: List + diff_files: List } type BootstrapResult diff --git a/dag/gunbc/doc_graph_roots.dag b/dag/gunbc/doc_graph_roots.dag index b898ea4a57a..77689fc6d94 100644 --- a/dag/gunbc/doc_graph_roots.dag +++ b/dag/gunbc/doc_graph_roots.dag @@ -119,6 +119,13 @@ data hand_authored_doc_binds: List = [ additional_works: [], dissolution: HasTrigger { text: "the grant/envelope model lands (the doc's FLAG-A §6 interim is its only near-term consumer)" }, }, + HandAuthoredDocBind { + home: PlanDoc, + slug: "emission-admission-stage-aware-pipeline-design", + primary_work: DeclarationRef { module_path: "v2.compiler.self_host.emitter_producer_provenance", decl_name: "emitter_producer_provenance_note", field: WholeDeclaration }, + additional_works: [DeclarationRef { module_path: "v2.compiler.self_host", decl_name: "canonical_emitted_bytes_digest", field: WholeDeclaration }, DeclarationRef { module_path: "tools.floor_effect_gate_witness", decl_name: "regen_verify_gate_passes", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.bootstrap", decl_name: "CompilerStage", field: WholeDeclaration }], + dissolution: HasTrigger { text: "Stages 1–7 in the design doc land or are honestly retired: declared-change vs regression verdict executes on the merge path, generation lineage graph binds ConsumerRead on regen and warm-merge, phased-single-process CI prelude-duplication witness is green on a representative PR, and the doc migrates into a registered gunbc.plan.Plan row (the registration becomes the binding)" }, + }, HandAuthoredDocBind { home: PlanDoc, slug: "fleet-self-converge-enforcement-design", diff --git a/dag/gunbc/emission_admission.dag b/dag/gunbc/emission_admission.dag deleted file mode 100644 index 00564575efe..00000000000 --- a/dag/gunbc/emission_admission.dag +++ /dev/null @@ -1,241 +0,0 @@ -module gunbc.emission_admission - -import std.types { String, NonEmptyStr, Bool, Int, List, FilePath, CommitSha } -import std.content_hash { Fnv1a64Structural } -import gunbc.bootstrap { CompilerStage, Emit, Infer, ChangeClassification } -import gunbc.generated_artifact { GeneratedArtifact, CiYamlArtifact } -import gunbc.guarantee_measurement { - MeasurementRevision, - ContentFingerprint, - GitRevision, - GuaranteeTargetName, -} -import v2.std.algebra { any, contains, fold_list } -import v2.std.change { ChangeSet } - -data emission_admission_charter_note: String = "STAGE 1 OF THE EMISSION-ADMISSION SPINE (design docs/plans/emission-admission-stage-aware-pipeline-design.md): vocabulary, pure admit_emission_delta, and synthetic witnesses only. No PR derivation, no regen phase stamp, no warm-merge consumer — those are downstream stages. This module carries EmissionSurface, StageStamp, DeclaredChangeSet, EmissionDelta, and EmissionAdmissionVerdict so regen/floor/admission phases can share one fail-closed seam instead of collapsing to bytes_equal / bytes_differ. CompilerStage authority is gunbc.bootstrap; MeasurementRevision is reused from gunbc.guarantee_measurement; FrontierProbeStage is harness vocabulary and is NOT imported here." - -type PipelinePhase - = RegenPhase - | FloorPhase - | AdmissionPhase - -type EmissionSurface - = Stage0SeedFile { rel_path: FilePath } - | GeneratedArtifactSurface { artifact: GeneratedArtifact } - | TargetLanguageBundle { target: GuaranteeTargetName, module: NonEmptyStr } - | OrchestrationMedium { medium: NonEmptyStr } - -type StageContribution - = StageProduced { stage: CompilerStage } - | StageForwarded { stage: CompilerStage, from_prior_phase: PipelinePhase } - | UpstreamRefusalEmbedded { origin_stage: CompilerStage, diagnostic: NonEmptyStr } - -type StageStamp { - phase: PipelinePhase - contributions: List - substrate_revision: ContentFingerprint -} - -type DeclaredChangeEvidence - = DerivedFromAffectedSet { changes: ChangeSet } - | DerivedFromBootstrapPlan { change: ChangeClassification } - | AuthorExplicit { reason: NonEmptyStr } - -type DeclaredChangeSet { - subject_revision: MeasurementRevision - allowed_stage_moves: List - allowed_surface_moves: List - derivation_evidence: DeclaredChangeEvidence -} - -type EmissionDelta { - surface: EmissionSurface - before: ContentFingerprint - after: ContentFingerprint - stage_stamp: StageStamp - observed_at_phase: PipelinePhase -} - -type EmissionRegressionCause - = SurfaceMovedWithoutDeclaration - | StageAttributionMismatch - | UpstreamRefusalMaskedAtEmit - | BehavioralWitnessFailed - | DigestMismatchUndeclared - -type EmissionAdmissionVerdict - = AdmittedDeclared { - delta: EmissionDelta - matched_declaration: DeclaredChangeSet - } - | RefusedRegression { - delta: EmissionDelta - cause: EmissionRegressionCause - } - | RefusedUndeclaredDelta { - delta: EmissionDelta - missing_from_declaration: EmissionSurface - } - | RefusedStageMismatch { - delta: EmissionDelta - declared_stages: List - observed_stage: CompilerStage - } - | RefusedStaleSubstrate { - stamp: StageStamp - cause: NonEmptyStr - } - -type EmissionAdmissionVerdictFold { - admitted_declared: fn(EmissionDelta, DeclaredChangeSet) -> R - refused_regression: fn(EmissionDelta, EmissionRegressionCause) -> R - refused_undeclared: fn(EmissionDelta, EmissionSurface) -> R - refused_stage_mismatch: fn(EmissionDelta, List, CompilerStage) -> R - refused_stale_substrate: fn(StageStamp, NonEmptyStr) -> R -} - -fn emission_admission_verdict_fold( - v: EmissionAdmissionVerdict, - fold: EmissionAdmissionVerdictFold -) -> R { - match v { - AdmittedDeclared { delta: d, matched_declaration: m } => fold.admitted_declared(d, m) - RefusedRegression { delta: d, cause: c } => fold.refused_regression(d, c) - RefusedUndeclaredDelta { delta: d, missing_from_declaration: s } => fold.refused_undeclared(d, s) - RefusedStageMismatch { delta: d, declared_stages: ds, observed_stage: os } => - fold.refused_stage_mismatch(d, ds, os) - RefusedStaleSubstrate { stamp: s, cause: c } => fold.refused_stale_substrate(s, c) - } -} - -fn emission_admission_is_admitted(v: EmissionAdmissionVerdict) -> Bool { - emission_admission_verdict_fold( - v: v, - fold: EmissionAdmissionVerdictFold { - admitted_declared: fn(_, _) { true }, - refused_regression: fn(_, _) { false }, - refused_undeclared: fn(_, _) { false }, - refused_stage_mismatch: fn(_, _, _) { false }, - refused_stale_substrate: fn(_, _) { false }, - }, - ) -} - -fn emission_surface_eq(left: EmissionSurface, right: EmissionSurface) -> Bool { - match left { - Stage0SeedFile { rel_path: lp } => match right { - Stage0SeedFile { rel_path: rp } => lp == rp - _ => false - } - GeneratedArtifactSurface { artifact: la } => match right { - GeneratedArtifactSurface { artifact: ra } => la == ra - _ => false - } - TargetLanguageBundle { target: lt, module: lm } => match right { - TargetLanguageBundle { target: rt, module: rm } => (lt as String) == (rt as String) && (lm as String) == (rm as String) - _ => false - } - OrchestrationMedium { medium: lm } => match right { - OrchestrationMedium { medium: rm } => (lm as String) == (rm as String) - _ => false - } - } -} - -fn surface_allowed(surface: EmissionSurface, allowed: List) -> Bool { - any( - xs: allowed, - predicate: fn(s) { emission_surface_eq(left: surface, right: s) }, - ) -} - -fn stage_contribution_stage(c: StageContribution) -> CompilerStage { - match c { - StageProduced { stage: s } => s - StageForwarded { stage: s, from_prior_phase: _ } => s - UpstreamRefusalEmbedded { origin_stage: s, diagnostic: _ } => s - } -} - -fn stage_stamp_has_upstream_refusal(stamp: StageStamp) -> Bool { - any( - xs: stamp.contributions, - predicate: fn(c) { - match c { - UpstreamRefusalEmbedded { origin_stage: _, diagnostic: _ } => true - _ => false - } - }, - ) -} - -fn first_stage_mismatch( - stamp: StageStamp, - allowed: List -) -> CompilerStage? { - fold_list( - xs: stamp.contributions, - empty: Absent as CompilerStage?, - cons: fn(acc, c) { - match acc { - Present { value: _ } => acc - Absent => { - let stage = stage_contribution_stage(c: c) - if contains(xs: allowed, item: stage, eq: fn(l, r) { l == r }) { - Absent - } else { - Present { value: stage } - } - } - } - }, - ) -} - -fn content_fingerprint_eq(left: ContentFingerprint, right: ContentFingerprint) -> Bool { - match left { - ContentFingerprint { digest: ld } => match right { - ContentFingerprint { digest: rd } => (ld as String) == (rd as String) - } - } -} - -fn emission_delta_is_no_op(delta: EmissionDelta) -> Bool { - content_fingerprint_eq(left: delta.before, right: delta.after) -} - -fn upstream_refusal_masked(delta: EmissionDelta) -> Bool { - emission_delta_is_no_op(delta: delta) == false - && stage_stamp_has_upstream_refusal(stamp: delta.stage_stamp) -} - -fn admit_emission_delta( - delta: EmissionDelta, - declared: DeclaredChangeSet -) -> EmissionAdmissionVerdict { - if emission_delta_is_no_op(delta: delta) { - AdmittedDeclared { delta: delta, matched_declaration: declared } - } else if !surface_allowed(surface: delta.surface, allowed: declared.allowed_surface_moves) { - RefusedUndeclaredDelta { - delta: delta, - missing_from_declaration: delta.surface, - } - } else { - match first_stage_mismatch(stamp: delta.stage_stamp, allowed: declared.allowed_stage_moves) { - Present { value: observed } => RefusedStageMismatch { - delta: delta, - declared_stages: declared.allowed_stage_moves, - observed_stage: observed, - } - Absent => if upstream_refusal_masked(delta: delta) { - RefusedRegression { - delta: delta, - cause: UpstreamRefusalMaskedAtEmit, - } - } else { - AdmittedDeclared { delta: delta, matched_declaration: declared } - } - } - } -} diff --git a/dag/test/claim/emission_admission_witness_test.dag b/dag/test/claim/emission_admission_witness_test.dag deleted file mode 100644 index 4f6d3fccb33..00000000000 --- a/dag/test/claim/emission_admission_witness_test.dag +++ /dev/null @@ -1,217 +0,0 @@ -module test.claim.emission_admission_witness_test - -import std.types { String, NonEmptyStr, Bool, Int, List, FilePath, CommitSha } -import std.content_hash { Fnv1a64Structural, content_hash_atom } -import gunbc.bootstrap { CompilerStage, Emit, Infer } -import gunbc.generated_artifact { GeneratedArtifact, CiYamlArtifact } -import gunbc.emission_admission { - PipelinePhase, RegenPhase, FloorPhase, - EmissionSurface, Stage0SeedFile, GeneratedArtifactSurface, StageProduced, UpstreamRefusalEmbedded, - StageStamp, StageContribution, - DeclaredChangeSet, DeclaredChangeEvidence, AuthorExplicit, - EmissionDelta, - EmissionAdmissionVerdict, - AdmittedDeclared, RefusedRegression, RefusedUndeclaredDelta, RefusedStageMismatch, - UpstreamRefusalMaskedAtEmit, - EmissionAdmissionVerdictFold, emission_admission_verdict_fold, - emission_admission_is_admitted, - admit_emission_delta, -} -import gunbc.guarantee_measurement { - MeasurementRevision, ContentFingerprint, GitRevision, -} - -data emission_admission_witness_scope_note: String = "SYNTHETIC ROWS ONLY, by the Stage 1 charter: these witnesses prove admit_emission_delta discriminates declared moves from regressions using invented fingerprints and surfaces — no PR facts, no regen execution, no warm receipts. When Stage 2 lands declared_change_set_from_pr_facts, real rows execute through THIS vocabulary and these witnesses stay as permanent regression controls (design sec 7)." - -fn fp(tag: String) -> ContentFingerprint { - ContentFingerprint { digest: content_hash_atom(value: tag as NonEmptyStr) } -} - -fn synth_stamp(stage: CompilerStage, with_upstream_refusal: Bool) -> StageStamp { - let base = [ - StageProduced { stage: stage }, - ] - let contribs = if with_upstream_refusal { - concat( - base, - [UpstreamRefusalEmbedded { origin_stage: Infer, diagnostic: "CompilerError planted at emit" as NonEmptyStr }], - ) - } else { - base - } - StageStamp { - phase: RegenPhase, - contributions: contribs, - substrate_revision: fp(tag: "substrate-a"), - } -} - -fn synth_declared( - surfaces: List, - stages: List -) -> DeclaredChangeSet { - DeclaredChangeSet { - subject_revision: GitRevision { commit: "0000000000000000000000000000000000000000" as CommitSha }, - allowed_stage_moves: stages, - allowed_surface_moves: surfaces, - derivation_evidence: AuthorExplicit { reason: "synthetic witness fixture" as NonEmptyStr }, - } -} - -fn synth_delta( - surface: EmissionSurface, - before_tag: String, - after_tag: String, - stage: CompilerStage, - with_upstream_refusal: Bool -) -> EmissionDelta { - EmissionDelta { - surface: surface, - before: fp(tag: before_tag), - after: fp(tag: after_tag), - stage_stamp: synth_stamp(stage: stage, with_upstream_refusal: with_upstream_refusal), - observed_at_phase: RegenPhase, - } -} - -test fn no_op_delta_admits_even_without_surface_declaration() -> Bool { - let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } - let delta = synth_delta( - surface: surface, - before_tag: "same", - after_tag: "same", - stage: Emit, - with_upstream_refusal: false, - ) - let declared = synth_declared(surfaces: [], stages: []) - emission_admission_is_admitted(v: admit_emission_delta(delta: delta, declared: declared)) -} - -test fn declared_surface_and_stage_move_admits() -> Bool { - let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } - let declared = synth_declared( - surfaces: [surface], - stages: [Emit], - ) - let delta = synth_delta( - surface: surface, - before_tag: "before", - after_tag: "after", - stage: Emit, - with_upstream_refusal: false, - ) - match admit_emission_delta(delta: delta, declared: declared) { - AdmittedDeclared { delta: _, matched_declaration: _ } => true - RefusedRegression { delta: _, cause: _ } => false - RefusedUndeclaredDelta { delta: _, missing_from_declaration: _ } => false - RefusedStageMismatch { delta: _, declared_stages: _, observed_stage: _ } => false - RefusedStaleSubstrate { stamp: _, cause: _ } => false - } -} - -test fn undeclared_surface_move_refuses() -> Bool { - let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } - let declared = synth_declared(surfaces: [], stages: [Emit]) - let delta = synth_delta( - surface: surface, - before_tag: "before", - after_tag: "after", - stage: Emit, - with_upstream_refusal: false, - ) - match admit_emission_delta(delta: delta, declared: declared) { - AdmittedDeclared { delta: _, matched_declaration: _ } => false - RefusedRegression { delta: _, cause: _ } => false - RefusedUndeclaredDelta { delta: _, missing_from_declaration: s } => - match s { - GeneratedArtifactSurface { artifact: CiYamlArtifact } => true - _ => false - } - RefusedStageMismatch { delta: _, declared_stages: _, observed_stage: _ } => false - RefusedStaleSubstrate { stamp: _, cause: _ } => false - } -} - -test fn stage_mismatch_refuses_when_emit_not_declared() -> Bool { - let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } - let declared = synth_declared(surfaces: [surface], stages: [Infer]) - let delta = synth_delta( - surface: surface, - before_tag: "before", - after_tag: "after", - stage: Emit, - with_upstream_refusal: false, - ) - match admit_emission_delta(delta: delta, declared: declared) { - AdmittedDeclared { delta: _, matched_declaration: _ } => false - RefusedRegression { delta: _, cause: _ } => false - RefusedUndeclaredDelta { delta: _, missing_from_declaration: _ } => false - RefusedStageMismatch { delta: _, declared_stages: _, observed_stage: os } => os == Emit - RefusedStaleSubstrate { stamp: _, cause: _ } => false - } -} - -test fn upstream_refusal_with_byte_move_refuses_regression() -> Bool { - let surface = Stage0SeedFile { rel_path: "src/v1/stage0/src/lib.rs" as FilePath } - let declared = synth_declared(surfaces: [surface], stages: [Emit, Infer]) - let delta = synth_delta( - surface: surface, - before_tag: "before", - after_tag: "after", - stage: Emit, - with_upstream_refusal: true, - ) - match admit_emission_delta(delta: delta, declared: declared) { - AdmittedDeclared { delta: _, matched_declaration: _ } => false - RefusedRegression { delta: _, cause: c } => c == UpstreamRefusalMaskedAtEmit - RefusedUndeclaredDelta { delta: _, missing_from_declaration: _ } => false - RefusedStageMismatch { delta: _, declared_stages: _, observed_stage: _ } => false - RefusedStaleSubstrate { stamp: _, cause: _ } => false - } -} - -fn verdict_tag(v: EmissionAdmissionVerdict) -> Int { - emission_admission_verdict_fold( - v: v, - fold: EmissionAdmissionVerdictFold { - admitted_declared: fn(_, _) { 1 }, - refused_regression: fn(_, _) { 2 }, - refused_undeclared: fn(_, _) { 3 }, - refused_stage_mismatch: fn(_, _, _) { 4 }, - refused_stale_substrate: fn(_, _) { 5 }, - }, - ) -} - -test fn verdict_fold_discriminates_every_arm() -> Bool { - let surface = GeneratedArtifactSurface { artifact: CiYamlArtifact } - let declared = synth_declared(surfaces: [surface], stages: [Emit]) - let moving = synth_delta( - surface: surface, - before_tag: "b", - after_tag: "a", - stage: Emit, - with_upstream_refusal: false, - ) - let no_op = synth_delta( - surface: surface, - before_tag: "x", - after_tag: "x", - stage: Emit, - with_upstream_refusal: false, - ) - verdict_tag(v: admit_emission_delta(delta: moving, declared: declared)) == 1 - && verdict_tag( - v: admit_emission_delta( - delta: moving, - declared: synth_declared(surfaces: [], stages: [Emit]), - ), - ) == 3 - && verdict_tag( - v: admit_emission_delta( - delta: moving, - declared: synth_declared(surfaces: [surface], stages: [Infer]), - ), - ) == 4 - && verdict_tag(v: admit_emission_delta(delta: no_op, declared: synth_declared(surfaces: [], stages: []))) == 1 -} diff --git a/docs/plans/emission-admission-stage-aware-pipeline-design.md b/docs/plans/emission-admission-stage-aware-pipeline-design.md index 59f7a9ff9a6..8e64943e70e 100644 --- a/docs/plans/emission-admission-stage-aware-pipeline-design.md +++ b/docs/plans/emission-admission-stage-aware-pipeline-design.md @@ -1,23 +1,25 @@ # Emission admission + stage-aware pipeline (declared-change vs regression) -**Status:** Stage 1 LANDED (vocabulary carrier + `admit_emission_delta` + synthetic witnesses). -Stages 2–6 remain design-only until separately dispatched. - -Work item: `node://adhoc-48a1f19c-1f8` (session calm-eagle-92). - -Parent context: v1-deletion lane (`still-bat-561`). This design unblocks honest -warm-merge admission, phased single-process CI, and self-host/regen gates that -today can only compare bytes — not classify *why* bytes changed. - -DESIGN refs: §2 (reuse existing stage/change authorities; no parallel ledger), -§3 (`gunbc.bootstrap.CompilerStage` is the stage authority; -`FrontierProbeStage` is harness vocabulary, not a second pipeline), -§4 (emission is the downstream half of the two-stage contract in -[compiler-guarantee-recovery-gap-analysis.md](compiler-guarantee-recovery-gap-analysis.md) +**Status:** design-only (model-before-implement). This document is the deliverable for +work item `node://adhoc-48a1f19c-1f8` (session calm-eagle-92). No load-bearing `.dag` +carrier lands in the design PR; each implementation stage below is a separately dispatched, +separately-signed PR. + +Parent context: v1-deletion / self-host lane (`still-bat-561`). The operator framing: +self-host must not collapse to one boolean fixed-point check. Each compiler stage needs +durable provenance — implementation, inputs, outputs, refusal, seed fallback, and named +consumers — and emission must **fail fast per stage** with a typed, located reason rather +than deferring classification to review or a corpus-wide rerun. + +DESIGN refs: §2 (DFS existing machinery before minting vocabulary; no parallel ledger), +§3 (`gunbc.bootstrap.CompilerStage` is the stage authority; `FrontierProbeStage` is harness +vocabulary, not a second pipeline), §4 (emission is the downstream half of the two-stage +contract in [compiler-guarantee-recovery-gap-analysis.md](compiler-guarantee-recovery-gap-analysis.md) §1), §5 (undeclared emission delta refuses — never widens to rerun-everything or -accept-anyway), §4b (discriminating RED: regression vs declared-change controls -stay enrolled after walls land), §6 (model-before-implement; scaffolds name -dissolution triggers). +accept-anyway), §4b (discriminating RED: regression vs declared-change controls stay enrolled +after walls land), §6 (model-before-implement; scaffolds name dissolution triggers), §7 +(behavioral equivalence over byte identity for self-host; bricking fear is a git revert, +not rollback machinery). Related: [five-minute-ci-gate-design.md](five-minute-ci-gate-design.md) (`warm-merge-admission`, `phased-single-process-ci`) · @@ -25,18 +27,18 @@ Related: [five-minute-ci-gate-design.md](five-minute-ci-gate-design.md) [module-identity-storage-binding-design.md](module-identity-storage-binding-design.md) (BothWays delta lens) · [post-zero-regen-gate-placement.md](post-zero-regen-gate-placement.md) · `gunbc.bootstrap` · `gunbc.guarantee_measurement` · `v2.compiler.self_host` · -`gunbc.generated_artifact` +`v2.compiler.self_host.emitter_producer_provenance` · `gunbc.generated_artifact` --- ## 0. One-sentence claim -> Every emission delta is admitted only when a **declared change set** names the -> compiler stages and artifact surfaces that may move; anything else is a -> **regression** and refuses. The pipeline is **stage-aware**: each phase -> (regen, floor, admission) stamps which `CompilerStage` produced which artifact, -> and admission consumes those stamps — it does not re-derive the whole compiler -> from bytes alone. +> Every emission delta is admitted only when a **declared change set** names the compiler +> stages and artifact surfaces that may move; anything else is a **regression** and refuses. +> The pipeline is **stage-aware**: each phase stamps which `CompilerStage` produced which +> artifact, and admission consumes those stamps plus a **generation lineage graph** — it +> does not re-derive the whole compiler from bytes alone, and it does not equate +> generation-to-generation output. --- @@ -44,255 +46,186 @@ Related: [five-minute-ci-gate-design.md](five-minute-ci-gate-design.md) | Cost today | Mechanism | |---|---| -| **Warm receipts cannot be trusted for merge** | `warm-merge-admission` can stamp resolve/materialization receipts, but has no typed rule for whether an emission delta was *expected* from the PR's source change. Stale-base and stale-roster refusals exist; *undeclared emitter drift* does not — so admission either reruns cold or risks admitting a regression. | -| **Phases pay duplicate prelude work** | `phased-single-process-ci` wants regen, floor, and admission on one substrate. Without stage-stamped verdicts, each phase must assume the prior phase might have silently changed upstream facts — so discovery/resolve/index rerun anyway (the roadmap's red_control: "silently reruns full discovery when prior phase already fixed those facts"). | -| **Byte gates cannot distinguish intent** | `generated_artifact_drift_gate`, `regen_verify`, and `self_host_realized_comparison` answer only *match / mismatch*. A PR that intentionally changes `05_emit` output and includes regen is indistinguishable from an emitter bug — both are "bytes differ." Review becomes the classifier (§5 specification-without-execution). | -| **Self-host flips lack stage attribution** | `frontier_probe_types` locates blockers at `ProbeStageAssemble` / `ProbeStageEmit` / `ProbeStageSemanticDerivation`, but that vocabulary is harness-facing. There is no durable receipt tying an emission delta to `CompilerStage::Emit` vs an upstream `Infer` refusal planted at emit — the `#7485` containment class. | -| **Guarantee paths stay unmeasured at emission** | `gunbc.guarantee_measurement` names `source→each-emission-target` as an independent path grain, but receipts today do not carry *declared vs regression* disposition — only observed verdict/refusal. Rung honesty (§4b) cannot rank emission-path regressions separately from declared emitter migrations. | +| **Self-host is one boolean** | Frontier disposition, regen verify, and realized comparison each answer a single match/mismatch bit. A stage-local refusal (infer ok, emit wrong) has no durable receipt tying *which stage* failed — the `#7485` containment class. | +| **Warm receipts cannot be trusted for merge** | `warm-merge-admission` can stamp resolve/materialization receipts, but has no typed rule for whether an emission delta was *expected* from the PR's source change. Stale-base refusals exist; *undeclared emitter drift* does not. | +| **Phases pay duplicate prelude work** | `phased-single-process-ci` wants regen, floor, and admission on one substrate. Without stage-stamped verdicts, each phase assumes the prior phase might have silently changed upstream facts — discovery/resolve/index rerun anyway. | +| **Byte gates cannot distinguish intent** | `generated_artifact_drift_gate`, `RegenVerifyGate`, and `self_host_realized_comparison` answer only *match / mismatch*. An intentional emitter migration with regen is indistinguishable from an emitter bug — review becomes the classifier (§5 specification-without-execution). | +| **Producer provenance is partial** | `EmitterProducer` × `EmissionQualification` and `mint_producer_emission_receipt` land per-module receipts, but digest fields remain scaffold-keyed (`producer_emission_receipt_digest_scaffold_note`) and `emitter_produced_baseline` is zero — no join from PR facts to admitted vs regression disposition. | +| **Generation is implicit** | `V2EmitterNative { generation: Int }` names a generation index on the producer axis only. There is no graph of *which generation's emitted artifact* a consumer read, so native-at-small-scale cutover cannot refuse stale-generation replay without widening to cold rerun. | --- -## 2. The gap — what exists vs what is missing +## 2. DFS — existing machinery (reuse, do not fork) -### 2.1 Authorities already correct (reuse, do not fork) +Apply DESIGN §2 before proposing vocabulary: the concept DAG already carries most of the +stage/provenance spine. **This design adds two genuinely new pieces** (§3); everything +below is live on main and should be cited, extended, or joined — not re-invented. -| Fact | Authority | Live today? | -|---|---|---| -| Compiler pipeline stages | `gunbc.bootstrap.CompilerStage` (`Tokenize`…`Emit`) | ✓ `dag/gunbc/bootstrap.dag` | -| Stage input/output kinds | `gunbc.bootstrap.StageInput` / `StageOutput` / `ArtifactKind` | ✓ modeled, lightly exercised | -| Change → affected stages (bootstrap edits) | `gunbc.bootstrap.ChangeClassification.affects_stages` | ✓ modeled; not wired to CI/self-host | -| Field propagation per stage | `gunbc.bootstrap.FieldPropagation` / `TransformContract` | ✓ modeled; not executed | -| Source-graph change kinds | `v2.std.change.ChangeKind` + `ChangeSet` | ✓ consumed by `v2.lens.affected_set` | -| Emitted-byte digest | `v2.compiler.self_host.canonical_emitted_bytes_digest` | ✓ executed on self-host roster | -| Per-path guarantee receipts | `gunbc.guarantee_measurement.GuaranteeMeasurementReceipt` | ✓ Stage-0 vocabulary; no emission-admission join | -| Generated artifact registry | `gunbc.generated_artifact` + drift witnesses | ✓ byte fixed-point; no declared-change arm | -| Harness probe positions | `v2.compiler.self_host.frontier_probe_types.FrontierProbeStage` | ✓ distinct from `CompilerStage` by design | +### 2.1 Producer × qualification provenance (live) -### 2.2 The violation (three coupled gaps) +Authority: `v2.compiler.self_host.emitter_producer_provenance`. -1. **No declared-change carrier.** PRs carry git diffs and affected-set projections, but nothing in-tree states *which compiler stages and emission surfaces are allowed to move* for this change. `ChangeClassification` in `gunbc.bootstrap` is bootstrap-authoring vocabulary only — not joined to pull-request admission. +| Symbol | Role | +|---|---| +| `EmitterProducer` | Orthogonal producer axis: `V1SeedEmitter` \| `V2EmitterInterpreted` \| `V2EmitterNative { generation }` | +| `EmissionQualification` | Qualification axis: `SourceProduced` \| `CargoGreen` \| `BehavioralEquivalent` \| `ProductionRouted` | +| `ProducerEmissionReceipt` | Per-module receipt: module/emitter/emitted digests, producer, qualifications, build result, behavioral probe coordinates, `RealizedEmitterClosure` | +| `emitter_producer_mint_admission` | **Construction wall:** `V2EmitterInterpreted` / `V2EmitterNative` refuse when `v1.compiler.emit_rust` is reachable in `realized_closure` (`^v2_emitter_in_seed_emit_rust_closure`) | +| `mint_producer_emission_receipt` | Sole minter for `ProducerEmissionReceipt`; refuses when mint admission refuses | +| `emitter_produced_baseline` | Counted baseline `0` — frontier `EmitterProduced` rows stay empty until execution-measured verdict digests land (`frontier_probe_survey` dissolve-on) | +| `producer_emission_receipt_digest_scaffold_note` | Honest scaffold: digests still keyed on `symbol_identity_digest` until measured binding | -2. **No emission-admission verdict.** Consumers collapse to `bytes_equal` / `bytes_differ`. Missing arms: `AdmittedDeclared { stages, surfaces }`, `RefusedRegression { stage, surface, cause }`, `RefusedUndeclaredDelta`, `RefusedStageMismatch { declared, observed }`. The absorbing fallback class (§5): treating any mismatch as "rerun regen" or "rerun whole floor" without a counted, typed reason. +**What this already buys:** producer/qualification decomposition (replacing `SelfEmitted` +state-space conflation), a mint wall against hand-authored receipts, and structural refusal +of mislabeled V2 producers in seed closures. -3. **No stage-stamped phase pipeline.** Regen, floor, and admission are separate processes (separate `gunbc run` invocations, separate indices). `phased-single-process-ci` needs phase boundaries with *separate verdict stamps* on one shared materialization provider — but the stamp shape is unspecified. +**What it does not yet buy:** PR-level declared-change vs regression, stage-stamped phase +pipeline, or generation lineage as a first-class graph. ---- +### 2.2 Emitted-byte digest + comparison gates (live) -## 3. Scope +| Symbol | Role | +|---|---| +| `v2.compiler.self_host.canonical_emitted_bytes_digest` | Host-grounded digest over actual emitted bytes (`Medium`) | +| `tools.floor_effect_gate_witness.regen_verify_gate_passes` | CI floor gate: `RegenVerifyGate` compares regen output to committed Rust | +| `v2.workflow.self_host_realized_comparison_gate` | Behavioral + staleness gates over realized comparison transport | +| `gunbc.ci_spec.RegenVerifyGate` | Spec row; regen skip policy keyed on merge-base diff vs regen input closure | -### 3.1 In scope (this design) +**What this already buys:** fail-closed byte mismatch on the regen path and behavioral +witnesses on a curated roster. -- Vocabulary and join contracts for **declared change**, **emission delta**, and **admission verdict**. -- **Stage-aware pipeline run** model: phases, stage stamps, artifact surfaces. -- Classification rules: declared-change vs regression (fail-closed). -- First consumers named with discriminating witnesses (design-level; implementation is staged). -- Explicit integration with `warm-merge-admission`, `phased-single-process-ci`, regen/self-host, and generated-artifact drift. +**What it does not yet buy:** classification of *why* bytes moved (declared migration vs +regression). -### 3.2 Out of scope (named neighbors) +### 2.3 Stage vocabulary (live, lightly joined) -| Neighbor | Why out | +| Symbol | Role | |---|---| -| Implementing warm-merge-admission (#7522) | Downstream consumer; this design is its admission *ontology*. | -| Materialization substrate (#7534) | Shared cache keys are orthogonal; this design consumes materialization receipts, does not define them. | -| Full `ChangeClassification` → live bootstrap planner | Bootstrap authoring tool; only the *join pattern* is reused for PR declared-change. | -| Byte-identical self-host fixed point | Retired non-goal (DESIGN §7); admission uses behavioral + digest receipts, not byte identity as the sole oracle. | -| DDC / `ddc_reference_compiler` | [seed-honesty-discharge-design.md](seed-honesty-discharge-design.md) — consumes admission receipts, does not define them. | -| Guarantee ladder disposition derivation | `gunbc.guarantee_measurement` Stage 0 only; dispositions stay on the claims carrier. | +| `gunbc.bootstrap.CompilerStage` | Canonical pipeline stages (`Tokenize`…`Emit`) | +| `gunbc.bootstrap.ChangeClassification.affects_stages` | Bootstrap-authoring: which stages a bootstrap edit may move | +| `v2.compiler.self_host.frontier_probe_types.FrontierProbeStage` | Harness probe positions (`ProbeStageAssemble` / `ProbeStageEmit` / …) — distinct from `CompilerStage` by design | +| `v2.std.change.ChangeKind` + `ChangeSet` | Source-graph change kinds; consumed by `v2.lens.affected_set` | -### 3.3 Load-bearing files (escalate before edit under a stale brief) +### 2.4 Neighbors (consume, do not redefine) -- `dag/gunbc/bootstrap.dag` — extend only via new exported types or new fns; do not repurpose `CompilerStage` variants. -- `src/v2/std/change.dag` — declared-change projection from `ChangeSet`. -- New module home (proposed): `gunbc.emission_admission` under `dag/gunbc/` (product/workflow layer, not `std/`). -- `src/v2/workflow/ci_floor_plan.dag` / `dag/gunbc/commit_workflow.dag` — phase enrollment (later PRs). -- `dag/tools/generated_artifact_gate.dag` — regen phase consumer (later PR). +| Authority | Relationship | +|---|---| +| `gunbc.generated_artifact` | Registry + drift witnesses — byte fixed-point; gains a declared-change arm | +| `gunbc.guarantee_measurement` | Per-path receipts — gains admission verdict projection on emission paths | +| [module-identity-storage-binding-design.md](module-identity-storage-binding-design.md) | BothWays quotient-delta lawful edits — orthogonal semantic comparison, not authority-vs-seed | --- -## 4. Target model +## 3. Two genuinely new pieces -### 4.1 Artifact surface (what emission admission governs) +Everything in §2 is reuse. **Only these are new design obligations:** -An **emission surface** is a typed target for bytes the compiler pipeline (or a -generated-artifact emitter) produces. Surfaces are enumerated — not free paths — -so admission cannot be bypassed via a novel string path. +### 3.1 Declared-change vs regression (not generation equivalence) -```dag -type EmissionSurface - = Stage0SeedFile { rel_path: FilePath } // GENERATED_STAGE0_FILES roster - | GeneratedArtifactSurface { artifact: GeneratedArtifact } - | TargetLanguageBundle { target: GuaranteeTargetName, module: QualifiedModule } - | OrchestrationMedium { medium: Symbol } // e.g. bash slice enrolled in ci_spec -``` +**Problem:** Today's gates ask "does output match committed?" or "does generation N match +generation N−1?" Neither answers the operational question: **did this PR declare that these +surfaces may move?** -**Authority:** surfaces derive from existing registries (`gunbc.generated_artifact`, -`gunbc.stage0_emit_model.generated_stage0_files`, self-host frontier module list, -`gunbc.guarantee_measurement.GuaranteeTargetName`). A path literal not reachable -from a registry row is `SurfaceUnknown` and refuses — never admitted by glob. - -### 4.2 Stage stamp (pipeline awareness) - -Each pipeline phase produces a **stage stamp** recording which `CompilerStage` -values contributed to an artifact (not just the last stage — emit can plant -upstream `CompilerError` nodes). - -```dag -type StageContribution - = StageProduced { stage: CompilerStage } - | StageForwarded { stage: CompilerStage, from_prior_phase: PipelinePhase } - | UpstreamRefusalEmbedded { origin_stage: CompilerStage, diagnostic: NonEmptyStr } - -type StageStamp { - phase: PipelinePhase - contributions: List - substrate_revision: ContentFingerprint // shared materialization identity -} - -type PipelinePhase - = RegenPhase - | FloorPhase - | AdmissionPhase -``` +**New carrier (proposed name: `EmissionAdmissionVerdict`):** -**Law:** `FrontierProbeStage` projects to `CompilerStage` via an explicit total -function `compiler_stage_from_probe` when probe receipts are consumed — never the -identity map. Harness positions (`Assemble`) are not compiler stages. +| Verdict | Meaning | +|---|---| +| `AdmittedDeclared { stages, surfaces, evidence }` | Observed delta ⊆ declared change set; stage stamps consistent | +| `RefusedRegression { stage, surface, cause }` | Bytes/behavior moved without declaration or with failed witness | +| `RefusedUndeclaredDelta` | Delta on a registered surface with no `allowed_surface_moves` entry | +| `RefusedStageMismatch { declared, observed }` | Declared `Infer`-only but emit digest moved (or converse) | +| `RefusedStaleSubstrate { expected_revision, observed_revision }` | Warm receipt replay against wrong materialization revision | -### 4.3 Declared change set (author intent) +**Declared change set (proposed):** derived from PR facts — affected-set projection, +touched emitter modules, explicit frontier row flips, regen-input-closure membership — +joined to `CompilerStage` and `ArtifactSurface` (generated-artifact registry paths, seed +files, behavioral probe coordinates). Human `AuthorExplicit` override is an open question +(§9). -A **declared change set** is the PR's claim about which stages and surfaces may -move. It is derived — not hand-authored per PR — from facts already in the tree: +**Law:** admission is **not** generation-to-generation equivalence. A declared emitter +migration may admit digest movement with behavioral green; an undeclared move refuses even +if bytes happen to match a prior generation. -| Source | Projection | -|---|---| -| Git merge-base diff + `v2.lens.affected_set` | `ChangeSet` over module graph | -| `v2.std.change.RecomputePlan` / readiness layers | stages that must rerun vs preserve | -| Touch of emitter / translate / generated-artifact emit modules | `CompilerStage::Emit` (+ upstream if not lossless) | -| `gunbc.bootstrap.ChangeClassification` rows (when editing bootstrap types) | explicit `affects_stages` | -| Generated-artifact registry + diff | `EmissionSurface` rows for touched artifacts | - -```dag -type DeclaredChangeSet { - subject_revision: MeasurementRevision // guarantee_measurement vocabulary - allowed_stage_moves: List - allowed_surface_moves: List - derivation_evidence: DeclaredChangeEvidence // typed, located — not prose -} - -type DeclaredChangeEvidence - = DerivedFromAffectedSet { changes: ChangeSet } - | DerivedFromBootstrapPlan { change: ChangeClassification } - | AuthorExplicit { reason: NonEmptyStr } // rare; counted; never default -``` +### 3.2 Generation lineage graph -**Fail-closed:** `DeclaredChangeUnknown` when affected-set refuses, selection -fails closed, or a touched path is outside modeled surfaces. Unknown ⇒ no -admission — not "allow and rerun." +**Problem:** `V2EmitterNative { generation: Int }` tags the producer but does not record +which emitted artifact generation a consumer actually used. Native cutover, warm-merge +replay, and phased CI all need to refuse **stale-generation artifacts** without absorbing +into "rerun everything." -### 4.4 Emission delta (observed fact) +**New graph (proposed):** -```dag -type EmissionDelta { - surface: EmissionSurface - before: ContentFingerprint - after: ContentFingerprint - stage_stamp: StageStamp - observed_at_phase: PipelinePhase -} ``` - -Deltas are observed by execution — regen compare, drift gate, self-host -`canonical_emitted_bytes_digest`, warm receipt replay. Numeric literals copied -from the current tree are not oracles (DESIGN §5 merge-blocking test rule). - -### 4.5 Admission verdict (the seam) - -```dag -type EmissionAdmissionVerdict - = AdmittedDeclared { - delta: EmissionDelta - matched_declaration: DeclaredChangeSet - } - | RefusedRegression { - delta: EmissionDelta - cause: EmissionRegressionCause - } - | RefusedUndeclaredDelta { - delta: EmissionDelta - missing_from_declaration: EmissionSurface - } - | RefusedStageMismatch { - delta: EmissionDelta - declared_stages: List - observed_stage: CompilerStage - } - | RefusedStaleSubstrate { - stamp: StageStamp - cause: NonEmptyStr - } - -type EmissionRegressionCause - = SurfaceMovedWithoutDeclaration - | StageAttributionMismatch - | UpstreamRefusalMaskedAtEmit - | BehavioralWitnessFailed - | DigestMismatchUndeclared +GenerationNode { index, producer, receipt_digest, substrate_revision } +GenerationEdge { parent, child, delta: EmissionDelta } +ArtifactBinding { path, generation, emitted_source_digest } +ConsumerRead { consumer_id, artifact_binding, admission_verdict } ``` -**Core decision** (pure, total on well-formed inputs): +**Laws:** -``` -admit_emission_delta(delta, declared) -> - if delta.before == delta.after -> AdmittedDeclared (no-op move) - else if !surface_allowed(delta.surface, declared) -> RefusedUndeclaredDelta - else if !stage_allowed(delta.stage_stamp, declared) -> RefusedStageMismatch - else if upstream_refusal_masked(delta) -> RefusedRegression - else -> AdmittedDeclared -``` +- Every `ArtifactBinding` points at exactly one `GenerationNode` that holds a + `ProducerEmissionReceipt` or an honest `SeedRetained` / scaffold row. +- Consumers (`RegenVerifyGate`, warm-merge admission, floor witness) record + `ConsumerRead` — not just Bool pass/fail. +- Replay of a warm receipt refuses when `substrate_revision` or `generation` edge does + not match the stamping run (fail-closed; no widen to cold rerun without counted cause). + +**Dissolve-on:** when `frontier_probe_survey` binds execution-measured digests, lineage +edges upgrade from scaffold digests to measured digests without changing graph shape. + +--- + +## 4. Bricking de-risk (operator ruling) + +Emitted Rust is **plain text in git**. A bad emitter migration does not require rollback +machinery, feature flags, or absorbing "rerun regen until green": -No arm widens: missing declaration never triggers regen-all; mismatch never -falls through to green. +1. `git checkout` the last good commit (or revert the PR). +2. Seed-retained modules continue to compile from committed `src/v1/**` until frontier rows + flip with receipts. + +This design **does not** introduce a rollback subsystem. It introduces **earlier, typed +refusal** so bricking is caught at regen/admission with a located cause (`RefusedRegression`, +`RefusedUndeclaredDelta`) rather than after merge or fleet deploy. The psychological "fear +of bricking" is already handled by version control; the gap is *classification at the +gate*. --- -## 5. Stage-aware pipeline (phased single process) +## 5. Stage-aware pipeline model -### 5.1 End-to-end shape +### 5.1 Phases on one substrate (`phased-single-process-ci`) ``` -┌─────────────────────────────────────────────────────────────────┐ -│ One initialized process (shared materialization provider) │ -│ │ -│ RegenPhase │ -│ ├─ compute DeclaredChangeSet from PR facts │ -│ ├─ run regen emitters → EmissionDelta[] │ -│ ├─ StageStamp per delta (Emit + forwarded upstream) │ -│ └─ RegenVerdict = fold admit_emission_delta │ -│ │ │ -│ ▼ (RegenVerdict must be Admitted* or Refused — blocks) │ -│ FloorPhase │ -│ ├─ reuse substrate_revision + declared set │ -│ ├─ witness / compile / native runs │ -│ └─ FloorVerdict (separate stamp; may not rerun resolve) │ -│ │ │ -│ ▼ │ -│ AdmissionPhase (merge / warm-merge-admission) │ -│ ├─ validate warm receipts against declared + stamps │ -│ └─ AdmissionVerdict → merge gate │ -└─────────────────────────────────────────────────────────────────┘ +┌─ Phase: Regen ─────────────────────────────────────────────────┐ +│ regen_stage0 / generated_artifact_gate │ +│ emit StageStamp + EmissionDelta list + GenerationNode │ +│ refuse: RefusedUndeclaredDelta (fail fast — floor not run) │ +└───────────────────────────┬──────────────────────────────────┘ + │ stamps + lineage edges +┌─ Phase: Floor ────────────▼──────────────────────────────────┐ +│ compile-clean + witness corpus on shared materialization │ +│ inherit stamps; refuse RefusedStaleSubstrate if replay │ +└───────────────────────────┬──────────────────────────────────┘ + │ +┌─ Phase: Admission ────────▼──────────────────────────────────┐ +│ warm-merge-admission / merge gate │ +│ validate receipts against DeclaredChangeSet + ConsumerRead │ +│ AdmissionVerdict → merge allow/refuse │ +└────────────────────────────────────────────────────────────────┘ ``` -**Ordering law (from roadmap):** regen still gates floor. Admission may consume -warm receipts only when `substrate_revision` and `DeclaredChangeSet` match the -stamping run. +**Ordering law:** regen still gates floor (existing `ci_regen_floor_skip_policy_note`). +Admission may consume warm receipts only when `substrate_revision`, `DeclaredChangeSet`, and +generation lineage match the stamping run. ### 5.2 Prelude duplication metric -First slice for `phased-single-process-ci` (roadmap handback): attribute time -spent in resolve, index construction, and discovery per phase with and without -stamps. **RED control:** if phase *N+1* repeats resolve whose inputs are -unchanged per stamp, count > 0 ⇒ regression refusal (the roadmap red_control, -made executable). +First slice for `phased-single-process-ci`: attribute resolve/index/discovery time per +phase with and without stamps. **RED control:** if phase *N+1* repeats resolve whose inputs +are unchanged per stamp, count > 0 ⇒ `RefusedStaleSubstrate` or a dedicated prelude +duplication refusal (roadmap red_control, made executable). --- @@ -301,11 +234,11 @@ made executable). | Consumer | Declared-change source | Regression signal | |---|---|---| | `warm-merge-admission` | PR affected set + stamped receipts at merge base | Receipt replay produces undeclared `EmissionDelta` | -| `generated_artifact_drift_gate` | diff touches artifact authority modules | `artifact_generate` ≠ committed without `allowed_surface_moves` | -| `regen_stage0 --verify` | diff touches `src/v2/compiler/*emit*` or frontier SelfEmitted rows | seed file digest mismatch undeclared | -| `self_host_realized_comparison` | frontier row flip / emitter edit in PR | behavioral witness fails with declared-only digest move | -| `ci_merge_admission_emit` | gate roster + floor stamp (existing) joined with admission verdict | stale-base / stale-roster (existing) + new undeclared-delta arm | -| `gunbc.guarantee_measurement` | probe harness declares subject revision | `ExecutionDiverged` on emission path with clean compile admission | +| `generated_artifact_drift_gate` | diff touches artifact authority modules | generate ≠ committed without `allowed_surface_moves` | +| `RegenVerifyGate` / `regen_verify_gate_passes` | diff touches `src/v2/compiler/*emit*` or frontier rows | seed digest mismatch undeclared | +| `self_host_realized_comparison` | frontier flip / emitter edit in PR | behavioral witness fails with digest-only move | +| `mint_producer_emission_receipt` | (future) declared emitter migration | mint with `BuildRefused` or wrong producer in closure | +| `gunbc.guarantee_measurement` | probe declares subject revision | `ExecutionDiverged` on emission path with clean compile | --- @@ -315,61 +248,100 @@ Each verdict arm gets a discriminating pair: | Arm | GREEN control | RED control | |---|---|---| -| `AdmittedDeclared` | emitter edit in PR + regen committed + `allowed_surface_moves` contains surface | same edit without declaration ⇒ `RefusedUndeclaredDelta` | -| `RefusedRegression` | planted emitter bug without source change ⇒ refusal | declaration cannot green a behavioral failure | -| `RefusedStageMismatch` | declared `Infer`-only change but emit digest moves | mismatch stage attribution | +| `AdmittedDeclared` | emitter edit + regen + declaration contains surface | same edit without declaration ⇒ `RefusedUndeclaredDelta` | +| `RefusedRegression` | planted emitter bug without source change ⇒ refusal | declaration cannot green behavioral failure | +| `RefusedStageMismatch` | declared `Infer`-only change | emit digest moves | | `RefusedStaleSubstrate` | warm receipt from prior `substrate_revision` | replay after index rebuild without restamp | +| Generation stale read | consumer reads binding at generation G | replay claims G but artifact is G−1 | | Prelude duplication | phase 2 skips resolve when stamp says preserved | forced re-resolve ⇒ counted refusal | -Probes that go green when a wall lands **flip to permanent regression controls** — -they do not retire (§4b dissolution-on rule). +Probes that go green when a wall lands **flip to permanent regression controls** — they do +not retire (§4b dissolution-on rule). --- -## 8. Staged plan (design only — implementation is separate PRs) +## 8. Staged implementation plan (future PRs — not this document) -| Stage | Deliverable | Depends on | +| Stage | Deliverable | Dissolution trigger | |---|---|---| -| **0 — this document** | Sign-ready design + operator review | — | -| **1 — vocabulary carrier** | `gunbc.emission_admission` types + pure `admit_emission_delta` + unit witnesses on synthetic deltas | Stage 0 sign-off | -| **2 — declared-change derivation** | `declared_change_set_from_pr_facts` wired to affected-set; refusal when selection fails | Stage 1 | -| **3 — regen phase stamp** | `regen_stage0` / `generated_artifact_gate` emit `StageStamp` + deltas; regen refuses undeclared | Stage 2 | -| **4 — warm-merge join** | #7522 admits from warm receipts only with matching stamps + declaration | Stage 3 | -| **5 — phased single process** | `phased-single-process-ci` shares substrate; prelude duplication witness | Stage 4 | -| **6 — guarantee path rows** | emission-path measurement receipts carry admission verdict projection | Stage 1 (can parallel 3–5) | - -**Dissolution triggers (scaffolds):** - -- Stage 1: delete any interim `bytes_differ` only branch in merge admission once - verdict arm executes in CI. -- Stage 3: retire `regen_divergence_count` prose-oracle when structured - `EmissionDelta` list is the transport (extends regen ratchet retirement). -- Stage 5: delete duplicate `process_shared_index` construction in per-phase - entrypoints when stamp hit rate is receipted ≥ target on representative PRs. +| **0 — this document** | Sign-ready design + doc graph bind | Operator sign-off | +| **1 — vocabulary carrier** | Types + pure `admit_emission_delta` + synthetic unit witnesses | Interim bytes-only merge branches gain verdict arm | +| **2 — declared-change derivation** | `declared_change_set_from_pr_facts` wired to affected-set; refuse when selection fails | Hand-classified PR intent in review prose | +| **3 — regen phase stamp** | `regen_stage0` emits `StageStamp` + deltas; regen refuses undeclared | `regen_divergence_count` prose-oracle | +| **4 — generation lineage** | `GenerationNode` / `ArtifactBinding` / `ConsumerRead` on regen + warm paths | Implicit generation in producer tag only | +| **5 — warm-merge join** | #7522 admits warm receipts only with matching stamps + declaration | Cold rerun as default merge path | +| **6 — phased single process** | Shared substrate; prelude duplication witness | Per-phase `process_shared_index` duplication | +| **7 — guarantee path rows** | emission-path measurement carries admission projection | Emission path unmeasured for intent | --- ## 9. Open questions (operator sign-off) -1. **AuthorExplicit declaration:** should human override ever admit a delta without - affected-set evidence, or is that always a §5 escape hatch to refuse? +### Q1 — `AuthorExplicit` override + +Should a human declaration ever admit a delta without affected-set evidence? + +| Option | Tradeoff | +|---|---| +| **A. Refuse always** | No escape hatch; missing affected-set evidence is `RefusedUndeclaredDelta` | +| **B. Typed override row** | `AuthorExplicit { author, rationale, surfaces }` admissible with counted audit + RED control | +| **C. FrontierAccepted only** | Override allowed only for enrolled scaffold rows, not production surfaces | + +**Recommendation:** **B** with a construction wall — override row required, never a silent +flag; aligns with §5 "no escape hatches" while allowing genuine operator migrations. -2. **Behavioral vs digest on self-host:** for `SelfEmitted` modules, is digest - match alone ever sufficient, or must `AdmittedDeclared` always include a - behavioral witness when the surface is `Stage0SeedFile`? +### Q2 — Behavioral vs digest on self-host -3. **Frontier scaffold rows:** do counted `SeedRetained` / `UnpinnedFrontier` rows - automatically widen `allowed_surface_moves`, or does each row carry an explicit - `DeclaredChangeTemplate`? +For `SelfEmitted` / `Stage0SeedFile` surfaces, is digest match alone ever sufficient? -4. **Registry completeness:** is `SurfaceUnknown` a hard refusal on the merge path, - or a counted `FrontierAccepted` disposition until the generated-artifact - registry covers all emit surfaces? +| Option | Tradeoff | +|---|---| +| **A. Digest only** | Faster CI; risks §5 validation-standing-where-construction-was-available | +| **B. Behavioral always** | Matches `BehavioralEquivalent` qualification; costs wet witness | +| **C. Tiered by surface** | Digest for formatting-only paths; behavioral when probe enrolled | + +**Recommendation:** **C** — join to existing `EmissionQualification` and behavioral transport +roster; refuse `AdmittedDeclared` without `BehavioralEquivalent` when a probe is enrolled. + +### Q3 — Frontier scaffold rows + +Do `SeedRetained` / `UnpinnedFrontier` rows auto-widen `allowed_surface_moves`? + +| Option | Tradeoff | +|---|---| +| **A. Auto-widen by row class** | Less author burden; risks over-admission | +| **B. Per-row `DeclaredChangeTemplate`** | Explicit; scales with roster size | +| **C. Census-derived default** | `witness_entry_eligibility_census` projects templates once | + +**Recommendation:** **B** for production surfaces, **A** only for counted scaffold classes +with dissolution triggers named on the row. + +### Q4 — Registry completeness + +Is `SurfaceUnknown` hard refusal or `FrontierAccepted` on the merge path? + +| Option | Tradeoff | +|---|---| +| **A. Hard refusal** | Fail-closed; blocks merge until registry complete | +| **B. FrontierAccepted** | Merge proceeds with counted debt; risks silent widen | +| **C. Refuse on warm path only** | Cold merge ok with debt; warm replay refuses | + +**Recommendation:** **C** — matches §4b `FrontierAccepted` semantics and warm-merge's +higher trust bar. + +### Q5 — Relationship to BothWays quotient delta + +Do lawful BothWays edits always become `AdmittedDeclared` when the six lens laws pass? + +| Option | Tradeoff | +|---|---| +| **A. Always admitted** | Semantic lawful ⇒ admission; digest move expected | +| **B. Requires explicit surface entry** | BothWays green ≠ emission admission without declaration | +| **C. Join only for `SelfEmitted` surfaces** | Seed-emitted paths use quotient delta; generated-artifact paths use registry | -5. **Relationship to `BothWays`:** [module-identity-storage-binding-design.md](module-identity-storage-binding-design.md) - intentional semantic edits compare `R1` vs `R2`, not authority vs seed — does - emission admission treat quotient-delta lawful edits as always `AdmittedDeclared` - when the six lens laws pass, even if digest moves? +**Recommendation:** **C** — [module-identity-storage-binding-design.md](module-identity-storage-binding-design.md) +comparison is semantic (R1 vs R2); emission admission still needs surface declaration for +registry-governed artifacts. --- @@ -377,8 +349,8 @@ they do not retire (§4b dissolution-on rule). This design is ready for operator review when: -- [ ] §4 types are agreed as the single admission authority (no parallel byte gate semantics). -- [ ] Stage 1–3 sequencing is accepted relative to #7522 / `phased-single-process-ci`. -- [ ] Open questions §9 have recorded verdicts (even if "defer with scaffold row"). -- [ ] Parent lane (`still-bat-561`) confirms this unblocks v1-deletion emission gates - without contradicting seed-honesty / real-fixpoint milestones. +1. All symbols in §2 resolve on main (grep-verified at PR open). +2. The two new pieces (§3) are explicit and not conflated with existing gates. +3. Open questions (§9) have options + recommendations recorded in the PR body. +4. Doc graph bind lands (`gunbc.doc_graph_roots` → this slug). +5. No implementation code ships in the design PR. From 7ad31a37e37f36db971118be5339a85fda329d0a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 2 Aug 2026 20:39:59 +0000 Subject: [PATCH 3/3] Revise emission-admission design per operator review. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand beyond declared-change/lineage to requirement revision identity, candidate/promoted lifecycle, recovery anchor, bridge-generation, and successor-capability fix-forward proof. Replace rollback-only de-risk with explicit temporal graph. Record resolved Q1–Q5 decisions. Co-authored-by: Cursor --- dag/gunbc/doc_graph_roots.dag | 2 +- ...n-admission-stage-aware-pipeline-design.md | 409 ++++++++++++------ 2 files changed, 271 insertions(+), 140 deletions(-) diff --git a/dag/gunbc/doc_graph_roots.dag b/dag/gunbc/doc_graph_roots.dag index 77689fc6d94..6e318b06de9 100644 --- a/dag/gunbc/doc_graph_roots.dag +++ b/dag/gunbc/doc_graph_roots.dag @@ -124,7 +124,7 @@ data hand_authored_doc_binds: List = [ slug: "emission-admission-stage-aware-pipeline-design", primary_work: DeclarationRef { module_path: "v2.compiler.self_host.emitter_producer_provenance", decl_name: "emitter_producer_provenance_note", field: WholeDeclaration }, additional_works: [DeclarationRef { module_path: "v2.compiler.self_host", decl_name: "canonical_emitted_bytes_digest", field: WholeDeclaration }, DeclarationRef { module_path: "tools.floor_effect_gate_witness", decl_name: "regen_verify_gate_passes", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.bootstrap", decl_name: "CompilerStage", field: WholeDeclaration }], - dissolution: HasTrigger { text: "Stages 1–7 in the design doc land or are honestly retired: declared-change vs regression verdict executes on the merge path, generation lineage graph binds ConsumerRead on regen and warm-merge, phased-single-process CI prelude-duplication witness is green on a representative PR, and the doc migrates into a registered gunbc.plan.Plan row (the registration becomes the binding)" }, + dissolution: HasTrigger { text: "Stages 1–9 in the design doc land or are honestly retired: candidate/promoted lifecycle with RecoveryAnchor retention, SuccessorCapabilityReceipt fix-forward gate, declared-change vs regression on the merge path, generation lineage binds ConsumerRead on regen and warm-merge (Promoted bindings only), bridge-generation protocol for incompatible requirement revisions, phased-single-process CI prelude-duplication witness is green on a representative PR, and the doc migrates into a registered gunbc.plan.Plan row (the registration becomes the binding)" }, }, HandAuthoredDocBind { home: PlanDoc, diff --git a/docs/plans/emission-admission-stage-aware-pipeline-design.md b/docs/plans/emission-admission-stage-aware-pipeline-design.md index 8e64943e70e..5d6845463ea 100644 --- a/docs/plans/emission-admission-stage-aware-pipeline-design.md +++ b/docs/plans/emission-admission-stage-aware-pipeline-design.md @@ -11,6 +11,9 @@ durable provenance — implementation, inputs, outputs, refusal, seed fallback, consumers — and emission must **fail fast per stage** with a typed, located reason rather than deferring classification to review or a corpus-wide rerun. +**Downstream:** [#7683](https://github.com/gunb-ai/gunbc/pull/7683) builds on this design; +correcting this note is on the critical path for that lane. + DESIGN refs: §2 (DFS existing machinery before minting vocabulary; no parallel ledger), §3 (`gunbc.bootstrap.CompilerStage` is the stage authority; `FrontierProbeStage` is harness vocabulary, not a second pipeline), §4 (emission is the downstream half of the two-stage @@ -18,8 +21,7 @@ contract in [compiler-guarantee-recovery-gap-analysis.md](compiler-guarantee-rec §1), §5 (undeclared emission delta refuses — never widens to rerun-everything or accept-anyway), §4b (discriminating RED: regression vs declared-change controls stay enrolled after walls land), §6 (model-before-implement; scaffolds name dissolution triggers), §7 -(behavioral equivalence over byte identity for self-host; bricking fear is a git revert, -not rollback machinery). +(behavioral equivalence over byte identity for self-host). Related: [five-minute-ci-gate-design.md](five-minute-ci-gate-design.md) (`warm-merge-admission`, `phased-single-process-ci`) · @@ -35,10 +37,11 @@ Related: [five-minute-ci-gate-design.md](five-minute-ci-gate-design.md) > Every emission delta is admitted only when a **declared change set** names the compiler > stages and artifact surfaces that may move; anything else is a **regression** and refuses. -> The pipeline is **stage-aware**: each phase stamps which `CompilerStage` produced which -> artifact, and admission consumes those stamps plus a **generation lineage graph** — it -> does not re-derive the whole compiler from bytes alone, and it does not equate -> generation-to-generation output. +> Promotion from **Candidate** to **Promoted** generation requires stage, behavioral, and +> **successor-capability** evidence — proving the new generation can understand and validate +> the changed requirements needed to build the repair, not merely reproduce the old output. +> Generation *N* stays retained as a **recovery anchor** until generation *N+1* produces a +> valid changed successor. --- @@ -47,19 +50,20 @@ Related: [five-minute-ci-gate-design.md](five-minute-ci-gate-design.md) | Cost today | Mechanism | |---|---| | **Self-host is one boolean** | Frontier disposition, regen verify, and realized comparison each answer a single match/mismatch bit. A stage-local refusal (infer ok, emit wrong) has no durable receipt tying *which stage* failed — the `#7485` containment class. | +| **Detects movement, not fix-forward** | Byte gates detect unexpected output movement but cannot answer: *can the last accepted compiler understand and validate the changed requirements needed to build the repair?* A defective generation may be the only one that can rebuild its own old source while being unable to compile the new requirement revision. | | **Warm receipts cannot be trusted for merge** | `warm-merge-admission` can stamp resolve/materialization receipts, but has no typed rule for whether an emission delta was *expected* from the PR's source change. Stale-base refusals exist; *undeclared emitter drift* does not. | | **Phases pay duplicate prelude work** | `phased-single-process-ci` wants regen, floor, and admission on one substrate. Without stage-stamped verdicts, each phase assumes the prior phase might have silently changed upstream facts — discovery/resolve/index rerun anyway. | | **Byte gates cannot distinguish intent** | `generated_artifact_drift_gate`, `RegenVerifyGate`, and `self_host_realized_comparison` answer only *match / mismatch*. An intentional emitter migration with regen is indistinguishable from an emitter bug — review becomes the classifier (§5 specification-without-execution). | | **Producer provenance is partial** | `EmitterProducer` × `EmissionQualification` and `mint_producer_emission_receipt` land per-module receipts, but digest fields remain scaffold-keyed (`producer_emission_receipt_digest_scaffold_note`) and `emitter_produced_baseline` is zero — no join from PR facts to admitted vs regression disposition. | -| **Generation is implicit** | `V2EmitterNative { generation: Int }` names a generation index on the producer axis only. There is no graph of *which generation's emitted artifact* a consumer read, so native-at-small-scale cutover cannot refuse stale-generation replay without widening to cold rerun. | +| **Generation is implicit** | `V2EmitterNative { generation: Int }` names a generation index on the producer axis only. There is no graph of *which generation's emitted artifact* a consumer read, no candidate/promoted distinction, and no recovery anchor — so native cutover cannot refuse stale-generation replay without widening to cold rerun. | --- ## 2. DFS — existing machinery (reuse, do not fork) Apply DESIGN §2 before proposing vocabulary: the concept DAG already carries most of the -stage/provenance spine. **This design adds two genuinely new pieces** (§3); everything -below is live on main and should be cited, extended, or joined — not re-invented. +stage/provenance spine. **§3 names the genuinely new obligations**; everything below is +live on main and should be cited, extended, or joined — not re-invented. ### 2.1 Producer × qualification provenance (live) @@ -79,8 +83,8 @@ Authority: `v2.compiler.self_host.emitter_producer_provenance`. state-space conflation), a mint wall against hand-authored receipts, and structural refusal of mislabeled V2 producers in seed closures. -**What it does not yet buy:** PR-level declared-change vs regression, stage-stamped phase -pipeline, or generation lineage as a first-class graph. +**What it does not yet buy:** requirement-revision identity, candidate/promoted lifecycle, +fix-forward validation, recovery anchors, or PR-level declared-change vs regression. ### 2.2 Emitted-byte digest + comparison gates (live) @@ -94,8 +98,8 @@ pipeline, or generation lineage as a first-class graph. **What this already buys:** fail-closed byte mismatch on the regen path and behavioral witnesses on a curated roster. -**What it does not yet buy:** classification of *why* bytes moved (declared migration vs -regression). +**What it does not yet buy:** classification of *why* bytes moved, or proof that a new +generation can build the *changed* successor (not merely reproduce the old one). ### 2.3 Stage vocabulary (live, lightly joined) @@ -116,17 +120,50 @@ regression). --- -## 3. Two genuinely new pieces +## 3. New design obligations + +Everything in §2 is reuse. **These are the genuinely new concepts** — detecting unexpected +output movement alone is insufficient; the design must answer whether the last accepted +compiler can understand and validate the changed requirements needed to build the repair. + +### 3.1 Requirement revision identity + +A **requirement revision** is a content-addressed identity for the compiler's obligation +set at a point in time — source language contract, IR shape, emitter contract, bootstrap +requirements, and enrolled witness roster. It is not a git commit and not a generation index; +two generations may share a revision, and one revision may span multiple candidate attempts. + +``` +RequirementRevision { revision_id: Hash, authority_modules: List, witness_roster_digest: Hash } +``` + +**Law:** every `DeclaredTransition`, `CandidateGeneration`, and `PromotedGeneration` names +the `RequirementRevision` it was evaluated against. Comparing artifacts across different +revision IDs without an explicit `BridgeGeneration` protocol is `RefusedRevisionMismatch`. -Everything in §2 is reuse. **Only these are new design obligations:** +### 3.2 Declared transition (revision N → N+1) -### 3.1 Declared-change vs regression (not generation equivalence) +A **declared transition** is the typed statement of what may change between requirement +revisions — not merely which surfaces may move in a PR, but which parts of the requirement +contract are being revised and why. -**Problem:** Today's gates ask "does output match committed?" or "does generation N match -generation N−1?" Neither answers the operational question: **did this PR declare that these -surfaces may move?** +``` +DeclaredTransition { + from_revision: RequirementRevision, + to_revision: RequirementRevision, + allowed_surface_moves: List, + affected_stages: List, + evidence: TransitionEvidence // affected-set projection, operator declaration, bridge protocol ref +} +``` + +**Law:** a `CandidateGeneration` at revision *N+1* is inadmissible without a +`DeclaredTransition` from the currently `PromotedGeneration`'s revision. Human-authored +declarations are **inputs** to admission (see §9 Q1), never overrides of missing evidence. + +### 3.3 Declared-change vs regression (within a revision) -**New carrier (proposed name: `EmissionAdmissionVerdict`):** +Within a single requirement revision, admission still classifies emission deltas: | Verdict | Meaning | |---|---| @@ -136,91 +173,198 @@ surfaces may move?** | `RefusedStageMismatch { declared, observed }` | Declared `Infer`-only but emit digest moved (or converse) | | `RefusedStaleSubstrate { expected_revision, observed_revision }` | Warm receipt replay against wrong materialization revision | -**Declared change set (proposed):** derived from PR facts — affected-set projection, -touched emitter modules, explicit frontier row flips, regen-input-closure membership — -joined to `CompilerStage` and `ArtifactSurface` (generated-artifact registry paths, seed -files, behavioral probe coordinates). Human `AuthorExplicit` override is an open question -(§9). - **Law:** admission is **not** generation-to-generation equivalence. A declared emitter migration may admit digest movement with behavioral green; an undeclared move refuses even if bytes happen to match a prior generation. -### 3.2 Generation lineage graph +### 3.4 Candidate vs promoted generation state + +Generation is not a single axis on `EmitterProducer` — it is a **lifecycle state**: + +| State | Meaning | +|---|---| +| `Candidate { generation, revision, producer, receipts }` | Emitted and validated under candidate rules; not authoritative for promotion, warm reuse, or merge admission | +| `Promoted { generation, revision, producer, receipts, successor_capability }` | The sole authoritative generation for production routing, warm replay, and merge admission | -**Problem:** `V2EmitterNative { generation: Int }` tags the producer but does not record -which emitted artifact generation a consumer actually used. Native cutover, warm-merge -replay, and phased CI all need to refuse **stale-generation artifacts** without absorbing -into "rerun everything." +**Construction wall:** a `Candidate` **cannot be the sole authority promoting itself**. +Promotion requires evidence from outside the candidate's own emission path — at minimum +the retained `RecoveryAnchor` generation and an independent validation chain (§4). -**New graph (proposed):** +**Non-blocking path:** candidate generation through the direct Rust door (`V1SeedEmitter` / +committed seed emit) **must not be blocked** by this design. The operator supports merging +this design note **before** the first behavioral-module promotion; only promotion and +authoritative admission are gated. + +### 3.5 Generation lineage graph ``` -GenerationNode { index, producer, receipt_digest, substrate_revision } -GenerationEdge { parent, child, delta: EmissionDelta } -ArtifactBinding { path, generation, emitted_source_digest } +GenerationNode { index, state: Candidate | Promoted, revision_id, producer, receipt_digest } +GenerationEdge { parent, child, delta: EmissionDelta, transition: DeclaredTransition } +ArtifactBinding { path, generation, state, emitted_source_digest } ConsumerRead { consumer_id, artifact_binding, admission_verdict } +RecoveryAnchor { generation, revision_id, retained_until: PromotedSuccessor } ``` **Laws:** -- Every `ArtifactBinding` points at exactly one `GenerationNode` that holds a - `ProducerEmissionReceipt` or an honest `SeedRetained` / scaffold row. -- Consumers (`RegenVerifyGate`, warm-merge admission, floor witness) record - `ConsumerRead` — not just Bool pass/fail. -- Replay of a warm receipt refuses when `substrate_revision` or `generation` edge does - not match the stamping run (fail-closed; no widen to cold rerun without counted cause). +- Every `ArtifactBinding` points at exactly one `GenerationNode` with an honest + `ProducerEmissionReceipt` or `SeedRetained` row. +- Consumers record `ConsumerRead` — not just Bool pass/fail. +- Warm replay refuses when `substrate_revision`, `generation`, or `state` does not match + the stamping run (`RefusedStaleSubstrate`). +- Only `Promoted` bindings are authoritative for merge admission and warm reuse. + +### 3.6 Retained recovery anchor + +**Recovery anchor** is the last `PromotedGeneration` *N* retained until generation *N+1* +produces a valid **changed** successor (not a byte-identical reproduction). + +``` +RecoveryAnchor { + generation: N, + revision_id, + retained_until: PromotedSuccessor { generation: N+1, successor_capability: Present } +} +``` + +**Law:** the anchor is not deleted on candidate mint. It remains the authority that can +validate whether the candidate understands the *new* requirements — and the fallback that +can still build generation-*N* artifacts if promotion refuses. + +Rollback (`git checkout` an earlier commit) remains possible independently; it is **not** +the fix-forward story and does not discharge the anchor obligation. + +### 3.7 Bridge-generation protocol (incompatible requirement changes) + +When `DeclaredTransition` crosses incompatible requirement revisions (new IR shape, new +emitter contract, new witness roster), a **bridge generation** mediates: + +``` +BridgeGeneration { + anchor: RecoveryAnchor, + candidate: CandidateGeneration, + expand_phase: RequirementExpand, // anchor understands expanded obligation surface + migrate_phase: ArtifactMigrate, // candidate artifacts produced under expanded contract + contract_phase: RequirementContract // promoted revision is strictly smaller than expand +} +``` + +**Law:** bridge is mandatory when `from_revision` and `to_revision` are not provably +compatible by digest-inclusion alone. Skipping bridge and promoting across incompatible +revisions is `RefusedBridgeRequired`. + +### 3.8 Successor-capability receipt (fix-forward, not reproduction) + +A **successor-capability receipt** proves generation *N+1* can build and validate the +*changed* successor — not merely reproduce generation *N*'s output. + +``` +SuccessorCapabilityReceipt { + anchor_generation: N, + candidate_generation: N+1, + transition: DeclaredTransition, + stage_validation: StageValidationVerdict, + behavior_validation: BehaviorValidationVerdict, + fix_forward_proof: FixForwardProof // candidate compiles/validates a changed-requirement artifact anchor cannot +} +``` + +**Law:** `PromotedGeneration N+1` is unwritable without `SuccessorCapabilityReceipt`. +Digest equality between *N* and *N+1* on unchanged surfaces is necessary but not +sufficient — it proves reproduction, not fix-forward. **Dissolve-on:** when `frontier_probe_survey` binds execution-measured digests, lineage -edges upgrade from scaffold digests to measured digests without changing graph shape. +edges and capability proofs upgrade from scaffold digests to measured digests without +changing graph shape. + +--- + +## 4. Fix-forward de-risk (replaces rollback-only framing) + +A prior briefing claimed bricking fear was handled because emitted Rust is committed and +`rustc` builds it, so checking out an earlier commit suffices. That proves **rollback**, +not **fix-forward**. + +**The actual risk:** generation *N* can rebuild its own old source while being unable to +understand the new source, IR, or requirement revision. The repair to a defective +generation *N+1* may only be buildable by the defective generation itself — leaving the +tree stuck even though `git checkout` still works. + +**This design's answer:** + +1. **Retain** `PromotedGeneration N` as `RecoveryAnchor` until *N+1* passes successor-capability. +2. **Bridge** incompatible requirement changes through expand-migrate-contract rather than + big-bang promotion. +3. **Refuse promotion** when only reproduction is proven (digest match without + `FixForwardProof`). +4. **Fail fast** at candidate validation (stage → behavior → fix-forward) before downstream + stages consume a refused artifact. + +Rollback remains a valid operator escape hatch via version control. This design does not +introduce rollback machinery — it introduces **typed fix-forward validation** so promotion +cannot proceed on reproduction alone. --- -## 4. Bricking de-risk (operator ruling) +## 5. Temporal graph (explicit lifecycle) -Emitted Rust is **plain text in git**. A bad emitter migration does not require rollback -machinery, feature flags, or absorbing "rerun regen until green": +``` +AcceptedGeneration N (Promoted) + │ + ├─ RequirementTransition (N → N+1) [DeclaredTransition + revision IDs] + ├─ RecoveryAnchor N [retained until valid successor] + │ + ▼ +Candidate N+1 + │ + ├─ stage validation [per CompilerStage stamps; fail fast] + ├─ behavior validation [BehavioralEquivalent where required; §9 Q2] + ├─ bootstrap + fix-forward validation [SuccessorCapabilityReceipt] + │ + ▼ +PromotedGeneration N+1 [only if all gates pass] + │ + └─ RecoveryAnchor N released [only on valid *changed* successor] +``` -1. `git checkout` the last good commit (or revert the PR). -2. Seed-retained modules continue to compile from committed `src/v1/**` until frontier rows - flip with receipts. +**Retention law:** generation *N* stays `Promoted` and authoritative until generation *N+1* +produces a valid changed successor. A refused candidate does not demote *N*. -This design **does not** introduce a rollback subsystem. It introduces **earlier, typed -refusal** so bricking is caught at regen/admission with a located cause (`RefusedRegression`, -`RefusedUndeclaredDelta`) rather than after merge or fleet deploy. The psychological "fear -of bricking" is already handled by version control; the gap is *classification at the -gate*. +**Candidate law:** generation *N+1* in `Candidate` state may emit, compile, and run +witnesses — including through the direct Rust seed door — but cannot promote itself and +cannot authorize warm reuse or merge admission. --- -## 5. Stage-aware pipeline model +## 6. Stage-aware pipeline model -### 5.1 Phases on one substrate (`phased-single-process-ci`) +### 6.1 Phases on one substrate (`phased-single-process-ci`) ``` ┌─ Phase: Regen ─────────────────────────────────────────────────┐ │ regen_stage0 / generated_artifact_gate │ -│ emit StageStamp + EmissionDelta list + GenerationNode │ +│ emit StageStamp + EmissionDelta + Candidate/Promoted state │ │ refuse: RefusedUndeclaredDelta (fail fast — floor not run) │ └───────────────────────────┬──────────────────────────────────┘ │ stamps + lineage edges ┌─ Phase: Floor ────────────▼──────────────────────────────────┐ │ compile-clean + witness corpus on shared materialization │ │ inherit stamps; refuse RefusedStaleSubstrate if replay │ +│ candidate artifacts: stage + behavior validation │ └───────────────────────────┬──────────────────────────────────┘ │ ┌─ Phase: Admission ────────▼──────────────────────────────────┐ │ warm-merge-admission / merge gate │ -│ validate receipts against DeclaredChangeSet + ConsumerRead │ +│ Promoted bindings only; validate DeclaredTransition + reads │ │ AdmissionVerdict → merge allow/refuse │ └────────────────────────────────────────────────────────────────┘ ``` **Ordering law:** regen still gates floor (existing `ci_regen_floor_skip_policy_note`). -Admission may consume warm receipts only when `substrate_revision`, `DeclaredChangeSet`, and -generation lineage match the stamping run. +Admission may consume warm receipts only for `Promoted` bindings when `substrate_revision`, +`DeclaredTransition`, and generation lineage match the stamping run. -### 5.2 Prelude duplication metric +### 6.2 Prelude duplication metric First slice for `phased-single-process-ci`: attribute resolve/index/discovery time per phase with and without stamps. **RED control:** if phase *N+1* repeats resolve whose inputs @@ -229,20 +373,21 @@ duplication refusal (roadmap red_control, made executable). --- -## 6. Consumer mapping +## 7. Consumer mapping -| Consumer | Declared-change source | Regression signal | +| Consumer | Declared-change source | Regression / refusal signal | |---|---|---| -| `warm-merge-admission` | PR affected set + stamped receipts at merge base | Receipt replay produces undeclared `EmissionDelta` | +| `warm-merge-admission` | `Promoted` receipts + `DeclaredTransition` at merge base | Undeclared `EmissionDelta`; `Candidate` binding on warm path | | `generated_artifact_drift_gate` | diff touches artifact authority modules | generate ≠ committed without `allowed_surface_moves` | -| `RegenVerifyGate` / `regen_verify_gate_passes` | diff touches `src/v2/compiler/*emit*` or frontier rows | seed digest mismatch undeclared | -| `self_host_realized_comparison` | frontier flip / emitter edit in PR | behavioral witness fails with digest-only move | -| `mint_producer_emission_receipt` | (future) declared emitter migration | mint with `BuildRefused` or wrong producer in closure | +| `RegenVerifyGate` / `regen_verify_gate_passes` | diff touches emit modules or frontier rows | seed digest mismatch undeclared | +| `self_host_realized_comparison` | frontier flip / emitter edit in PR | behavioral witness fails; digest relabelled as equivalence | +| `mint_producer_emission_receipt` | declared emitter migration | mint with `BuildRefused` or wrong producer in closure | +| promotion gate (new) | `SuccessorCapabilityReceipt` | reproduction without fix-forward proof | | `gunbc.guarantee_measurement` | probe declares subject revision | `ExecutionDiverged` on emission path with clean compile | --- -## 7. Witness / RED discipline (§4b, §5) +## 8. Witness / RED discipline (§4b, §5) Each verdict arm gets a discriminating pair: @@ -252,7 +397,9 @@ Each verdict arm gets a discriminating pair: | `RefusedRegression` | planted emitter bug without source change ⇒ refusal | declaration cannot green behavioral failure | | `RefusedStageMismatch` | declared `Infer`-only change | emit digest moves | | `RefusedStaleSubstrate` | warm receipt from prior `substrate_revision` | replay after index rebuild without restamp | -| Generation stale read | consumer reads binding at generation G | replay claims G but artifact is G−1 | +| `Candidate` self-promotion | promotion refused without anchor evidence | candidate-only path claims `Promoted` | +| `SuccessorCapability` | candidate builds changed-requirement artifact | digest-only match between *N* and *N+1* | +| `SurfaceUnknown` | enrolled in candidate/audit only | `SurfaceUnknown` on promotion or warm path | | Prelude duplication | phase 2 skips resolve when stamp says preserved | forced re-resolve ⇒ counted refusal | Probes that go green when a wall lands **flip to permanent regression controls** — they do @@ -260,97 +407,81 @@ not retire (§4b dissolution-on rule). --- -## 8. Staged implementation plan (future PRs — not this document) +## 9. Operator decisions (resolved) -| Stage | Deliverable | Dissolution trigger | -|---|---|---| -| **0 — this document** | Sign-ready design + doc graph bind | Operator sign-off | -| **1 — vocabulary carrier** | Types + pure `admit_emission_delta` + synthetic unit witnesses | Interim bytes-only merge branches gain verdict arm | -| **2 — declared-change derivation** | `declared_change_set_from_pr_facts` wired to affected-set; refuse when selection fails | Hand-classified PR intent in review prose | -| **3 — regen phase stamp** | `regen_stage0` emits `StageStamp` + deltas; regen refuses undeclared | `regen_divergence_count` prose-oracle | -| **4 — generation lineage** | `GenerationNode` / `ArtifactBinding` / `ConsumerRead` on regen + warm paths | Implicit generation in producer tag only | -| **5 — warm-merge join** | #7522 admits warm receipts only with matching stamps + declaration | Cold rerun as default merge path | -| **6 — phased single process** | Shared substrate; prelude duplication witness | Per-phase `process_shared_index` duplication | -| **7 — guarantee path rows** | emission-path measurement carries admission projection | Emission path unmeasured for intent | - ---- - -## 9. Open questions (operator sign-off) +### Q1 — Human override -### Q1 — `AuthorExplicit` override +**Decision:** A human may author a **typed declaration of intended change** as an input to +admission. A human may **not** override missing affected-set or requirement evidence. -Should a human declaration ever admit a delta without affected-set evidence? +- Author-declared change = **input** to `DeclaredTransition`, not an admitting escape hatch. +- A frontier row may authorize **candidate** or **shadow** work; it must **not** authorize + promotion. -| Option | Tradeoff | -|---|---| -| **A. Refuse always** | No escape hatch; missing affected-set evidence is `RefusedUndeclaredDelta` | -| **B. Typed override row** | `AuthorExplicit { author, rationale, surfaces }` admissible with counted audit + RED control | -| **C. FrontierAccepted only** | Override allowed only for enrolled scaffold rows, not production surfaces | +### Q2 — Behavioral vs digest -**Recommendation:** **B** with a construction wall — override row required, never a silent -flag; aligns with §5 "no escape hatches" while allowing genuine operator migrations. +**Decision:** Stricter **C**. -### Q2 — Behavioral vs digest on self-host +- Digest equality proves **byte agreement only**. +- Formatting-only byte-contract artifacts may close on digest alone. +- `SelfEmitted`, executable stage0, and semantic compiler surfaces require **build and + behavioral evidence** appropriate to the declared requirement revision. +- A digest must **never** be relabelled `BehavioralEquivalent`. -For `SelfEmitted` / `Stage0SeedFile` surfaces, is digest match alone ever sufficient? +### Q3 — Scaffold auto-widening -| Option | Tradeoff | -|---|---| -| **A. Digest only** | Faster CI; risks §5 validation-standing-where-construction-was-available | -| **B. Behavioral always** | Matches `BehavioralEquivalent` qualification; costs wet witness | -| **C. Tiered by surface** | Digest for formatting-only paths; behavioral when probe enrolled | +**Decision:** **B only**. -**Recommendation:** **C** — join to existing `EmissionQualification` and behavioral transport -roster; refuse `AdmittedDeclared` without `BehavioralEquivalent` when a probe is enrolled. +- No scaffold class automatically widens production emission authority. +- Each admitted move needs an **exact row** or a derived exact surface identity. +- A class-level template may generate **Candidate** rows; it cannot confer promotion authority. -### Q3 — Frontier scaffold rows +### Q4 — Unknown surfaces -Do `SeedRetained` / `UnpinnedFrontier` rows auto-widen `allowed_surface_moves`? +**Decision:** **A** at authoritative admission. -| Option | Tradeoff | -|---|---| -| **A. Auto-widen by row class** | Less author burden; risks over-admission | -| **B. Per-row `DeclaredChangeTemplate`** | Explicit; scales with roster size | -| **C. Census-derived default** | `witness_entry_eligibility_census` projects templates once | +- `SurfaceUnknown` **refuses** promotion and warm reuse. +- It may remain `FrontierAccepted` only in a clearly non-authoritative candidate or audit + state; it cannot merge into the promoted generation as merely counted debt. -**Recommendation:** **B** for production surfaces, **A** only for counted scaffold classes -with dissolution triggers named on the row. +### Q5 — BothWays quotient delta -### Q4 — Registry completeness +**Decision:** **B**. -Is `SurfaceUnknown` hard refusal or `FrontierAccepted` on the merge path? +- A lawful semantic representation change is **not** automatically an admitted emission + change; the target artifact surface still needs an **explicit declared entry**. +- BothWays answers whether the representation change is lawful; emission admission answers + whether this artifact was allowed to move in this generation. Neither entails the other. -| Option | Tradeoff | -|---|---| -| **A. Hard refusal** | Fail-closed; blocks merge until registry complete | -| **B. FrontierAccepted** | Merge proceeds with counted debt; risks silent widen | -| **C. Refuse on warm path only** | Cold merge ok with debt; warm replay refuses | - -**Recommendation:** **C** — matches §4b `FrontierAccepted` semantics and warm-merge's -higher trust bar. - -### Q5 — Relationship to BothWays quotient delta +--- -Do lawful BothWays edits always become `AdmittedDeclared` when the six lens laws pass? +## 10. Staged implementation plan (future PRs — not this document) -| Option | Tradeoff | -|---|---| -| **A. Always admitted** | Semantic lawful ⇒ admission; digest move expected | -| **B. Requires explicit surface entry** | BothWays green ≠ emission admission without declaration | -| **C. Join only for `SelfEmitted` surfaces** | Seed-emitted paths use quotient delta; generated-artifact paths use registry | - -**Recommendation:** **C** — [module-identity-storage-binding-design.md](module-identity-storage-binding-design.md) -comparison is semantic (R1 vs R2); emission admission still needs surface declaration for -registry-governed artifacts. +| Stage | Deliverable | Dissolution trigger | +|---|---|---| +| **0 — this document** | Sign-ready design + doc graph bind | Operator sign-off (this revision) | +| **1 — vocabulary carrier** | Revision, transition, candidate/promoted, anchor types + pure folds + synthetic witnesses | Interim bytes-only merge branches | +| **2 — declared-change derivation** | `declared_transition_from_pr_facts` wired to affected-set; refuse when selection fails | Hand-classified PR intent in review prose | +| **3 — regen phase stamp** | `regen_stage0` emits `StageStamp` + deltas + candidate state; regen refuses undeclared | `regen_divergence_count` prose-oracle | +| **4 — lineage + anchor** | `GenerationNode` / `RecoveryAnchor` / `ConsumerRead` on regen + warm paths | Implicit generation in producer tag only | +| **5 — successor capability** | `SuccessorCapabilityReceipt` + fix-forward witness; promotion gate | Reproduction-only promotion | +| **6 — bridge protocol** | expand-migrate-contract for incompatible revision jumps | Big-bang incompatible promotion | +| **7 — warm-merge join** | #7522 admits `Promoted` receipts only with matching transition + lineage | Cold rerun as default merge path | +| **8 — phased single process** | Shared substrate; prelude duplication witness | Per-phase `process_shared_index` duplication | +| **9 — guarantee path rows** | emission-path measurement carries admission projection | Emission path unmeasured for intent | --- -## 10. Sign-off bar +## 11. Sign-off bar -This design is ready for operator review when: +This design is ready for operator merge when: -1. All symbols in §2 resolve on main (grep-verified at PR open). -2. The two new pieces (§3) are explicit and not conflated with existing gates. -3. Open questions (§9) have options + recommendations recorded in the PR body. -4. Doc graph bind lands (`gunbc.doc_graph_roots` → this slug). -5. No implementation code ships in the design PR. +1. All symbols in §2 resolve on main (grep-verified). +2. §3 obligations are explicit — not reduced to declared-change + lineage alone. +3. §4 states fix-forward (not rollback-only) de-risk. +4. §5 temporal graph is explicit. +5. §9 operator decisions are recorded (not open questions). +6. Doc graph bind lands (`gunbc.doc_graph_roots` → this slug). +7. No implementation code ships in the design PR. +8. Candidate generation through the direct Rust door remains unblocked; only promotion + and authoritative admission are gated.