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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions dag/gunbc/merge_admission.dag
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,23 @@ fn classify_merge_admission_verdict_v2(
}
}

fn merge_admission_verdict_label(v: MergeAdmissionVerdict) -> String {
match v {
MergeAdmitted => "admitted"
MergeDeniedStaleBase => "stale base tree — re-run CI against current main"
MergeDeniedStaleRoster => "stale gate roster — re-run CI after criteria change"
MergeDeniedNotSuccess => "receipt conclusion is not success"
MergeDeniedWrongAttempt => "receipt belongs to a different walk attempt — not the subject; re-run CI"
MergeDeniedSubjectMismatch => "receipt describes a different head or base tree than this attempt captured — not the subject; re-run CI"
}
}

data merge_admission_current_context_staging_note: String = "STAGED INVOCABLE ENTRY, NOT A LIVE CONSUMER (review 46238, incident 2026-08-01). tools.merge_admission_current_context can fetch the current merge target, read attempt-scoped TestedSubject and MergeAdmissionReceiptV2 wires, and classify them against current_gate_roster_hash, but no production path invokes the entry and no production path mints either V2 wire. It therefore closes no runtime admission gap today. What IS delivered and executing is the discovery-corpus control pair: classify_merge_admission_verdict_v2 refuses a receipt carrying a synthetic changed-roster hash against the current roster, and admits a receipt minted under the current roster. A correctly computed but uninvoked verdict is staging, not a warning and not a wall."

data merge_admission_v2_production_wires_dissolution_trigger: String = "FIRST DISSOLVE-ON: production captures TestedSubject before the floor, mints MergeAdmissionReceiptV2 after the floor, and persists both through merge_admission_tested_subject_path and merge_admission_floor_receipt_path for the same WalkAttemptId. Until both producers execute, tools.merge_admission_current_context has no production subject or evidence to consume and cannot be called live."

data merge_admission_required_check_binding_dissolution_trigger: String = "SECOND DISSOLVE-ON, after merge_admission_v2_production_wires_dissolution_trigger: gunbc.ci_failure_class.merge_freshness_gating_status changes from GatingComputedDeferred to the existing GatingEnforced arm after operator sign-off and the ci_failure_class prerequisites named by gunbc.plans.merge_admission_gate_shape_proposal. That transition binds the V2 current-context verdict into the approved merge-time consumption path so a denial refuses rather than merely being invocable. The later policy-content increment is already declared by gate_content_hash_variant_tag_scaffold: replacing gate_content_hash's variant-tag placeholder fires that SingleAuthority dissolution; it is not part of this staged increment."

