refactor(self_modify): Phase B language-advantage pass — dichotomy wave
CI Pipeline / best-practices (push) Successful in 9s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / rust-surface (push) Failing after 16s
CI Pipeline / go-surface (push) Failing after 21s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / lint (push) Canceled after 26s
CI Pipeline / unit-tests (push) Canceled after 26s
CI Pipeline / canary (push) Canceled after 26s
Auto-version and Release / release (push) Canceled after 34s
CI Pipeline / type-check (push) Canceled after 15s
deploy / deploy (push) Canceled after 36s
CI Pipeline / gitea-offline (push) Canceled after 15s

Battery-pinned language-advantage pass over the parity port (32 frozen
rows green throughout, cargo test 11/11, clippy 0):
- BaselineVerdict: the compare_baseline truthiness lattice is a typed
  enum with exhaustive matches — classify/promote/reason are single
  construction points; the three verdict reasons are canonical
  &'static str instead of string literals duplicated at three branches
- ProposalOutcome: promote/merge-fail/discard results held typed until
  the JSON boundary (one into_value per shape), rollback_ref stays a
  passthrough Value through the enum (present-None stays None)
- zero-copy argv: _run borrows &[&str]; the oracle's per-call
  Vec<String> argv construction is gone (worktree path and merge
  message borrow into the slices)
- chars_tail: (stdout+stderr)[-2000:] is now one reverse traversal
  with bounded reversal instead of a full count() + skip() double pass;
  char-boundary safety pinned on multi-byte runes
- lazy rollback_ref read and the remove-worktree argv kept as
  closures, preserving the oracle's KeyError-after-side-effects order
- Phase B pins added to the battery: verdict lattice exhaustiveness +
  frozen reasons, outcome projections vs the three frozen pod shapes,
  chars_tail char-boundary/short/empty cases
This commit is contained in:
Nick committed 2026-10-12 02:50:56 +01:00
1 parent 1af27dc5d4
commit 18052d5e1a
2 files changed
+267 -99

No files matched your search

