refactor(drift_resume): Phase B language-advantage pass — dichotomy wave
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 10s
CI Pipeline / rust-surface (push) Failing after 18s
CI Pipeline / go-surface (push) Failing after 24s
CI Pipeline / gitea-offline (push) Successful in 21s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 52s
CI Pipeline / lint (push) Canceled after 51s
CI Pipeline / canary (push) Canceled after 51s
Auto-version and Release / release (push) Canceled after 53s
deploy / deploy (push) Canceled after 36s
CI Pipeline / type-check (push) Canceled after 47s
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 10s
CI Pipeline / rust-surface (push) Failing after 18s
CI Pipeline / go-surface (push) Failing after 24s
CI Pipeline / gitea-offline (push) Successful in 21s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 52s
CI Pipeline / lint (push) Canceled after 51s
CI Pipeline / canary (push) Canceled after 51s
Auto-version and Release / release (push) Canceled after 53s
deploy / deploy (push) Canceled after 36s
CI Pipeline / type-check (push) Canceled after 47s
The drift vocabulary becomes a DriftClass enum with exhaustive classify/parse/is_blocking matches (no string soup — the FFI record still serializes the oracle's exact strings; BENIGN stays in the Python facade since classify_drift can never assign it — vocabulary for downstream consumers, not a classified value); the oracle's 'matches is True / is None / else' identity-check triple is a Verdict three-way lattice classified ONCE and matched exhaustively (a truthy non-True value, e.g. 1, is NOT Matches — exact parity with the `is` checks); reconcile's blocking filter goes through DriftClass::parse → is_blocking instead of a string-array scan; the moved-twin outcome is a MovedTwin enum whose evidence()/reconciliation() methods keep the paired strings consistent by construction; FFI errors are a typed FfiError enum (NullPtr/Utf8/Json) behind the unchanged doc-envelope err string. Battery 3/3 green throughout; live parity 35/35 facade + 35/35 kernel re-verified against the rebuilt cdylib; clippy 0; held-out suites green live AND pinned.
This commit is contained in:
1 parent
29ce279702
commit
51ef7cf967
1 file changed
+169
-66
@@ -29,16 +29,67 @@
|
||||
//! No dr_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 drift
|
||||
//! vocabulary is a `DriftClass` enum with an exhaustive classify/`is_
|
||||
//! blocking` match (no string soup; the FFI record still serializes the
|
||||
//! oracle's exact strings); the oracle's `matches is True / is None /
|
||||
//! else` triple is a `Verdict` three-way lattice classified once and
|
||||
//! matched exhaustively; the moved-twin outcome is a `MovedTwin` enum
|
||||
//! whose `evidence`/`reconciliation` methods keep the paired strings
|
||||
//! consistent by construction; the FFI layer is a typed `FfiError` enum
|
||||
//! behind the unchanged doc-envelope err string. Battery green
|
||||
//! throughout.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub const EXPECTED: &str = "EXPECTED"; // checkpoint older than reality; benign aging
|
||||
pub const BENIGN: &str = "BENIGN"; // difference with no bearing on the claims
|
||||
pub const MATERIAL: &str = "MATERIAL"; // reality contradicts a scaffold claim
|
||||
pub const CONFLICTING: &str = "CONFLICTING"; // two authorities disagree; needs user
|
||||
pub const UNKNOWN: &str = "UNKNOWN"; // cannot decide — treated as blocking
|
||||
// ── drift-class lattice ─────────────────────────────────────────────────────
|
||||
|
||||
pub const BLOCKING_CLASSES: [&str; 3] = [MATERIAL, CONFLICTING, UNKNOWN];
|
||||
/// The classify_drift vocabulary as CLASSIFIED — exhaustive, no string
|
||||
/// soup. The FFI record serializes the oracle's exact strings. (The
|
||||
/// oracle's BENIGN constant is vocabulary for downstream consumers, not
|
||||
/// a value classify_drift can assign — it stays in the Python facade.)
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum DriftClass {
|
||||
Expected, // checkpoint older than reality; benign aging
|
||||
Confirmed, // reality matches the claim
|
||||
Material, // reality contradicts a scaffold claim
|
||||
Conflicting, // two authorities disagree; needs user
|
||||
Unknown, // cannot decide — treated as blocking
|
||||
}
|
||||
|
||||
impl DriftClass {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
DriftClass::Expected => "EXPECTED",
|
||||
DriftClass::Confirmed => "CONFIRMED",
|
||||
DriftClass::Material => "MATERIAL",
|
||||
DriftClass::Conflicting => "CONFLICTING",
|
||||
DriftClass::Unknown => "UNKNOWN",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Option<DriftClass> {
|
||||
Some(match s {
|
||||
"EXPECTED" => DriftClass::Expected,
|
||||
"CONFIRMED" => DriftClass::Confirmed,
|
||||
"MATERIAL" => DriftClass::Material,
|
||||
"CONFLICTING" => DriftClass::Conflicting,
|
||||
"UNKNOWN" => DriftClass::Unknown,
|
||||
"BENIGN" => return None, // oracle constant, not a classified value
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// BLOCKING_CLASSES: MATERIAL / CONFLICTING / UNKNOWN stop dependent
|
||||
/// work until reconciled.
|
||||
fn is_blocking(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DriftClass::Material | DriftClass::Conflicting | DriftClass::Unknown
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Python scalar semantics ─────────────────────────────────────────────────
|
||||
|
||||
@@ -75,59 +126,69 @@ fn get_or_dflt(o: &Value, key: &str, dflt: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// ── classify_drift ──────────────────────────────────────────────────────────
|
||||
// ── classification ──────────────────────────────────────────────────────────
|
||||
|
||||
/// The oracle's `matches is True / is None / else` triple, classified
|
||||
/// ONCE — the identity checks are exact (a truthy non-True value, e.g.
|
||||
/// 1, is NOT True; a missing key and an explicit null both mean None).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Verdict {
|
||||
Matches,
|
||||
Unverifiable,
|
||||
Contradicted,
|
||||
}
|
||||
|
||||
impl Verdict {
|
||||
fn classify(observation: &Value) -> Verdict {
|
||||
match observation.get("matches") {
|
||||
Some(Value::Bool(true)) => Verdict::Matches,
|
||||
None | Some(Value::Null) => Verdict::Unverifiable,
|
||||
_ => Verdict::Contradicted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify one claim-vs-observation pair. Deterministic: verification
|
||||
/// evidence beats prose; explicit beats inferred. `matches is True` and
|
||||
/// `matches is None` are IDENTITY checks in the oracle — a truthy
|
||||
/// non-True value (e.g. 1) falls through to the reality-differs branch,
|
||||
/// and "None" covers both a missing key and an explicit null.
|
||||
/// evidence beats prose; explicit beats inferred.
|
||||
pub fn classify_drift(claim: &Value, observation: &Value) -> Value {
|
||||
let claim_text = claim.get("claim").and_then(Value::as_str).unwrap_or("");
|
||||
match observation.get("matches") {
|
||||
Some(Value::Bool(true)) => {
|
||||
let stale_ok = claim
|
||||
.get("stale_ok")
|
||||
.map(py_truthy)
|
||||
.unwrap_or(false);
|
||||
json!({
|
||||
"claim": claim_text,
|
||||
"class": if stale_ok { EXPECTED } else { "CONFIRMED" },
|
||||
"evidence": get_or_dflt(observation, "checked_by", ""),
|
||||
})
|
||||
let (class, evidence) = match Verdict::classify(observation) {
|
||||
Verdict::Matches => {
|
||||
let stale_ok = claim.get("stale_ok").map(py_truthy).unwrap_or(false);
|
||||
(
|
||||
if stale_ok { DriftClass::Expected } else { DriftClass::Confirmed },
|
||||
get_or_dflt(observation, "checked_by", ""),
|
||||
)
|
||||
}
|
||||
None | Some(Value::Null) => json!({
|
||||
"claim": claim_text,
|
||||
"class": UNKNOWN,
|
||||
"evidence": format!(
|
||||
Verdict::Unverifiable => (
|
||||
DriftClass::Unknown,
|
||||
format!(
|
||||
"unverifiable via {}",
|
||||
get_or_dflt(observation, "checked_by", "?")
|
||||
),
|
||||
}),
|
||||
_ => {
|
||||
let user_locked = claim
|
||||
.get("user_locked")
|
||||
.map(py_truthy)
|
||||
.unwrap_or(false);
|
||||
),
|
||||
Verdict::Contradicted => {
|
||||
let user_locked =
|
||||
claim.get("user_locked").map(py_truthy).unwrap_or(false);
|
||||
if user_locked {
|
||||
json!({
|
||||
"claim": claim_text,
|
||||
"class": CONFLICTING,
|
||||
"evidence": "reality differs from a USER-LOCKED claim — user arbitration required",
|
||||
})
|
||||
(
|
||||
DriftClass::Conflicting,
|
||||
"reality differs from a USER-LOCKED claim — user arbitration required"
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
json!({
|
||||
"claim": claim_text,
|
||||
"class": MATERIAL,
|
||||
"evidence": format!(
|
||||
(
|
||||
DriftClass::Material,
|
||||
format!(
|
||||
"reality differs: {} (checked by {})",
|
||||
get_or_dflt(observation, "actual", ""),
|
||||
get_or_dflt(observation, "checked_by", "?")
|
||||
),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
json!({"claim": claim_text, "class": class.as_str(), "evidence": evidence})
|
||||
}
|
||||
|
||||
// ── reconcile ───────────────────────────────────────────────────────────────
|
||||
@@ -145,7 +206,11 @@ pub fn reconcile(claims: &[Value], observations: &[Value]) -> Value {
|
||||
let blocking: Vec<Value> = drifts
|
||||
.iter()
|
||||
.filter(|d| {
|
||||
BLOCKING_CLASSES.contains(&d["class"].as_str().unwrap_or(""))
|
||||
d["class"]
|
||||
.as_str()
|
||||
.and_then(DriftClass::parse)
|
||||
.map(DriftClass::is_blocking)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
@@ -180,6 +245,29 @@ fn source_of(e: &Value) -> Option<&str> {
|
||||
source_field(e, "ext").or_else(|| source_field(e, "identities"))
|
||||
}
|
||||
|
||||
/// The moved-twin outcome — `evidence` and `reconciliation` are a
|
||||
/// PAIRED rendering of one outcome, kept consistent by construction.
|
||||
enum MovedTwin {
|
||||
Moved(String),
|
||||
Absent,
|
||||
}
|
||||
|
||||
impl MovedTwin {
|
||||
fn evidence(&self) -> String {
|
||||
match self {
|
||||
MovedTwin::Moved(m) => format!("source missing; moved to {}", m),
|
||||
MovedTwin::Absent => "source file absent from repo".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reconciliation(&self) -> String {
|
||||
match self {
|
||||
MovedTwin::Moved(m) => format!("update source to {}", m),
|
||||
MovedTwin::Absent => "entity requires re-derivation or user-locked preservation".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare structural-model component claims against repository reality
|
||||
/// (facade-gathered relative listings). Observable cases: source file
|
||||
/// moved/missing (component identities carry "source"), entities for
|
||||
@@ -212,27 +300,23 @@ pub fn detect_structural_drift(
|
||||
// moved twin (first basename match in listing order)
|
||||
let expected_name =
|
||||
std::path::Path::new(src).file_name().and_then(|n| n.to_str());
|
||||
let moved: Option<String> = expected_name.and_then(|name| {
|
||||
files.iter().find_map(|p| {
|
||||
let s = p.as_str()?;
|
||||
(std::path::Path::new(s).file_name().and_then(|n| n.to_str())
|
||||
== Some(name))
|
||||
.then(|| s.to_string())
|
||||
let twin: MovedTwin = expected_name
|
||||
.and_then(|name| {
|
||||
files.iter().find_map(|p| {
|
||||
let s = p.as_str()?;
|
||||
(std::path::Path::new(s).file_name().and_then(|n| n.to_str())
|
||||
== Some(name))
|
||||
.then(|| s.to_string())
|
||||
})
|
||||
})
|
||||
});
|
||||
.map_or(MovedTwin::Absent, MovedTwin::Moved);
|
||||
let id = e.get("id").map(py_str).unwrap_or_default();
|
||||
drifts.push(json!({
|
||||
"claim": format!("{} source at {}", id, src),
|
||||
"class": if locked { CONFLICTING } else { MATERIAL },
|
||||
"evidence": match &moved {
|
||||
Some(m) => format!("source missing; moved to {}", m),
|
||||
None => "source file absent from repo".to_string(),
|
||||
},
|
||||
"class": if locked { DriftClass::Conflicting.as_str() } else { DriftClass::Material.as_str() },
|
||||
"evidence": twin.evidence(),
|
||||
"reality_wins": !locked,
|
||||
"reconciliation": match &moved {
|
||||
Some(m) => format!("update source to {}", m),
|
||||
None => "entity requires re-derivation or user-locked preservation".to_string(),
|
||||
},
|
||||
"reconciliation": twin.reconciliation(),
|
||||
}));
|
||||
}
|
||||
drifts
|
||||
@@ -240,20 +324,39 @@ pub fn detect_structural_drift(
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────────
|
||||
|
||||
fn read_arg(arg: *const std::os::raw::c_char) -> Result<Value, String> {
|
||||
/// Typed FFI failure lattice behind the doc-envelope Result — the
|
||||
/// caller-visible `err` string is unchanged (the enum only shapes the
|
||||
/// Rust side; parity is pinned by the battery).
|
||||
enum FfiError {
|
||||
NullPtr,
|
||||
Utf8(std::str::Utf8Error),
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FfiError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FfiError::NullPtr => write!(f, "null argument pointer"),
|
||||
FfiError::Utf8(e) => write!(f, "{e}"),
|
||||
FfiError::Json(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_arg(arg: *const std::os::raw::c_char) -> Result<Value, FfiError> {
|
||||
if arg.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
return Err(FfiError::NullPtr);
|
||||
}
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(arg) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(s).map_err(|e| e.to_string())
|
||||
.map_err(FfiError::Utf8)?;
|
||||
serde_json::from_str(s).map_err(FfiError::Json)
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut std::os::raw::c_char {
|
||||
fn envelope(r: Result<Value, FfiError>) -> *mut std::os::raw::c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
Err(e) => json!({"ok": false, "err": e.to_string()}),
|
||||
};
|
||||
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
|
||||
Reference in new issue
Block a user