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
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:
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
@@ -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()))?;
|
||||
|
||||
Reference in new issue
Block a user