fn merge_admission_verdict_would_block(v: MergeAdmissionVerdict) -> Bool {
match v {
MergeAdmitted => false
Expand Down
82 changes: 82 additions & 0 deletions dag/test/claim/merge_admission_attempt_witness_test.dag
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import gunbc.merge_admission {
MergeDeniedSubjectMismatch,
classify_merge_admission_verdict_v2,
merge_admission_verdict_would_block,
gate_roster_content_hash,
current_gate_roster_hash,
}
import gunbc.commit_workflow {
commit_gate_roster,
}
import gunbc.merge_admission_produce {
merge_admission_receipt_schema_v2,
Expand Down Expand Up @@ -96,6 +101,83 @@ test fn matching_attempt_and_subject_with_fresh_base_admits() -> Bool {
) { MergeAdmitted => true _ => false }
}

fn synthetic_changed_roster_hash() -> ContentHash {
gate_roster_content_hash(roster: reverse(commit_gate_roster))
}

data roster_staleness_control_recut_note: String = "RECUT AFTER #7591 INVALIDATED #7583. The 2026-08-01 incident motivates the roster-identity law, but this control does not reconstruct that incident by execution. Its alternative revision reverses the exact live enrollments: identical members and surfaces, still unique and otherwise valid, but with a different order-sensitive roster hash. Order participates in roster identity even though it does not affect roster validity, so a source-only reordering is a real roster revision that refuses every in-flight admission minted under the prior order. It names no particular production gate and asserts no historical roster content. The prior control named PublicationPlacementGate; that coupled the regression control to one volatile roster member's continued existence, and the member's operator-ordered deletion invalidated the entire PR from a direction its original evidence did not watch. A control for a roster-identity law must exercise the SHAPE of a roster change, never a particular member, or its lifetime is bounded by the least stable thing it mentions. A description of a control's shape must live with that control or derive from it, so an implementation change cannot leave a remote carrier describing evidence that no longer executes. The discriminator proves staleness rather than malformedness: the alternative-revision receipt refuses against the live revision and the identical receipt admits when that alternative revision is supplied as current. The live-revision receipt also admits, so an always-admit or always-refuse implementation fails a control."

fn synthetic_changed_roster_receipt() -> MergeAdmissionReceiptV2 {
MergeAdmissionReceiptV2 {
attempt_id: attempt_a(),
pr_number: Present { value: 7544 },
tested_head_sha: head_sha_fx,
tested_base_tree: base_tree_fx(),
gate_roster_hash: synthetic_changed_roster_hash(),
conclusion: Success,
}
}

test fn synthetic_changed_roster_receipt_refuses_against_live_roster() -> Bool {
(synthetic_changed_roster_hash() != current_gate_roster_hash)
&& match classify_merge_admission_verdict_v2(
current_attempt_id: attempt_a(),
tested_subject: subject_fx(attempt: attempt_a(), tree: base_tree_fx(), head: head_sha_fx),
receipt: synthetic_changed_roster_receipt(),
merge_target_tree: base_tree_fx(),
current_roster_hash: current_gate_roster_hash,
) {
MergeDeniedStaleRoster => true
MergeAdmitted => false
MergeDeniedStaleBase => false
MergeDeniedNotSuccess => false
MergeDeniedWrongAttempt => false
MergeDeniedSubjectMismatch => false
}
}

test fn synthetic_changed_roster_receipt_admits_when_revision_is_current() -> Bool {
match classify_merge_admission_verdict_v2(
current_attempt_id: attempt_a(),
tested_subject: subject_fx(attempt: attempt_a(), tree: base_tree_fx(), head: head_sha_fx),
receipt: synthetic_changed_roster_receipt(),
merge_target_tree: base_tree_fx(),
current_roster_hash: synthetic_changed_roster_hash(),
) {
MergeAdmitted => true
MergeDeniedStaleBase => false
MergeDeniedStaleRoster => false
MergeDeniedNotSuccess => false
MergeDeniedWrongAttempt => false
MergeDeniedSubjectMismatch => false
}
}

test fn current_roster_receipt_admits_against_live_roster() -> Bool {
let receipt = MergeAdmissionReceiptV2 {
attempt_id: attempt_a(),
pr_number: Present { value: 7544 },
tested_head_sha: head_sha_fx,
tested_base_tree: base_tree_fx(),
gate_roster_hash: current_gate_roster_hash,
conclusion: Success,
}
match classify_merge_admission_verdict_v2(
current_attempt_id: attempt_a(),
tested_subject: subject_fx(attempt: attempt_a(), tree: base_tree_fx(), head: head_sha_fx),
receipt: receipt,
merge_target_tree: base_tree_fx(),
current_roster_hash: current_gate_roster_hash,
) {
MergeAdmitted => true
MergeDeniedStaleBase => false
MergeDeniedStaleRoster => false
MergeDeniedNotSuccess => false
MergeDeniedWrongAttempt => false
MergeDeniedSubjectMismatch => false
}
}

test fn foreign_attempt_refuses_as_wrong_attempt() -> Bool {
let v = classify_merge_admission_verdict_v2(
current_attempt_id: attempt_a(),
Expand Down
138 changes: 138 additions & 0 deletions dag/tools/merge_admission_current_context.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
module tools.merge_admission_current_context

import std.process { ProcessExit, ExitSuccess, exit_failure }
import std.types { NonEmptyStr, GitRef }
import extdeps.git
import extdeps.git.inspect
import extdeps.git.object_store {
GitObjectIdTextDecoded,
GitObjectIdTextRefused,
git_decode_object_id_text,
git_object_id_format,
}
import extdeps.filesystem.filesystem_io { Filesystem }
import extdeps.shell
import gunbc.merge_admission {
WalkAttemptId,
MergeAdmissionVerdict,
MergeAdmitted,
MergeDeniedStaleBase,
MergeDeniedStaleRoster,
MergeDeniedNotSuccess,
MergeDeniedWrongAttempt,
MergeDeniedSubjectMismatch,
classify_merge_admission_verdict_v2,
current_gate_roster_hash,
merge_admission_verdict_label,
}
import gunbc.merge_admission_produce {
compose_walk_attempt_id,
compose_explicit_walk_attempt_id,
merge_admission_tested_subject_path,
merge_admission_floor_receipt_path,
parse_tested_subject_wire,
parse_receipt_wire_v2,
merge_admission_merge_base_ref,
merge_admission_gate_fetch_parts,
shell_stdout_line,
}

fn optional_env(name: NonEmptyStr) -> String? {
let observed = shell.Env.Get(name: name)
match observed.value {
Present { value: raw } =>
let value = trim(raw)
if value == "" { none } else { Present { value: value } }
Absent => none
}
}

fn current_walk_attempt_id() -> WalkAttemptId? {
match optional_env(name: "GUNBC_WALK_ATTEMPT_ID") {
Present { value: explicit } => compose_explicit_walk_attempt_id(explicit: explicit)
Absent =>
compose_walk_attempt_id(
run_id: match optional_env(name: "GITHUB_RUN_ID") {
Present { value: value } => value
Absent => ""
},
run_attempt: match optional_env(name: "GITHUB_RUN_ATTEMPT") {
Present { value: value } => value
Absent => ""
},
job: match optional_env(name: "GITHUB_JOB") {
Present { value: value } => value
Absent => ""
},
)
}
}

fn denied_exit(verdict: MergeAdmissionVerdict) -> ProcessExit {
exit_failure(
reason: concat(
"merge_admission_current_context: blocked (",
concat(merge_admission_verdict_label(v: verdict), ")")
)
)
}

func main() -> ProcessExit {
let attempt = match current_walk_attempt_id() {
Present { value: value } => value
Absent => return exit_failure(reason: "merge_admission_current_context: walk attempt identity absent or invalid")
}
let root = git.Inspect.Toplevel().path as String
let subject_read = Filesystem.Read(
path: merge_admission_tested_subject_path(root: root, attempt_id: attempt)
)
if !subject_read.success {
return exit_failure(reason: "merge_admission_current_context: tested-subject wire missing")
}
let subject = match parse_tested_subject_wire(text: subject_read.content) {
Present { value: value } => value
Absent => return exit_failure(reason: "merge_admission_current_context: tested-subject wire invalid")
}
let receipt_read = Filesystem.Read(
path: merge_admission_floor_receipt_path(root: root, attempt_id: attempt)
)
if !receipt_read.success {
return exit_failure(reason: "merge_admission_current_context: floor-receipt wire missing")
}
let receipt = match parse_receipt_wire_v2(text: receipt_read.content) {
Present { value: value } => value
Absent => return exit_failure(reason: "merge_admission_current_context: floor-receipt wire invalid")
}
let fetch_parts = merge_admission_gate_fetch_parts()
let fetched = git.Core.FetchNoTags(remote: fetch_parts.remote, ref: fetch_parts.ref_name)
if !fetched.success {
return exit_failure(reason: "merge_admission_current_context: current merge target fetch refused")
}
let observed = git.Inspect.ShowTree(ref: merge_admission_merge_base_ref as GitRef)
if !observed.success {
return exit_failure(reason: "merge_admission_current_context: current merge target tree observation refused")
}
let target = match git_decode_object_id_text(
format: git_object_id_format(oid: receipt.tested_base_tree),
text: shell_stdout_line(raw: observed.tree),
) {
GitObjectIdTextDecoded { oid: oid } => oid
GitObjectIdTextRefused { cause: _ } =>
return exit_failure(reason: "merge_admission_current_context: current merge target object id refused")
}
let verdict = classify_merge_admission_verdict_v2(
current_attempt_id: attempt,
tested_subject: subject,
receipt: receipt,
merge_target_tree: target,
current_roster_hash: current_gate_roster_hash,
)
match verdict {
MergeAdmitted => ExitSuccess
MergeDeniedStaleBase => denied_exit(verdict: verdict)
MergeDeniedStaleRoster => denied_exit(verdict: verdict)
MergeDeniedNotSuccess => denied_exit(verdict: verdict)
MergeDeniedWrongAttempt => denied_exit(verdict: verdict)
MergeDeniedSubjectMismatch => denied_exit(verdict: verdict)
}
}
14 changes: 2 additions & 12 deletions dag/tools/merge_admission_gate.dag
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import extdeps.filesystem.filesystem_io { Filesystem }
import gunbc.merge_admission {
current_gate_roster_hash,
classify_merge_admission_verdict,
merge_admission_verdict_label,
MergeAdmissionVerdict,
MergeDeniedStaleBase,
MergeDeniedStaleRoster,
Expand All @@ -27,17 +28,6 @@ import gunbc.merge_admission_produce {
}
import gunbc.ci_failure_class { merge_admission_blocks_merge }

fn verdict_reason(v: MergeAdmissionVerdict) -> String {
match v {
MergeAdmitted => "admitted"
MergeDeniedStaleBase => "stale base tree — re-run CI against current main"
MergeDeniedStaleRoster => "stale gate roster — re-run CI after criteria change"
MergeDeniedNotSuccess => "receipt conclusion is not success"
MergeDeniedWrongAttempt => "receipt belongs to a different walk attempt — not the subject; re-run CI"
MergeDeniedSubjectMismatch => "receipt describes a different head or base tree than this attempt captured — not the subject; re-run CI"
}
}

func main() -> ProcessExit {
let root = git.Inspect.Toplevel().path as String
let path = merge_admission_receipt_path(root: root)
Expand Down Expand Up @@ -77,7 +67,7 @@ func main() -> ProcessExit {
exit_failure(
reason: concat(
"merge_admission_gate: blocked (",
concat(verdict_reason(v: verdict), ")")
concat(merge_admission_verdict_label(v: verdict), ")")
)
)
} else {
Expand Down
Loading