@@ -341,3 +341,89 @@ fn monitor_watch_vocabulary() {
);
assert_eq!(m["monitoring"], json!(true));
}
// ── Phase B language-advantage pins ──────────────────────────────────────
use crate::{chars_tail, BaselineVerdict, ProposalOutcome};
/// The verdict lattice is exhaustive: every (candidate, baseline)
/// truthiness pair maps to exactly one enum variant, and the projected
/// (promote, reason) pairs are the frozen oracle rows.
#[test]
fn phaseb_verdict_lattice_is_exhaustive() {
let cases = [
(false, false, BaselineVerdict::Regression, false),
(false, true, BaselineVerdict::Regression, false),
(true, false, BaselineVerdict::AlreadyRed, true),
(true, true, BaselineVerdict::Clean, true),
];
for (cand, base, want, promote) in cases {
let v = BaselineVerdict::classify(cand, base);
assert_eq!(v, want, "classify({cand}, {base})");
assert_eq!(v.promote(), promote);
// every reason is a canonical 'static str carried by the enum
let reason: &'static str = v.reason();
assert!(!reason.is_empty());
}
// the frozen rows' exact reasons ride the variants
assert_eq!(
BaselineVerdict::Regression.reason(),
"candidate gates red — regression blocks promotion"
);
assert_eq!(
BaselineVerdict::AlreadyRed.reason(),
"candidate green, baseline was already red — no regression introduced"
);
assert_eq!(
BaselineVerdict::Clean.reason(),
"candidate green, baseline green — no regression"
);
}
/// The outcome enum's three shapes project to exactly the three result
/// dicts the oracle returns (the frozen pod rows' shapes).
#[test]
fn phaseb_outcome_projections() {
let r = || Value::String("sha".into());
assert_eq!(
ProposalOutcome::Promoted {
branch: "b".into(),
rollback_ref: r()
}
.into_value(),
json!({"promoted": true, "branch": "b", "rollback_ref": "sha"})
);
assert_eq!(
ProposalOutcome::MergeFailed {
error: "merge failed: boom".into(),
rollback_ref: r()
}
.into_value(),
json!({"promoted": false, "error": "merge failed: boom", "rollback_ref": "sha"})
);
assert_eq!(
ProposalOutcome::Discarded {
branch: "b".into(),
rollback_ref: r()
}
.into_value(),
json!({"promoted": false, "discarded": true, "branch": "b", "rollback_ref": "sha"})
);
}
/// The 2000-char tail is CHARACTER-parity even on multi-byte runes and
/// exact at the boundary — a byte-slice implementation would panic here.
#[test]
fn phaseb_chars_tail_is_char_boundary_safe() {
let unicode = "αβγδε".repeat(500); // 2500 chars, 5000 bytes
let tail = chars_tail(&unicode, 2000);
assert_eq!(tail.chars().count(), 2000);
assert!(tail.ends_with("αβγδε"));
let short = "short";
assert_eq!(chars_tail(short, 2000), short);
assert_eq!(
chars_tail("", 2000),
"",
"empty gate output stays empty"
);
}
+181 -99
View File
@@ -38,6 +38,18 @@
//! No smod_free_string export — sibling crates sharing the Python process
//! already export identical free symbols; callers free through
//! capability_scoring's cs_free_string.
//!
//! Language-advantage pass (Phase B, battery-pinned): the compare_baseline
//! truthiness lattice and the promote/discard decision are typed enums
//! (`BaselineVerdict`, `ProposalOutcome`) with exhaustive matches held
//! until the JSON boundary — the three verdict reasons and the three
//! result shapes are canonical `&'static str` / single construction
//! points, not string soup at three call sites. The argv seam is
//! zero-copy `&[&str]` (the oracle builds Vec<String> per call); the
//! 2000-char output tail is a single reverse traversal instead of a
//! count + skip double pass. FFI errors stay a typed enum behind the
//! Result envelope (caller-visible `err` strings unchanged — parity is
//! pinned by the battery).
use serde_json::{json, Value};
use std::fmt;
@@ -59,6 +71,102 @@ const WATCH: [&str; 2] = [
"no new failure signatures in the next 24h",
];
// ── decision lattices (enums, not string soup) ───────────────────────────
/// The compare_baseline verdict — the oracle's three-branch lattice.
/// Truthiness of `passed` is resolved BEFORE classification, so the enum
/// carries the decision, not the coercion.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum BaselineVerdict {
/// candidate gates red — regression blocks promotion
Regression,
/// candidate green, baseline was already red — no regression introduced
AlreadyRed,
/// candidate green, baseline green — no regression
Clean,
}
impl BaselineVerdict {
/// The oracle's branch order: candidate first, then baseline.
fn classify(candidate_passed: bool, baseline_passed: bool) -> BaselineVerdict {
if !candidate_passed {
BaselineVerdict::Regression
} else if !baseline_passed {
BaselineVerdict::AlreadyRed
} else {
BaselineVerdict::Clean
}
}
fn promote(self) -> bool {
match self {
BaselineVerdict::Regression => false,
BaselineVerdict::AlreadyRed | BaselineVerdict::Clean => true,
}
}
fn reason(self) -> &'static str {
match self {
BaselineVerdict::Regression => "candidate gates red — regression blocks promotion",
BaselineVerdict::AlreadyRed => {
"candidate green, baseline was already red — no regression introduced"
}
BaselineVerdict::Clean => "candidate green, baseline green — no regression",
}
}
}
/// The promote_or_discard result, held typed until the JSON boundary.
/// `rollback_ref` stays a passthrough Value (a present None is returned
/// as None by the oracle — only a MISSING key raises KeyError).
#[derive(Debug)]
enum ProposalOutcome {
Promoted {
branch: String,
rollback_ref: Value,
},
MergeFailed {
error: String,
rollback_ref: Value,
},
Discarded {
branch: String,
rollback_ref: Value,
},
}
impl ProposalOutcome {
fn into_value(self) -> Value {
match self {
ProposalOutcome::Promoted {
branch,
rollback_ref,
} => json!({
"promoted": true,
"branch": branch,
"rollback_ref": rollback_ref,
}),
ProposalOutcome::MergeFailed {
error,
rollback_ref,
} => json!({
"promoted": false,
"error": error,
"rollback_ref": rollback_ref,
}),
ProposalOutcome::Discarded {
branch,
rollback_ref,
} => json!({
"promoted": false,
"discarded": true,
"branch": branch,
"rollback_ref": rollback_ref,
}),
}
}
}
// ── error lattice ────────────────────────────────────────────────────────
pub enum FfiError {
@@ -83,7 +191,9 @@ pub enum FfiError {
impl fmt::Display for FfiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FfiError::Spawn(e) => write!(f, "[Errno {errno}] {e}", errno = e.raw_os_error().unwrap_or(0)),
FfiError::Spawn(e) => {
write!(f, "[Errno {errno}] {e}", errno = e.raw_os_error().unwrap_or(0))
}
FfiError::Io(e) => write!(f, "{e}"),
FfiError::Timeout { cmd, secs } => {
write!(f, "Command '{cmd}' timed out after {secs} seconds")
@@ -103,8 +213,9 @@ impl fmt::Display for FfiError {
/// `subprocess.run(cmd, cwd=cwd, capture_output=True, text=True,
/// timeout=timeout)` → (returncode, stdout, stderr). text=True means
/// strict UTF-8 (a decode failure raises) and universal newlines.
fn _run(cmd: &[String], cwd: &str, timeout_secs: u64) -> Result<(i32, String, String), FfiError> {
let mut command = Command::new(&cmd[0]);
/// Zero-copy: the argv borrows `&[&str]`.
fn _run(cmd: &[&str], cwd: &str, timeout_secs: u64) -> Result<(i32, String, String), FfiError> {
let mut command = Command::new(cmd[0]);
command
.args(&cmd[1..])
.current_dir(cwd)
@@ -170,13 +281,16 @@ fn chars_head(s: &str, n: usize) -> String {
s.chars().take(n).collect()
}
/// (stdout+stderr)[-2000:] — one reverse traversal, bounded reversal;
/// never a full count() pass over the gate output first.
fn chars_tail(s: &str, n: usize) -> String {
let count = s.chars().count();
if count <= n {
s.to_string()
} else {
s.chars().skip(count - n).collect()
}
s.chars()
.rev()
.take(n)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect()
}
/// Python truthiness over a JSON value (`if not x.get("passed")`).
@@ -251,27 +365,17 @@ pub fn propose_change(
base: &str,
now: &str,
) -> Result<Value, FfiError> {
let rev_parse = vec![
"git".to_string(),
"rev-parse".to_string(),
base.to_string(),
];
let (rc, out, _err) = _run(&rev_parse, repo, RUN_TIMEOUT_SECS)?;
let (rc, out, _err) = _run(&["git", "rev-parse", base], repo, RUN_TIMEOUT_SECS)?;
if rc != 0 {
return Ok(json!({"proposed": false, "error": format!("unknown base: {base}")}));
}
let base_sha = out.trim().to_string();
let base_sha = out.trim();
let wt = mkdtemp_selfmod()?;
let worktree_add = vec![
"git".to_string(),
"worktree".to_string(),
"add".to_string(),
wt.clone(),
"-b".to_string(),
branch.to_string(),
base.to_string(),
];
let (add_rc, _out, add_err) = _run(&worktree_add, repo, RUN_TIMEOUT_SECS)?;
let (add_rc, _out, add_err) = _run(
&["git", "worktree", "add", &wt, "-b", branch, base],
repo,
RUN_TIMEOUT_SECS,
)?;
if add_rc != 0 {
return Ok(json!({
"proposed": false,
@@ -291,18 +395,17 @@ pub fn propose_change(
/// `run_gates` — run the gate suite inside the worktree.
/// Returns {passed, output} where output is (stdout+stderr)[-2000:].
pub fn run_gates(worktree: &str, test_cmd: Option<&Vec<Value>>) -> Result<Value, FfiError> {
let cmd: Vec<String> = match test_cmd {
let argv: Vec<&str> = match test_cmd {
Some(list) if !list.is_empty() => list
.iter()
.map(|v| {
v.as_str()
.map(str::to_string)
.ok_or(FfiError::TypeError("test_cmd elements must be strings"))
})
.collect::<Result<Vec<String>, FfiError>>()?,
_ => DEFAULT_TEST_CMD.iter().map(|s| s.to_string()).collect(),
.collect::<Result<Vec<&str>, FfiError>>()?,
_ => DEFAULT_TEST_CMD.to_vec(),
};
let (rc, out, err) = _run(&cmd, worktree, GATES_TIMEOUT_SECS)?;
let (rc, out, err) = _run(&argv, worktree, GATES_TIMEOUT_SECS)?;
let mut combined = out;
combined.push_str(&err);
Ok(json!({
@@ -313,21 +416,13 @@ pub fn run_gates(worktree: &str, test_cmd: Option<&Vec<Value>>) -> Result<Value,
/// `compare_baseline` — candidate vs baseline gate runs. Regression blocks.
pub fn compare_baseline(candidate_result: &Value, baseline_result: &Value) -> Value {
if !truthy(candidate_result.get("passed")) {
return json!({
"promote": false,
"reason": "candidate gates red — regression blocks promotion",
});
}
if !truthy(baseline_result.get("passed")) {
return json!({
"promote": true,
"reason": "candidate green, baseline was already red — no regression introduced",
});
}
let verdict = BaselineVerdict::classify(
truthy(candidate_result.get("passed")),
truthy(baseline_result.get("passed")),
);
json!({
"promote": true,
"reason": "candidate green, baseline green — no regression",
"promote": verdict.promote(),
"reason": verdict.reason(),
})
}
@@ -356,71 +451,56 @@ fn passthrough_field(doc: &Value, key: &str) -> Result<Value, FfiError> {
/// proposal branch. rollback_ref is read lazily: in the oracle the
/// KeyError for a missing key fires only after the merge/remove side
/// effects have run.
pub fn promote_or_discard(repo: &str, proposal: &Value, verdict: &Value) -> Result<Value, FfiError> {
let branch = argv_str(proposal, "branch")?.to_string();
let wt = argv_str(proposal, "worktree")?.to_string();
pub fn promote_or_discard(
repo: &str,
proposal: &Value,
verdict: &Value,
) -> Result<Value, FfiError> {
let branch = argv_str(proposal, "branch")?;
let wt = argv_str(proposal, "worktree")?;
let rollback_ref = || passthrough_field(proposal, "rollback_ref");
let remove_wt = vec![
"git".to_string(),
"worktree".to_string(),
"remove".to_string(),
wt,
"--force".to_string(),
];
let remove_wt = || ["git", "worktree", "remove", wt, "--force"];
if truthy(verdict.get("promote")) {
let msg = format!("self-modify: promote {branch}");
let merge = vec![
"git".to_string(),
"merge".to_string(),
branch.clone(),
"--no-ff".to_string(),
"-m".to_string(),
msg,
];
let (rc, _out, err) = _run(&merge, repo, RUN_TIMEOUT_SECS)?;
let _ = _run(&remove_wt, repo, RUN_TIMEOUT_SECS);
let (rc, _out, err) = _run(
&["git", "merge", branch, "--no-ff", "-m", &msg],
repo,
RUN_TIMEOUT_SECS,
)?;
let _ = _run(&remove_wt(), repo, RUN_TIMEOUT_SECS);
let rollback_ref = rollback_ref()?;
if rc != 0 {
return Ok(json!({
"promoted": false,
"error": format!("merge failed: {}", chars_head(&err, ERROR_HEAD_CHARS)),
"rollback_ref": rollback_ref,
}));
}
return Ok(json!({
"promoted": true,
"branch": branch,
"rollback_ref": rollback_ref,
}));
let outcome = if rc != 0 {
ProposalOutcome::MergeFailed {
error: format!("merge failed: {}", chars_head(&err, ERROR_HEAD_CHARS)),
rollback_ref,
}
} else {
ProposalOutcome::Promoted {
branch: branch.to_string(),
rollback_ref,
}
};
return Ok(outcome.into_value());
}
let _ = _run(&remove_wt, repo, RUN_TIMEOUT_SECS);
let drop_branch = vec![
"git".to_string(),
"branch".to_string(),
"-D".to_string(),
branch.clone(),
];
let _ = _run(&drop_branch, repo, RUN_TIMEOUT_SECS);
let _ = _run(&remove_wt(), repo, RUN_TIMEOUT_SECS);
let _ = _run(&["git", "branch", "-D", branch], repo, RUN_TIMEOUT_SECS);
let rollback_ref = rollback_ref()?;
Ok(json!({
"promoted": false,
"discarded": true,
"branch": branch,
"rollback_ref": rollback_ref,
}))
Ok(ProposalOutcome::Discarded {
branch: branch.to_string(),
rollback_ref,
}
.into_value())
}
/// `rollback` — restore the pre-change ref. The recovery path every
/// proposal carries.
pub fn rollback(repo: &str, rollback_ref: &str) -> Result<Value, FfiError> {
let reset = vec![
"git".to_string(),
"reset".to_string(),
"--hard".to_string(),
rollback_ref.to_string(),
];
let (rc, _out, err) = _run(&reset, repo, RUN_TIMEOUT_SECS)?;
let (rc, _out, err) = _run(
&["git", "reset", "--hard", rollback_ref],
repo,
RUN_TIMEOUT_SECS,
)?;
if rc != 0 {
return Ok(json!({
"rolled_back": false,
@@ -518,7 +598,9 @@ pub extern "C" fn smod_compare_baseline(arg: *const c_char) -> *mut c_char {
pub extern "C" fn smod_promote_or_discard(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).and_then(|a| {
let repo = required_str(&a, "repo")?;
let proposal = a.get("proposal").ok_or_else(|| FfiError::KeyError("proposal".to_string()))?;
let proposal = a
.get("proposal")
.ok_or_else(|| FfiError::KeyError("proposal".to_string()))?;
let verdict = a
.get("verdict")
.ok_or_else(|| FfiError::KeyError("verdict".to_string()))?;