1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//! The speculator pass (bl-d0c2, design
//! docs/design/bl-24e7-speculative-merge-queue.md): one idempotent sweep of
//! the merge queue that pre-pays gates so closes land as cache hits.
//!
//! One pass, in order: SWEEP unsealed queue entries (their branch moved — a
//! landed close deleted it, or a fix is coming and will re-tag; either way the
//! stale tag holds no position), then walk the SEALED entries head-first, then
//! ADOPT every quiet `work/<id>` tip not already sealed
//! ([`crate::speculate_queue::adopt`], bl-b761) — the paved path by which an
//! agent that never learns the queue exists still rides it. The walk chains
//! candidates with [`crate::speculate_candidate`], consulting the
//! verdict cache before ever building. Strict head-first order is the whole
//! scheduling theory: a candidate is only built when every shallower prefix
//! already holds a PASS, so the depth-risk the design worried about — building
//! on a predecessor that then evicts — is zero at build time, evictions being
//! gate failures and gate failures being known. What remains of the eagerness
//! knob is `builds` (how many gates one pass may spend — the caller's
//! watts-vs-wall-time declaration) and the chain stops: a CONFLICT or a FAIL
//! verdict ends the buildable prefix (deeper candidates contain the same
//! problem), and the queue's end ends the pass.
//!
//! The gate has THREE answers, not two (bl-1643): exit `0` is a PASS, exit
//! [`NO_VERDICT`] (75, BSD `EX_TEMPFAIL`) is "no verdict" — the gate died for
//! a reason that is not a judgement of the tree (a runner signaling tarpaulin
//! mid-suite, an OOM kill the gate classified), so NOTHING is recorded, the
//! candidate stays unbuilt and the pass ends there for the next pass to
//! rebuild — and every other exit is a FAIL. The distinction matters because a
//! recorded FAIL is permanent: the walk stops at it on every later pass
//! without rebuilding, so an infrastructure death stored as FAIL would be a
//! permanently false-negative tree. Classifying the death is the GATE's job
//! (it knows which of its stages died how); the speculator only honors the
//! reserved code.
//!
//! The gate subprocess runs under `nice` in a detached build worktree,
//! removed before the pass reports — a close-time gate on cache miss runs
//! unniced by construction, so the real merge path always preempts. On a pass
//! the gate's own hook records the verdict; [`run`] records it again
//! (idempotent, same key) so a stub gate under test and the real gate behave
//! identically, and records FAIL itself, which the aborting hook never can.
use std::io;
use std::path::Path;
use std::process::Command;
use crate::speculate;
use crate::speculate_candidate::{self, Merge};
use crate::speculate_queue;
/// What one pass did, one line per event — the plain-text contract agents and
/// tests read back.
pub type Report = Vec<String>;
/// One speculator pass over the queue at `repo`. `onto` names the landing
/// branch the chain grows from; `gate` is the command run inside each
/// candidate (the real caller passes `scripts/pre-commit`); `builds` caps how
/// many gates this pass may spend.
pub fn run(
repo: &Path,
scratch: &Path,
territory: &Path,
toolchain: &str,
onto: &str,
gate: &str,
builds: usize,
) -> io::Result<Report> {
let mut report = Vec::new();
let mut base = base_commit(repo, onto)?;
// One fingerprint for the whole pass: the toolchain is the same for every
// candidate, and a candidate that edits the gate scripts is simply a
// different tree — the key the closer's hook will look up too.
let gate_fp = speculate::gate_fingerprint(repo, scratch, toolchain)?;
let mut spent = 0;
for entry in speculate_queue::queue(repo)? {
if !entry.sealed {
speculate_queue::dequeue(repo, &entry.id)?;
report.push(format!("swept {} (unsealed)", entry.id));
continue;
}
let tree = match speculate_candidate::merge_tree(repo, &base, &entry.tip)? {
Merge::Tree(tree) => tree,
Merge::Conflict => {
report.push(format!("conflict {} — fold-at-close, chain stops", entry.id));
break;
}
};
let candidate = speculate_candidate::commit_tree(repo, &tree, &[&base, &entry.tip])?;
match speculate::read(territory, &tree, &gate_fp)? {
Some(v) if v.pass => report.push(format!("hit {} {tree}", entry.id)),
Some(_) => {
report.push(format!("fail {} {tree} — chain stops", entry.id));
break;
}
None if spent >= builds => {
report.push(format!("deferred {} (builds spent)", entry.id));
break;
}
None => {
spent += 1;
let Some(pass) = build(repo, scratch, &candidate, gate)? else {
report.push(format!(
"no verdict {} {tree} (gate exit {NO_VERDICT}) — unbuilt, pass ends",
entry.id
));
break;
};
let verdict = speculate::Verdict { pass, builder: "bl-speculate".to_string() };
speculate::write(territory, &tree, &gate_fp, &verdict)?;
if pass {
report.push(format!("built {} {tree} pass", entry.id));
} else {
report.push(format!("built {} {tree} FAIL — chain stops", entry.id));
break;
}
}
}
base = candidate;
}
// ADOPT, last (bl-b761): seal every quiet `work/<id>` tip not already
// sealed, so agents that only ever commit and close still ride the queue.
// Pass-END placement is the debounce — a fresh seal must survive one full
// inter-pass interval before the walk above will build it, so quiescence
// is measured in passes and no clock (least of all a smeared commit
// date) is consulted. Sweep + adopt across one pass IS requeue-at-bottom
// for a moved tip.
for (id, tip) in speculate_queue::adopt(repo, None)? {
report.push(format!("adopted {id} {tip}"));
}
Ok(report)
}
/// The tip of the landing branch — where the candidate chain is rooted.
fn base_commit(repo: &Path, onto: &str) -> io::Result<String> {
let mut cmd = crate::safegit::at(repo);
cmd.args(["rev-parse", "--verify"]).arg(format!("refs/heads/{onto}"));
let out = cmd.output()?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
Err(io::Error::other(format!(
"git rev-parse {onto}: {}",
String::from_utf8_lossy(&out.stderr).trim()
)))
}
}
/// The gate exit code reserved as "no verdict" — BSD `EX_TEMPFAIL`, the
/// sysexits code for "try again later". A gate exits it when its run died of
/// something that is not a judgement of the tree.
pub const NO_VERDICT: i32 = 75;
/// Materialize the candidate, run the gate under `nice` inside it, tear the
/// worktree down whatever the outcome. Only the gate's exit code speaks:
/// `Some(true)` on 0, `None` on [`NO_VERDICT`], `Some(false)` on anything
/// else — a signal death included, since the gate had no chance to classify
/// it and every non-verdict it CAN name is the reserved code.
fn build(repo: &Path, scratch: &Path, candidate: &str, gate: &str) -> io::Result<Option<bool>> {
std::fs::create_dir_all(scratch)?;
let dir = scratch.join(format!("build-{candidate}"));
speculate_candidate::build_dir(repo, candidate, &dir)?;
let status = Command::new("nice").arg("-n19").arg(gate).current_dir(&dir).status();
speculate_candidate::remove_build_dir(repo, &dir)?;
let status = status?;
Ok((status.code() != Some(NO_VERDICT)).then(|| status.success()))
}
#[cfg(test)]
#[path = "speculate_run_tests.rs"]
mod speculate_run_tests;