chore(rust): rustfmt rollout to current stable canonicality (#2535)
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Local-Model First-Pass Review (#131) / first-pass-review (pull_request) Successful in 41s
CI Pipeline / best-practices (pull_request) Successful in 4s
Spec gate (headless) / spec-gate (pull_request) Successful in 41s
CI Pipeline / documentation (pull_request) Successful in 7s
CI Pipeline / go-surface (pull_request) Failing after 51s
CI Pipeline / gitea-offline (pull_request) Successful in 22s
CI Pipeline / lint (pull_request) Failing after 1m11s
CI Pipeline / type-check (pull_request) Failing after 55s
CI Pipeline / canary (pull_request) Failing after 5m7s
CI Pipeline / rust-surface (pull_request) Failing after 10m54s
CI Pipeline / unit-tests (pull_request) Failing after 46m3s
CI Pipeline / required-checks (pull_request) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Failing after 5s
CI Pipeline / lint (push) Canceled after 0s
CI Pipeline / unit-tests (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 0s
CI Pipeline / canary (push) Canceled after 0s
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Canceled after 0s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
Local-Model First-Pass Review (#131) / first-pass-review (pull_request) Successful in 41s
CI Pipeline / best-practices (pull_request) Successful in 4s
Spec gate (headless) / spec-gate (pull_request) Successful in 41s
CI Pipeline / documentation (pull_request) Successful in 7s
CI Pipeline / go-surface (pull_request) Failing after 51s
CI Pipeline / gitea-offline (pull_request) Successful in 22s
CI Pipeline / lint (pull_request) Failing after 1m11s
CI Pipeline / type-check (pull_request) Failing after 55s
CI Pipeline / canary (pull_request) Failing after 5m7s
CI Pipeline / rust-surface (pull_request) Failing after 10m54s
CI Pipeline / unit-tests (pull_request) Failing after 46m3s
CI Pipeline / required-checks (pull_request) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Failing after 5s
contract:ADMIT-rustfmt-rollout — the host rustfmt was upgraded (stable, 2026-10-20); its canonical wrapping changed and ~100 crates drifted out of cargo fmt -- --check, reding rust-surface on master and every branch. One mechanical per-crate cargo fmt; formatting only; all crates build clean; fmt --check green per crate. Same class as the uv 0.13.4 resolver drift: host toolchain upgrades must ship a repo rollout commit.
This commit is contained in:
1 parent
5844519723
commit
7aff988f14
133 files changed
+7094
-2685
No files matched your search
@@ -0,0 +1,114 @@
|
||||
"""Admission dispatch for the rustfmt rollout across src/rust/*
|
||||
(toolchain-upgrade rollout, owner-approved via the #2535 CI repair wave).
|
||||
|
||||
classify + activate + admit (the headless scaffolding entry law,
|
||||
scripts/admit_repair_verification_layer.py pattern): the host rustfmt
|
||||
(stable toolchain, upgraded 2026-10-20) changed canonical formatting —
|
||||
every committed crate drifted out of `cargo fmt -- --check`, reding the
|
||||
rust-surface CI job on master AND every branch (observed: run 33038 job
|
||||
91717, diffs in worthiness.rs / branch_battery.rs / ~60 more crates). The
|
||||
rollout applies one mechanical per-crate `cargo fmt` in the current
|
||||
toolchain's canonicality; formatting only, no semantic diff; every crate
|
||||
builds clean after (verified per-crate cargo build --release).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from rune import construction_activation as cact # noqa: E402
|
||||
from rune import construction_admission as cadm # noqa: E402
|
||||
from rune import construction_mode as cmode # noqa: E402
|
||||
from rune import scaffold as sc # noqa: E402
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
||||
|
||||
OBJECTIVE = (
|
||||
"rustfmt rollout: reformat every src/rust crate in the CURRENT stable "
|
||||
"toolchain's canonicality — the host rustfmt was upgraded (2026-10-20) "
|
||||
"and its canonical wrapping changed, so ~100 committed crates drifted "
|
||||
"out of cargo fmt -- --check and the rust-surface CI job went red on "
|
||||
"master and every branch (formatting-only change; per-crate cargo "
|
||||
"build --release verified clean after; fmt -- --check verified green "
|
||||
"per crate). Mechanical, evidence-backed: red-first proof = the CI job "
|
||||
"diffs (worthiness.rs:435, branch_battery.rs:7, spine gap_battery)."
|
||||
)
|
||||
|
||||
|
||||
def _affected() -> list[str]:
|
||||
surface = []
|
||||
rust_dir = os.path.join(ROOT, "src", "rust")
|
||||
for entry in sorted(os.listdir(rust_dir)):
|
||||
crate = os.path.join(rust_dir, entry)
|
||||
if os.path.isdir(crate) and os.path.isfile(os.path.join(crate, "Cargo.toml")):
|
||||
surface.append(f"src/rust/{entry}")
|
||||
return surface
|
||||
|
||||
|
||||
def main() -> int:
|
||||
store = os.path.join(ROOT, "store", "scaffold")
|
||||
scaffold = sc.Scaffold(store, "rune")
|
||||
|
||||
affected = _affected()
|
||||
md = cmode.classify_mode(
|
||||
OBJECTIVE,
|
||||
target_project="rune",
|
||||
project_exists=True,
|
||||
affected_surface=affected,
|
||||
)
|
||||
print("classified mode:", md.mode)
|
||||
|
||||
need = {
|
||||
"need_id": "need-rustfmt-rollout",
|
||||
"objective": OBJECTIVE,
|
||||
"origin": "worker",
|
||||
"project": "rune",
|
||||
"task_ref": "rustfmt-rollout",
|
||||
"affected_surface": affected,
|
||||
"evidence_refs": [
|
||||
"src/rust/worthiness/src/worthiness.rs",
|
||||
"src/rust/spine/src/branch_battery_gap.rs",
|
||||
".gitea/workflows/ci.yml",
|
||||
],
|
||||
}
|
||||
act = cact.activate(md, need=need, scaffold=scaffold,
|
||||
builder="rustfmt-rollout")
|
||||
print("activated:", act.get("activated"), act.get("mode"))
|
||||
|
||||
env = cadm.admit(
|
||||
act,
|
||||
objective=OBJECTIVE,
|
||||
builder="rustfmt-rollout",
|
||||
affected_surface=affected,
|
||||
evidence_refs=need["evidence_refs"],
|
||||
origin="worker",
|
||||
task_ref="rustfmt-rollout",
|
||||
expected_new_entities=[
|
||||
"scripts/admit_rustfmt_rollout.py",
|
||||
],
|
||||
expected_runtime_effects=[
|
||||
"cargo fmt -- --check green per crate under the current "
|
||||
"stable toolchain; every crate builds clean after reformat",
|
||||
],
|
||||
verification_obligations=[
|
||||
"cargo fmt --manifest-path <crate>/Cargo.toml -- --check green "
|
||||
"for every crate with a manifest",
|
||||
"per-crate cargo build --release green on the reformatted tree",
|
||||
"rust-surface CI job green on the next run",
|
||||
],
|
||||
regression_obligations=[
|
||||
"formatting-only: no semantic diff (no identifiers, logic, or "
|
||||
"test expectations changed by the rollout)",
|
||||
],
|
||||
approval_ref="user:#2535 CI repair wave dispatch (fix the wave first)",
|
||||
contract_id="ADMIT-rustfmt-rollout",
|
||||
project_store=store,
|
||||
project_id="rune",
|
||||
)
|
||||
print(json.dumps(env, indent=2, default=str)[:2400])
|
||||
return 0 if env.get("success") else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -3,7 +3,7 @@
|
||||
//! inputs deep-copied before capture). Order-insensitive object
|
||||
//! comparison; numbers compared exactly (both sides IEEE-754 f64).
|
||||
|
||||
use capability_scoring::pyjson::{parse, dumps, Json};
|
||||
use capability_scoring::pyjson::{dumps, parse, Json};
|
||||
|
||||
use crate::ladder;
|
||||
|
||||
@@ -66,7 +66,11 @@ fn run_case(fn_name: &str, args: &Json) -> Result<Json, String> {
|
||||
fn frozen_truth_replays_verbatim() {
|
||||
let truth = parse(TRUTH).expect("frozen_al_truth.json parses");
|
||||
let cases = as_arr(truth.get("cases").expect("cases key"));
|
||||
assert!(cases.len() >= 76, "expected the full frozen battery, got {}", cases.len());
|
||||
assert!(
|
||||
cases.len() >= 76,
|
||||
"expected the full frozen battery, got {}",
|
||||
cases.len()
|
||||
);
|
||||
let mut failures: Vec<String> = vec![];
|
||||
for (i, case) in cases.iter().enumerate() {
|
||||
let fn_name = match case.get("fn") {
|
||||
@@ -80,7 +84,9 @@ fn frozen_truth_replays_verbatim() {
|
||||
Ok(got) => failures.push(format!(
|
||||
"case {i} ({} {}): kernel {:?} != expected {}",
|
||||
fn_name,
|
||||
case.get("name").and_then(|v| as_str(Some(v))).unwrap_or("?"),
|
||||
case.get("name")
|
||||
.and_then(|v| as_str(Some(v)))
|
||||
.unwrap_or("?"),
|
||||
dumps(&got),
|
||||
dumps(expected),
|
||||
)),
|
||||
|
||||
@@ -42,9 +42,7 @@ use Stage::{Belief, Concept, Episode, Fact, Observation, Rule, Skill};
|
||||
|
||||
impl Stage {
|
||||
/// All rungs, in ladder order (the oracle's STAGES tuple).
|
||||
pub const LADDER: [Stage; 7] = [
|
||||
Episode, Fact, Observation, Concept, Belief, Rule, Skill,
|
||||
];
|
||||
pub const LADDER: [Stage; 7] = [Episode, Fact, Observation, Concept, Belief, Rule, Skill];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
@@ -138,7 +136,11 @@ struct Verdict {
|
||||
|
||||
impl Verdict {
|
||||
fn refuse(reason: impl Into<String>) -> Verdict {
|
||||
Verdict { promote: false, to: None, reason: reason.into() }
|
||||
Verdict {
|
||||
promote: false,
|
||||
to: None,
|
||||
reason: reason.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn grant(to: Stage) -> Verdict {
|
||||
@@ -150,8 +152,7 @@ impl Verdict {
|
||||
}
|
||||
|
||||
fn to_pairs(&self) -> Vec<(String, Json)> {
|
||||
let mut pairs: Vec<(String, Json)> =
|
||||
vec![("promote".into(), Json::Bool(self.promote))];
|
||||
let mut pairs: Vec<(String, Json)> = vec![("promote".into(), Json::Bool(self.promote))];
|
||||
if let Some(to) = self.to {
|
||||
pairs.push(("to".into(), Json::Str(to.name().into())));
|
||||
}
|
||||
@@ -187,7 +188,10 @@ impl Gate {
|
||||
/// The stage after this one (None at the top). A non-rung stage
|
||||
/// mirrors the oracle's STAGES.index ValueError → None.
|
||||
pub fn next_stage(stage: Option<&str>) -> Option<&'static str> {
|
||||
stage.and_then(Stage::parse).and_then(Stage::next).map(Stage::name)
|
||||
stage
|
||||
.and_then(Stage::parse)
|
||||
.and_then(Stage::next)
|
||||
.map(Stage::name)
|
||||
}
|
||||
|
||||
/// Can this item promote? Needs: evidence threshold met, provenance
|
||||
@@ -230,8 +234,7 @@ fn verdict(item: &Json) -> Verdict {
|
||||
}) {
|
||||
return Verdict::refuse(reason);
|
||||
}
|
||||
if stage.needs_verified_outcome()
|
||||
&& !item.get("verified_outcome").map(truthy).unwrap_or(false)
|
||||
if stage.needs_verified_outcome() && !item.get("verified_outcome").map(truthy).unwrap_or(false)
|
||||
{
|
||||
return Verdict::refuse("belief+ requires a verified outcome");
|
||||
}
|
||||
@@ -267,10 +270,9 @@ fn with_item(mut pairs: Vec<(String, Json)>, item: Json) -> Json {
|
||||
pub fn promote(item: &Json) -> Json {
|
||||
let v = verdict(item);
|
||||
let out_item = if v.promote {
|
||||
let to = v
|
||||
.to
|
||||
.map(|s| Json::Str(s.name().into()))
|
||||
.unwrap_or(Json::Null);
|
||||
let to =
|
||||
v.to.map(|s| Json::Str(s.name().into()))
|
||||
.unwrap_or(Json::Null);
|
||||
set_key(&set_key(item, "stage", to), "sightings", Json::Num(0.0))
|
||||
} else {
|
||||
item.clone()
|
||||
|
||||
@@ -29,16 +29,15 @@ fn read_arg(ptr: *const c_char) -> Result<Json, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
pyjson::parse(x)
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Ok(x) => Json::Obj(vec![("ok".into(), Json::Bool(true)), ("value".into(), x)]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
//! capture_messages / to_prompt / estimate_tokens / create_capsule across
|
||||
//! every branch. Embedded VERBATIM — never transcribed.
|
||||
|
||||
use crate::{
|
||||
capture_messages, create_capsule, estimate_tokens, test_capsule, AttentionCapsule,
|
||||
};
|
||||
use crate::{capture_messages, create_capsule, estimate_tokens, test_capsule, AttentionCapsule};
|
||||
use serde_json::Value;
|
||||
|
||||
const TRUTH: &str = include_str!("frozen_ac_truth.json");
|
||||
|
||||
@@ -61,14 +61,24 @@ pub enum BlockWire {
|
||||
/// attributes in Python — getattr defaults — and is skipped by
|
||||
/// `to_messages`).
|
||||
enum Block<'a> {
|
||||
Obj { name: &'a str, content: &'a Value, payload: Option<&'a Value> },
|
||||
Dict { payload: &'a Value },
|
||||
Obj {
|
||||
name: &'a str,
|
||||
content: &'a Value,
|
||||
payload: Option<&'a Value>,
|
||||
},
|
||||
Dict {
|
||||
payload: &'a Value,
|
||||
},
|
||||
}
|
||||
|
||||
impl AttentionCapsule {
|
||||
fn blocks(&self) -> impl Iterator<Item = Block<'_>> {
|
||||
self.blocks.iter().map(|b| match b {
|
||||
BlockWire::Obj { name, content, dict } => Block::Obj {
|
||||
BlockWire::Obj {
|
||||
name,
|
||||
content,
|
||||
dict,
|
||||
} => Block::Obj {
|
||||
name: name.as_str(),
|
||||
content,
|
||||
payload: dict.as_ref(),
|
||||
@@ -140,7 +150,12 @@ impl PromptSection {
|
||||
PromptSection::Memories => {
|
||||
if !c.relevant_memories.is_empty() {
|
||||
parts.push("Relevant memories:".into());
|
||||
parts.extend(c.relevant_memories.iter().take(5).map(|mem| format!(" - {mem}")));
|
||||
parts.extend(
|
||||
c.relevant_memories
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|mem| format!(" - {mem}")),
|
||||
);
|
||||
}
|
||||
}
|
||||
PromptSection::Commitments => {
|
||||
@@ -294,8 +309,8 @@ fn relevant_lines<'a>(
|
||||
}
|
||||
let line_low = line.to_lowercase();
|
||||
let line_words: HashSet<&str> = line_low.split_whitespace().collect();
|
||||
(task_words.intersection(&line_words).count() >= 2)
|
||||
.then(|| take(line, 100)) // truncate long lines
|
||||
(task_words.intersection(&line_words).count() >= 2).then(|| take(line, 100))
|
||||
// truncate long lines
|
||||
})
|
||||
.take(5) // max 5 memories
|
||||
}
|
||||
@@ -349,8 +364,11 @@ enum TrimStage {
|
||||
Objective,
|
||||
}
|
||||
|
||||
const TRIM_ORDER: &[TrimStage] =
|
||||
&[TrimStage::Memories, TrimStage::Constraints, TrimStage::Objective];
|
||||
const TRIM_ORDER: &[TrimStage] = &[
|
||||
TrimStage::Memories,
|
||||
TrimStage::Constraints,
|
||||
TrimStage::Objective,
|
||||
];
|
||||
|
||||
impl AttentionCapsule {
|
||||
fn retrim(&mut self, max_tokens: i64) {
|
||||
@@ -439,7 +457,9 @@ fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -460,14 +480,18 @@ fn parse_capsule(v: &Value) -> Result<AttentionCapsule, String> {
|
||||
/// ac_to_dict(capsule) -> capsule dict
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ac_to_dict(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| parse_capsule(&a)).map(|c| c.to_dict());
|
||||
let r = read_arg(arg)
|
||||
.and_then(|a| parse_capsule(&a))
|
||||
.map(|c| c.to_dict());
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// ac_to_prompt(capsule) -> prompt string
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ac_to_prompt(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| parse_capsule(&a)).map(|c| Value::String(c.to_prompt()));
|
||||
let r = read_arg(arg)
|
||||
.and_then(|a| parse_capsule(&a))
|
||||
.map(|c| Value::String(c.to_prompt()));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
@@ -476,7 +500,10 @@ pub extern "C" fn ac_to_prompt(arg: *const c_char) -> *mut c_char {
|
||||
pub extern "C" fn ac_to_messages(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let capsule = parse_capsule(a.get("capsule").unwrap_or(&Value::Null))?;
|
||||
let preamble = a.get("system_preamble").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let preamble = a
|
||||
.get("system_preamble")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
Ok(capsule.to_messages(preamble))
|
||||
});
|
||||
envelope(r)
|
||||
|
||||
@@ -9,11 +9,12 @@
|
||||
//! bh_pure_truth.json (freeze_truth envelope) replays through the same
|
||||
//! public fns — the in-crate twin of `freeze_truth.py --verify`.
|
||||
|
||||
use super::{behavior_stats, connect, list_items, plugin,
|
||||
_bh_core, _correction_matches_python, _habit_matches_python,
|
||||
_reinforce_python, _reinforce_rust,
|
||||
correction_matches, correction_reinforce, correction_visible, habit_matches,
|
||||
habit_reinforce, habit_visible, parse_scope_tags, CorrectionRow, HabitRow};
|
||||
use super::{
|
||||
_bh_core, _correction_matches_python, _habit_matches_python, _reinforce_python,
|
||||
_reinforce_rust, behavior_stats, connect, correction_matches, correction_reinforce,
|
||||
correction_visible, habit_matches, habit_reinforce, habit_visible, list_items,
|
||||
parse_scope_tags, plugin, CorrectionRow, HabitRow,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
const TRUTH: &str = include_str!("bh_truth.json");
|
||||
@@ -68,7 +69,11 @@ fn test_add_shapes() {
|
||||
assert_eq!(row["evidence_count"].as_i64(), Some(1));
|
||||
assert_eq!(
|
||||
row["scope_tags"].as_str(),
|
||||
Some(if c["name"] == "scoped" { r#"{"k": "v"}"# } else { "{}" }),
|
||||
Some(if c["name"] == "scoped" {
|
||||
r#"{"k": "v"}"#
|
||||
} else {
|
||||
"{}"
|
||||
}),
|
||||
"{}: scope_tags stored as JSON string",
|
||||
c["name"]
|
||||
);
|
||||
@@ -85,7 +90,11 @@ fn test_add_shapes() {
|
||||
);
|
||||
assert_eq!(
|
||||
row["context_tags"].as_str(),
|
||||
Some(if c["name"] == "full" { r#"["a"]"# } else { "[]" }),
|
||||
Some(if c["name"] == "full" {
|
||||
r#"["a"]"#
|
||||
} else {
|
||||
"[]"
|
||||
}),
|
||||
"{}: context_tags stored as JSON string",
|
||||
c["name"]
|
||||
);
|
||||
@@ -116,21 +125,40 @@ fn test_match_matrix() {
|
||||
// every frozen habit survivor text corresponds to a trigger that
|
||||
// matches the context; corrections need BOTH pattern and domain.
|
||||
if case["name"].as_str() == Some("pattern-domain-hit") {
|
||||
assert!(correction_matches("deploy", "shell", case["context"].as_str().unwrap()));
|
||||
assert!(correction_matches(
|
||||
"deploy",
|
||||
"shell",
|
||||
case["context"].as_str().unwrap()
|
||||
));
|
||||
assert!(habit_matches("deploy", case["context"].as_str().unwrap()));
|
||||
}
|
||||
if case["name"].as_str() == Some("domain-only-miss") {
|
||||
// "deploy the editor": editor-domain correction (no pattern)
|
||||
// survives, shell-domain one dies on the domain filter.
|
||||
assert!(correction_matches("", "editor", case["context"].as_str().unwrap()));
|
||||
assert!(!correction_matches("", "shell", case["context"].as_str().unwrap()));
|
||||
assert!(correction_matches(
|
||||
"",
|
||||
"editor",
|
||||
case["context"].as_str().unwrap()
|
||||
));
|
||||
assert!(!correction_matches(
|
||||
"",
|
||||
"shell",
|
||||
case["context"].as_str().unwrap()
|
||||
));
|
||||
}
|
||||
if case["name"].as_str() == Some("low-conf-filtered") {
|
||||
assert!(!habit_visible("candidate", 0.2), "confidence gate");
|
||||
}
|
||||
if case["name"].as_str() == Some("bad-regex-skipped") {
|
||||
assert!(!correction_matches("([unclosed", "", case["context"].as_str().unwrap()));
|
||||
assert!(!habit_matches("([broken", case["context"].as_str().unwrap()));
|
||||
assert!(!correction_matches(
|
||||
"([unclosed",
|
||||
"",
|
||||
case["context"].as_str().unwrap()
|
||||
));
|
||||
assert!(!habit_matches(
|
||||
"([broken",
|
||||
case["context"].as_str().unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,14 +263,18 @@ fn test_reinforce_correction_matrix() {
|
||||
}
|
||||
}
|
||||
};
|
||||
let pre_count = row["evidence_count"].as_i64().unwrap()
|
||||
- i64::from(outcome == "success") * n;
|
||||
let pre_count =
|
||||
row["evidence_count"].as_i64().unwrap() - i64::from(outcome == "success") * n;
|
||||
// Replay and compare every step + final row.
|
||||
let mut count = pre_count;
|
||||
for i in 0..n {
|
||||
let (nc, ncnt, ns) = correction_reinforce(conf, count, outcome);
|
||||
let want = &case["results"][i as usize];
|
||||
assert_eq!(want["state"].as_str(), Some(ns.as_str()), "{name}: step {i} state");
|
||||
assert_eq!(
|
||||
want["state"].as_str(),
|
||||
Some(ns.as_str()),
|
||||
"{name}: step {i} state"
|
||||
);
|
||||
let wconf = want["confidence"].as_f64().unwrap();
|
||||
assert!(
|
||||
(wconf - nc).abs() < 1e-12,
|
||||
@@ -281,18 +313,29 @@ fn test_visibility_gates_and_list_counts() {
|
||||
assert_eq!(c["n_corr"].as_i64(), Some(1), "LIMIT 1 applies");
|
||||
}
|
||||
"type-correction" => {
|
||||
assert!(c["keys"].as_array().unwrap().iter().all(|k| k.as_str() == Some("corrections")));
|
||||
assert!(c["keys"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|k| k.as_str() == Some("corrections")));
|
||||
assert!(c["n_hab"].as_i64().unwrap() == 0 || c.get("n_hab").is_none());
|
||||
}
|
||||
"type-habit" => {
|
||||
assert!(c["keys"].as_array().unwrap().iter().all(|k| k.as_str() == Some("habits")));
|
||||
assert!(c["keys"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|k| k.as_str() == Some("habits")));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Gates (unit-level, oracle-probed formulas):
|
||||
assert!(correction_visible("active", 0.31));
|
||||
assert!(!correction_visible("active", 0.3), "confidence > 0.3 strict");
|
||||
assert!(
|
||||
!correction_visible("active", 0.3),
|
||||
"confidence > 0.3 strict"
|
||||
);
|
||||
assert!(!correction_visible("archived", 0.9));
|
||||
assert!(habit_visible("candidate", 0.31));
|
||||
assert!(habit_visible("active", 0.31));
|
||||
@@ -305,19 +348,38 @@ fn test_visibility_gates_and_list_counts() {
|
||||
#[test]
|
||||
fn test_match_predicates() {
|
||||
assert!(correction_matches("deploy", "shell", "deploy to shell now"));
|
||||
assert!(!correction_matches("deploy", "editor", "deploy to shell now"),
|
||||
"domain miss filters");
|
||||
assert!(!correction_matches("editor", "shell", "deploy to shell now"),
|
||||
"pattern miss filters");
|
||||
assert!(!correction_matches("([unclosed", "", "anything"),
|
||||
"invalid regex skips");
|
||||
assert!(correction_matches("", "", "anything"), "empty filters never block");
|
||||
assert!(correction_matches("", "SHELL", "deploy to shell now"),
|
||||
"domain case-insensitive");
|
||||
assert!(habit_matches("deploy", "DEPLOY the thing"), "trigger IGNORECASE");
|
||||
assert!(!habit_matches("test (unit|e2e)", "run the unit tests"),
|
||||
"unescaped parens match nothing (frozen)");
|
||||
assert!(!habit_matches("([broken", "anything"), "invalid trigger skips");
|
||||
assert!(
|
||||
!correction_matches("deploy", "editor", "deploy to shell now"),
|
||||
"domain miss filters"
|
||||
);
|
||||
assert!(
|
||||
!correction_matches("editor", "shell", "deploy to shell now"),
|
||||
"pattern miss filters"
|
||||
);
|
||||
assert!(
|
||||
!correction_matches("([unclosed", "", "anything"),
|
||||
"invalid regex skips"
|
||||
);
|
||||
assert!(
|
||||
correction_matches("", "", "anything"),
|
||||
"empty filters never block"
|
||||
);
|
||||
assert!(
|
||||
correction_matches("", "SHELL", "deploy to shell now"),
|
||||
"domain case-insensitive"
|
||||
);
|
||||
assert!(
|
||||
habit_matches("deploy", "DEPLOY the thing"),
|
||||
"trigger IGNORECASE"
|
||||
);
|
||||
assert!(
|
||||
!habit_matches("test (unit|e2e)", "run the unit tests"),
|
||||
"unescaped parens match nothing (frozen)"
|
||||
);
|
||||
assert!(
|
||||
!habit_matches("([broken", "anything"),
|
||||
"invalid trigger skips"
|
||||
);
|
||||
}
|
||||
|
||||
/// scope_tags parse fallback: valid JSON parses, corrupt input → {}.
|
||||
@@ -359,10 +421,17 @@ fn test_habit_update_dict_shape() {
|
||||
#[test]
|
||||
fn test_correction_update_dict_shape() {
|
||||
// failure-archives: confidence 0.5 → 0.2, evidence_count UNCHANGED.
|
||||
let upd = corr_upd(&serde_json::json!({"confidence": 0.5, "evidence_count": 1}), "failure");
|
||||
let upd = corr_upd(
|
||||
&serde_json::json!({"confidence": 0.5, "evidence_count": 1}),
|
||||
"failure",
|
||||
);
|
||||
assert_eq!(upd["state"].as_str(), Some("archived"));
|
||||
assert_eq!(upd["confidence"].as_f64(), Some(0.2));
|
||||
assert_eq!(upd["evidence_count"].as_f64(), Some(1.0), "failure keeps count");
|
||||
assert_eq!(
|
||||
upd["evidence_count"].as_f64(),
|
||||
Some(1.0),
|
||||
"failure keeps count"
|
||||
);
|
||||
}
|
||||
|
||||
// ── gap-close surfaces ───────────────────────────────────────────────────
|
||||
@@ -382,9 +451,19 @@ fn test_connect_schema() {
|
||||
let frozen_schema = frozen["schema"].as_array().unwrap();
|
||||
assert_eq!(spec.schema.len(), frozen_schema.len(), "schema stmt count");
|
||||
for (stmt, want) in spec.schema.iter().zip(frozen_schema) {
|
||||
assert_eq!(stmt.kind, want["kind"].as_str().unwrap(), "{} kind", stmt.name);
|
||||
assert_eq!(
|
||||
stmt.kind,
|
||||
want["kind"].as_str().unwrap(),
|
||||
"{} kind",
|
||||
stmt.name
|
||||
);
|
||||
assert_eq!(stmt.name, want["name"].as_str().unwrap(), "stmt name");
|
||||
assert_eq!(stmt.sql, want["sql"].as_str().unwrap(), "{} DDL verbatim", stmt.name);
|
||||
assert_eq!(
|
||||
stmt.sql,
|
||||
want["sql"].as_str().unwrap(),
|
||||
"{} DDL verbatim",
|
||||
stmt.name
|
||||
);
|
||||
}
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
let key = "RUNE_MEMORY_DIR";
|
||||
@@ -430,7 +509,10 @@ fn test_python_predicates_matrix() {
|
||||
let p = &t["cases"]["predicates"];
|
||||
let n_corr = p["corrections"].as_array().unwrap().len();
|
||||
let n_habit = p["habits"].as_array().unwrap().len();
|
||||
assert!(n_corr >= 40 && n_habit >= 30, "matrix is full ({n_corr}+{n_habit})");
|
||||
assert!(
|
||||
n_corr >= 40 && n_habit >= 30,
|
||||
"matrix is full ({n_corr}+{n_habit})"
|
||||
);
|
||||
for c in p["corrections"].as_array().unwrap() {
|
||||
let row = corr_row_full(&c["row"]);
|
||||
let ctx = c["context"].as_str().unwrap();
|
||||
@@ -439,7 +521,8 @@ fn test_python_predicates_matrix() {
|
||||
_correction_matches_python(&row, ctx, ctx_low),
|
||||
c["matched"].as_bool().unwrap(),
|
||||
"correction {} vs {:?}",
|
||||
row.id, ctx
|
||||
row.id,
|
||||
ctx
|
||||
);
|
||||
}
|
||||
for h in p["habits"].as_array().unwrap() {
|
||||
@@ -449,7 +532,8 @@ fn test_python_predicates_matrix() {
|
||||
_habit_matches_python(&row, ctx),
|
||||
h["matched"].as_bool().unwrap(),
|
||||
"habit {} vs {:?}",
|
||||
row.id, ctx
|
||||
row.id,
|
||||
ctx
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -468,25 +552,57 @@ fn test_reinforce_plan_replay() {
|
||||
let item_type = case["item_type"].as_str().unwrap();
|
||||
let n = case["n"].as_i64().unwrap();
|
||||
if case["pre_row"].is_null() {
|
||||
let id = if name == "habit-not-found" { 99991 } else { 99992 };
|
||||
let id = if name == "habit-not-found" {
|
||||
99991
|
||||
} else {
|
||||
99992
|
||||
};
|
||||
let plan = _reinforce_rust(id, outcome, item_type, None, "ts");
|
||||
assert_eq!(plan.result, case["results"][0], "{name}: not-found envelope");
|
||||
assert!(plan.writeback.is_null() && plan.activation.is_none(), "{name}");
|
||||
assert_eq!(
|
||||
plan.result, case["results"][0],
|
||||
"{name}: not-found envelope"
|
||||
);
|
||||
assert!(
|
||||
plan.writeback.is_null() && plan.activation.is_none(),
|
||||
"{name}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let ts = case["pre_row"]["created_at"].as_str().unwrap();
|
||||
let mut cur = case["pre_row"].clone();
|
||||
for i in 0..n as usize {
|
||||
let plan = _reinforce_rust(case["pre_row"]["id"].as_i64().unwrap(), outcome, item_type, Some(&cur), ts);
|
||||
let twin = _reinforce_python(case["pre_row"]["id"].as_i64().unwrap(), outcome, item_type, Some(&cur), ts);
|
||||
let plan = _reinforce_rust(
|
||||
case["pre_row"]["id"].as_i64().unwrap(),
|
||||
outcome,
|
||||
item_type,
|
||||
Some(&cur),
|
||||
ts,
|
||||
);
|
||||
let twin = _reinforce_python(
|
||||
case["pre_row"]["id"].as_i64().unwrap(),
|
||||
outcome,
|
||||
item_type,
|
||||
Some(&cur),
|
||||
ts,
|
||||
);
|
||||
assert_eq!(plan.result, twin.result, "{name}: step {i} halves coincide");
|
||||
assert_eq!(plan.writeback, twin.writeback, "{name}: step {i} writebacks coincide");
|
||||
assert_eq!(plan.activation, twin.activation, "{name}: step {i} activation rows coincide");
|
||||
assert_eq!(
|
||||
plan.writeback, twin.writeback,
|
||||
"{name}: step {i} writebacks coincide"
|
||||
);
|
||||
assert_eq!(
|
||||
plan.activation, twin.activation,
|
||||
"{name}: step {i} activation rows coincide"
|
||||
);
|
||||
assert_eq!(plan.result, case["results"][i], "{name}: step {i} result");
|
||||
if item_type == "habit" {
|
||||
let act = plan.activation.as_ref().unwrap();
|
||||
assert_eq!(act["activated_at"].as_str(), Some(ts), "{name}: ts pinned");
|
||||
assert_eq!(act["context"].as_str(), Some(""), "{name}: logged context ''");
|
||||
assert_eq!(
|
||||
act["context"].as_str(),
|
||||
Some(""),
|
||||
"{name}: logged context ''"
|
||||
);
|
||||
}
|
||||
// thread the write-back (the oracle's UPDATE)
|
||||
for (k, v) in plan.writeback.as_object().unwrap() {
|
||||
@@ -520,10 +636,17 @@ fn test_list_items_replay() {
|
||||
// tie law: the two 0.85 corrections appear in rowid order behind the
|
||||
// single 1.0 one.
|
||||
let all = list_items(&corrections, &habits, "all", 20);
|
||||
let ids: Vec<i64> = all["corrections"].as_array().unwrap()
|
||||
.iter().map(|c| c["id"].as_i64().unwrap()).collect();
|
||||
let ids: Vec<i64> = all["corrections"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c["id"].as_i64().unwrap())
|
||||
.collect();
|
||||
let pos = |id: i64| ids.iter().position(|x| *x == id).unwrap();
|
||||
assert!(pos(11) < pos(1) && pos(1) < pos(2), "confidence order then rowid ties: {ids:?}");
|
||||
assert!(
|
||||
pos(11) < pos(1) && pos(1) < pos(2),
|
||||
"confidence order then rowid ties: {ids:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// behavior_stats over the frozen row collections, both captured stats
|
||||
@@ -538,7 +661,10 @@ fn test_behavior_stats_replay() {
|
||||
let activations = sf["activations"].as_i64().unwrap();
|
||||
let got = behavior_stats(&corrections, &habits, activations);
|
||||
assert_eq!(got, sf["stats"], "stats_full counts");
|
||||
assert_eq!(got, t["cases"]["stats"][0]["expected"], "matrix-time counts");
|
||||
assert_eq!(
|
||||
got, t["cases"]["stats"][0]["expected"],
|
||||
"matrix-time counts"
|
||||
);
|
||||
}
|
||||
|
||||
/// bh_pure_truth.json (freeze_truth envelope): the pure-fn corpus the
|
||||
@@ -557,7 +683,11 @@ fn test_pure_truth_corpus() {
|
||||
"_correction_matches_python" => {
|
||||
let row = corr_row_full(&args[0]);
|
||||
assert_eq!(
|
||||
_correction_matches_python(&row, args[1].as_str().unwrap(), args[2].as_str().unwrap()),
|
||||
_correction_matches_python(
|
||||
&row,
|
||||
args[1].as_str().unwrap(),
|
||||
args[2].as_str().unwrap()
|
||||
),
|
||||
c["expected"].as_bool().unwrap()
|
||||
);
|
||||
}
|
||||
@@ -571,7 +701,12 @@ fn test_pure_truth_corpus() {
|
||||
"reinforce" => {
|
||||
let item_type = c["kwargs"]["item_type"].as_str().unwrap();
|
||||
let plan = _reinforce_rust(
|
||||
args[0].as_i64().unwrap(), args[1].as_str().unwrap(), item_type, None, "ts");
|
||||
args[0].as_i64().unwrap(),
|
||||
args[1].as_str().unwrap(),
|
||||
item_type,
|
||||
None,
|
||||
"ts",
|
||||
);
|
||||
assert_eq!(plan.result, c["expected"]);
|
||||
}
|
||||
"_bh_core" => {
|
||||
|
||||
@@ -349,7 +349,10 @@ pub struct SchemaStmt {
|
||||
}
|
||||
|
||||
const SCHEMA: [SchemaStmt; 7] = [
|
||||
SchemaStmt { kind: "table", name: "corrections", sql: r#"CREATE TABLE corrections (
|
||||
SchemaStmt {
|
||||
kind: "table",
|
||||
name: "corrections",
|
||||
sql: r#"CREATE TABLE corrections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
original_behavior TEXT,
|
||||
corrected_behavior TEXT NOT NULL,
|
||||
@@ -362,8 +365,12 @@ const SCHEMA: [SchemaStmt; 7] = [
|
||||
created_at TEXT NOT NULL,
|
||||
last_reinforced TEXT,
|
||||
supersedes_id INTEGER
|
||||
)"# },
|
||||
SchemaStmt { kind: "table", name: "habits", sql: r#"CREATE TABLE habits (
|
||||
)"#,
|
||||
},
|
||||
SchemaStmt {
|
||||
kind: "table",
|
||||
name: "habits",
|
||||
sql: r#"CREATE TABLE habits (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT DEFAULT '',
|
||||
trigger_pattern TEXT NOT NULL,
|
||||
@@ -378,19 +385,40 @@ const SCHEMA: [SchemaStmt; 7] = [
|
||||
state TEXT DEFAULT 'candidate',
|
||||
created_at TEXT NOT NULL,
|
||||
last_activated TEXT
|
||||
)"# },
|
||||
SchemaStmt { kind: "table", name: "habit_activations", sql: r#"CREATE TABLE habit_activations (
|
||||
)"#,
|
||||
},
|
||||
SchemaStmt {
|
||||
kind: "table",
|
||||
name: "habit_activations",
|
||||
sql: r#"CREATE TABLE habit_activations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
habit_id INTEGER REFERENCES habits(id),
|
||||
context TEXT NOT NULL,
|
||||
outcome TEXT DEFAULT 'neutral',
|
||||
feedback TEXT DEFAULT '',
|
||||
activated_at TEXT NOT NULL
|
||||
)"# },
|
||||
SchemaStmt { kind: "index", name: "idx_corrections_state", sql: r#"CREATE INDEX idx_corrections_state ON corrections(state)"# },
|
||||
SchemaStmt { kind: "index", name: "idx_corrections_domain", sql: r#"CREATE INDEX idx_corrections_domain ON corrections(scope_domain)"# },
|
||||
SchemaStmt { kind: "index", name: "idx_habits_state", sql: r#"CREATE INDEX idx_habits_state ON habits(state)"# },
|
||||
SchemaStmt { kind: "index", name: "idx_habits_trigger", sql: r#"CREATE INDEX idx_habits_trigger ON habits(trigger_pattern)"# },
|
||||
)"#,
|
||||
},
|
||||
SchemaStmt {
|
||||
kind: "index",
|
||||
name: "idx_corrections_state",
|
||||
sql: r#"CREATE INDEX idx_corrections_state ON corrections(state)"#,
|
||||
},
|
||||
SchemaStmt {
|
||||
kind: "index",
|
||||
name: "idx_corrections_domain",
|
||||
sql: r#"CREATE INDEX idx_corrections_domain ON corrections(scope_domain)"#,
|
||||
},
|
||||
SchemaStmt {
|
||||
kind: "index",
|
||||
name: "idx_habits_state",
|
||||
sql: r#"CREATE INDEX idx_habits_state ON habits(state)"#,
|
||||
},
|
||||
SchemaStmt {
|
||||
kind: "index",
|
||||
name: "idx_habits_trigger",
|
||||
sql: r#"CREATE INDEX idx_habits_trigger ON habits(trigger_pattern)"#,
|
||||
},
|
||||
];
|
||||
|
||||
/// connect()'s deterministic content as data: where the database lives,
|
||||
@@ -407,7 +435,11 @@ pub struct ConnectSpec {
|
||||
/// `connect`): the decision surface — path resolution, WAL journal
|
||||
/// mode, the full schema — returned as data for the facade to apply.
|
||||
pub fn connect() -> ConnectSpec {
|
||||
ConnectSpec { path: db_path(), journal_mode: "wal", schema: SCHEMA }
|
||||
ConnectSpec {
|
||||
path: db_path(),
|
||||
journal_mode: "wal",
|
||||
schema: SCHEMA,
|
||||
}
|
||||
}
|
||||
|
||||
// ── the Python-pin predicates (the oracle's inline fallbacks) ────────────
|
||||
@@ -419,8 +451,7 @@ pub fn connect() -> ConnectSpec {
|
||||
/// caller; passing anything else is a caller bug).
|
||||
pub fn _correction_matches_python(row: &CorrectionRow, context: &str, context_low: &str) -> bool {
|
||||
scope_pattern_matches(&row.scope_pattern, context)
|
||||
&& (row.scope_domain.is_empty()
|
||||
|| context_low.contains(&row.scope_domain.to_lowercase()))
|
||||
&& (row.scope_domain.is_empty() || context_low.contains(&row.scope_domain.to_lowercase()))
|
||||
}
|
||||
|
||||
/// The inline per-row habit predicate (name-preserving port of
|
||||
@@ -630,7 +661,11 @@ pub fn list_items(
|
||||
/// Return behavioral pattern statistics (name-preserving port of
|
||||
/// `behavior_stats`): the five counters over the row collections; the
|
||||
/// activation count is passed in (its rows aren't decision input).
|
||||
pub fn behavior_stats(corrections: &[CorrectionRow], habits: &[HabitRow], activations: i64) -> serde_json::Value {
|
||||
pub fn behavior_stats(
|
||||
corrections: &[CorrectionRow],
|
||||
habits: &[HabitRow],
|
||||
activations: i64,
|
||||
) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"corrections_active":
|
||||
corrections.iter().filter(|c| c.state == "active").count() as i64,
|
||||
@@ -711,7 +746,9 @@ fn str_field(row: &serde_json::Value, key: &str) -> String {
|
||||
|
||||
/// bh_habit_reinforce({row, outcome}) -> the update dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bh_habit_reinforce(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
pub extern "C" fn bh_habit_reinforce(
|
||||
arg: *const std::os::raw::c_char,
|
||||
) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let row_v = a.get("row").cloned().unwrap_or(serde_json::json!({}));
|
||||
let outcome = str_field(&a, "outcome");
|
||||
@@ -800,7 +837,9 @@ pub extern "C" fn bh_core_probe() -> *mut std::os::raw::c_char {
|
||||
/// bh_python_predicates({kind, row, context, context_low?}) -> bool.
|
||||
/// kind: "correction" (context_low defaults to context.lower()) | "habit".
|
||||
#[no_mangle]
|
||||
pub extern "C" fn bh_python_predicates(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
pub extern "C" fn bh_python_predicates(
|
||||
arg: *const std::os::raw::c_char,
|
||||
) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let kind = str_field(&a, "kind");
|
||||
let row_v = a.get("row").cloned().unwrap_or(serde_json::json!({}));
|
||||
@@ -814,7 +853,11 @@ pub extern "C" fn bh_python_predicates(arg: *const std::os::raw::c_char) -> *mut
|
||||
_ => context.to_lowercase(),
|
||||
};
|
||||
let row: CorrectionRow = serde_json::from_value(row_v).map_err(|e| e.to_string())?;
|
||||
Ok(serde_json::json!(_correction_matches_python(&row, &context, &context_low)))
|
||||
Ok(serde_json::json!(_correction_matches_python(
|
||||
&row,
|
||||
&context,
|
||||
&context_low
|
||||
)))
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -844,13 +887,14 @@ pub extern "C" fn bh_reinforce_plan(arg: *const std::os::raw::c_char) -> *mut st
|
||||
pub extern "C" fn bh_list_items(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let corrections: Vec<CorrectionRow> = serde_json::from_value(
|
||||
a.get("corrections").cloned().unwrap_or(serde_json::json!([])),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let habits: Vec<HabitRow> = serde_json::from_value(
|
||||
a.get("habits").cloned().unwrap_or(serde_json::json!([])),
|
||||
a.get("corrections")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::json!([])),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let habits: Vec<HabitRow> =
|
||||
serde_json::from_value(a.get("habits").cloned().unwrap_or(serde_json::json!([])))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let item_type = str_field(&a, "item_type");
|
||||
let limit = num(&a, "limit", 20.0) as i64;
|
||||
Ok(list_items(&corrections, &habits, &item_type, limit))
|
||||
@@ -862,13 +906,14 @@ pub extern "C" fn bh_list_items(arg: *const std::os::raw::c_char) -> *mut std::o
|
||||
pub extern "C" fn bh_behavior_stats(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let corrections: Vec<CorrectionRow> = serde_json::from_value(
|
||||
a.get("corrections").cloned().unwrap_or(serde_json::json!([])),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let habits: Vec<HabitRow> = serde_json::from_value(
|
||||
a.get("habits").cloned().unwrap_or(serde_json::json!([])),
|
||||
a.get("corrections")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::json!([])),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let habits: Vec<HabitRow> =
|
||||
serde_json::from_value(a.get("habits").cloned().unwrap_or(serde_json::json!([])))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let activations = num(&a, "activations", 0.0) as i64;
|
||||
Ok(behavior_stats(&corrections, &habits, activations))
|
||||
}))
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
|
||||
let default_path = PathBuf::from(&manifest)
|
||||
.join("../../../verification/build-id/boundary.json");
|
||||
let default_path =
|
||||
PathBuf::from(&manifest).join("../../../verification/build-id/boundary.json");
|
||||
let path = std::env::var("RUNE_BUILD_ID_FILE")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(default_path);
|
||||
|
||||
@@ -37,10 +37,10 @@ pub const BUILD_ID: &str = match option_env!("RUNE_BUILD_ID") {
|
||||
None => "unset",
|
||||
};
|
||||
|
||||
/// Schema: schemas/boundary/v1/error.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Error {
|
||||
/// Schema: schemas/boundary/v1/error.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Error {
|
||||
pub v: u32,
|
||||
pub r#type: String,
|
||||
pub msg_id: String,
|
||||
@@ -50,12 +50,12 @@ pub const BUILD_ID: &str = match option_env!("RUNE_BUILD_ID") {
|
||||
pub detail: String,
|
||||
pub task_id: String,
|
||||
pub recoverable: bool,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema: schemas/boundary/v1/log_line.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LogLine {
|
||||
/// Schema: schemas/boundary/v1/log_line.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LogLine {
|
||||
pub v: u32,
|
||||
pub r#type: String,
|
||||
pub msg_id: String,
|
||||
@@ -64,46 +64,46 @@ pub const BUILD_ID: &str = match option_env!("RUNE_BUILD_ID") {
|
||||
pub stream: String,
|
||||
pub seq: u64,
|
||||
pub text: String,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema: schemas/boundary/v1/ping.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Ping {
|
||||
/// Schema: schemas/boundary/v1/ping.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Ping {
|
||||
pub v: u32,
|
||||
pub r#type: String,
|
||||
pub msg_id: String,
|
||||
pub ts: String,
|
||||
pub nonce: String,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema: schemas/boundary/v1/pong.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Pong {
|
||||
/// Schema: schemas/boundary/v1/pong.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Pong {
|
||||
pub v: u32,
|
||||
pub r#type: String,
|
||||
pub msg_id: String,
|
||||
pub ts: String,
|
||||
pub nonce: String,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema: schemas/boundary/v1/shutdown.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Shutdown {
|
||||
/// Schema: schemas/boundary/v1/shutdown.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Shutdown {
|
||||
pub v: u32,
|
||||
pub r#type: String,
|
||||
pub msg_id: String,
|
||||
pub ts: String,
|
||||
pub reason: String,
|
||||
pub graceful: bool,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema: schemas/boundary/v1/status_snapshot.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StatusSnapshot {
|
||||
/// Schema: schemas/boundary/v1/status_snapshot.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StatusSnapshot {
|
||||
pub v: u32,
|
||||
pub r#type: String,
|
||||
pub msg_id: String,
|
||||
@@ -114,12 +114,12 @@ pub const BUILD_ID: &str = match option_env!("RUNE_BUILD_ID") {
|
||||
pub tasks_pending: u64,
|
||||
pub tasks_running: u64,
|
||||
pub tasks_completed: u64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema: schemas/boundary/v1/task_result.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TaskResult {
|
||||
/// Schema: schemas/boundary/v1/task_result.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TaskResult {
|
||||
pub v: u32,
|
||||
pub r#type: String,
|
||||
pub msg_id: String,
|
||||
@@ -129,12 +129,12 @@ pub const BUILD_ID: &str = match option_env!("RUNE_BUILD_ID") {
|
||||
pub summary: String,
|
||||
pub evidence_refs: Vec<String>,
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema: schemas/boundary/v1/task_start.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TaskStart {
|
||||
/// Schema: schemas/boundary/v1/task_start.schema.json (v1)
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TaskStart {
|
||||
pub v: u32,
|
||||
pub r#type: String,
|
||||
pub msg_id: String,
|
||||
@@ -143,7 +143,7 @@ pub const BUILD_ID: &str = match option_env!("RUNE_BUILD_ID") {
|
||||
pub objective: String,
|
||||
pub lane: String,
|
||||
pub tools_allowed: Vec<String>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BoundaryError {
|
||||
@@ -153,16 +153,21 @@ pub enum BoundaryError {
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for BoundaryError {
|
||||
fn from(e: serde_json::Error) -> Self { BoundaryError::Parse(e) }
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
BoundaryError::Parse(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BoundaryError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
BoundaryError::Parse(e) => write!(f, "boundary: parse error: {}", e),
|
||||
BoundaryError::UnknownMessageType(t) => write!(f, "boundary: unknown message type {:?}", t),
|
||||
BoundaryError::VersionMismatch { expected, got } => write!(
|
||||
f, "boundary: wire version {} != {}", got, expected),
|
||||
BoundaryError::UnknownMessageType(t) => {
|
||||
write!(f, "boundary: unknown message type {:?}", t)
|
||||
}
|
||||
BoundaryError::VersionMismatch { expected, got } => {
|
||||
write!(f, "boundary: wire version {} != {}", got, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,17 +191,32 @@ pub fn roundtrip_canonical(line: &str) -> Result<String, BoundaryError> {
|
||||
.to_string();
|
||||
let v = value.get("v").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||
if v != u64::from(BOUNDARY_WIRE_VERSION) {
|
||||
return Err(BoundaryError::VersionMismatch { expected: BOUNDARY_WIRE_VERSION, got: v });
|
||||
return Err(BoundaryError::VersionMismatch {
|
||||
expected: BOUNDARY_WIRE_VERSION,
|
||||
got: v,
|
||||
});
|
||||
}
|
||||
match t.as_str() {
|
||||
"error" => canonical(&serde_json::from_value::<Error>(value).map_err(BoundaryError::Parse)?),
|
||||
"log_line" => canonical(&serde_json::from_value::<LogLine>(value).map_err(BoundaryError::Parse)?),
|
||||
"error" => {
|
||||
canonical(&serde_json::from_value::<Error>(value).map_err(BoundaryError::Parse)?)
|
||||
}
|
||||
"log_line" => {
|
||||
canonical(&serde_json::from_value::<LogLine>(value).map_err(BoundaryError::Parse)?)
|
||||
}
|
||||
"ping" => canonical(&serde_json::from_value::<Ping>(value).map_err(BoundaryError::Parse)?),
|
||||
"pong" => canonical(&serde_json::from_value::<Pong>(value).map_err(BoundaryError::Parse)?),
|
||||
"shutdown" => canonical(&serde_json::from_value::<Shutdown>(value).map_err(BoundaryError::Parse)?),
|
||||
"status_snapshot" => canonical(&serde_json::from_value::<StatusSnapshot>(value).map_err(BoundaryError::Parse)?),
|
||||
"task_result" => canonical(&serde_json::from_value::<TaskResult>(value).map_err(BoundaryError::Parse)?),
|
||||
"task_start" => canonical(&serde_json::from_value::<TaskStart>(value).map_err(BoundaryError::Parse)?),
|
||||
"shutdown" => {
|
||||
canonical(&serde_json::from_value::<Shutdown>(value).map_err(BoundaryError::Parse)?)
|
||||
}
|
||||
"status_snapshot" => canonical(
|
||||
&serde_json::from_value::<StatusSnapshot>(value).map_err(BoundaryError::Parse)?,
|
||||
),
|
||||
"task_result" => {
|
||||
canonical(&serde_json::from_value::<TaskResult>(value).map_err(BoundaryError::Parse)?)
|
||||
}
|
||||
"task_start" => {
|
||||
canonical(&serde_json::from_value::<TaskStart>(value).map_err(BoundaryError::Parse)?)
|
||||
}
|
||||
other => Err(BoundaryError::UnknownMessageType(other.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -220,7 +240,8 @@ mod tests {
|
||||
fn wrong_version_is_rejected() {
|
||||
assert!(roundtrip_canonical(
|
||||
r#"{"v":2,"type":"ping","msg_id":"m","ts":"2026-01-01T00:00:00Z","nonce":"n"}"#,
|
||||
).is_err());
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn corpus_dir() -> PathBuf {
|
||||
std::env::var("BOUNDARY_CORPUS_DIR").map(PathBuf::from).unwrap_or_else(|_| {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../../schemas/boundary/corpus")
|
||||
})
|
||||
std::env::var("BOUNDARY_CORPUS_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../schemas/boundary/corpus")
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -25,8 +26,8 @@ fn corpus_roundtrips_canonically() {
|
||||
for name in &names {
|
||||
let text = std::fs::read_to_string(dir.join(name)).expect("corpus file");
|
||||
let line = text.trim_end();
|
||||
let out = boundary_types::roundtrip_canonical(line)
|
||||
.unwrap_or_else(|e| panic!("{}: {}", name, e));
|
||||
let out =
|
||||
boundary_types::roundtrip_canonical(line).unwrap_or_else(|e| panic!("{}: {}", name, e));
|
||||
assert_eq!(out, line, "canonical round-trip drift in {}", name);
|
||||
}
|
||||
}
|
||||
@@ -40,9 +41,9 @@ fn corpus_covers_every_schema() {
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.filter(|n| n.ends_with(".json"))
|
||||
.filter_map(|n| {
|
||||
std::fs::read_to_string(dir.join(&n)).ok().and_then(|t| {
|
||||
serde_json::from_str::<serde_json::Value>(&t).ok()
|
||||
})
|
||||
std::fs::read_to_string(dir.join(&n))
|
||||
.ok()
|
||||
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
|
||||
})
|
||||
.filter_map(|v| v.get("type").and_then(|t| t.as_str().map(String::from)))
|
||||
.collect();
|
||||
|
||||
@@ -21,8 +21,14 @@ pub const MAX_SELECTED: usize = 10;
|
||||
/// Task-family keyword table (rune_operator _TASK_FAMILY_KEYWORDS, verbatim
|
||||
/// order — first match wins).
|
||||
const TASK_FAMILY_KEYWORDS: &[(&str, &[&str])] = &[
|
||||
("code", &["fix", "code", "debug", "test", "refactor", "implement"]),
|
||||
("research", &["design", "architecture", "research", "analyze"]),
|
||||
(
|
||||
"code",
|
||||
&["fix", "code", "debug", "test", "refactor", "implement"],
|
||||
),
|
||||
(
|
||||
"research",
|
||||
&["design", "architecture", "research", "analyze"],
|
||||
),
|
||||
("chat", &["hello", "hi", "thanks", "status", "summarize"]),
|
||||
];
|
||||
|
||||
@@ -40,7 +46,6 @@ fn as_str(v: Option<&Json>) -> &str {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// rune_operator.infer_task_family port (first-match keyword scan).
|
||||
pub fn infer_task_family(description: &str) -> &str {
|
||||
let d = description.to_lowercase();
|
||||
@@ -119,16 +124,16 @@ fn scored_tools(cards: &[Json], schemas: &Json, description: &str) -> Vec<(f64,
|
||||
scored.push((score, name.to_string()));
|
||||
}
|
||||
}
|
||||
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal).then(a.1.cmp(&b.1)));
|
||||
scored.sort_by(|a, b| {
|
||||
b.0.partial_cmp(&a.0)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then(a.1.cmp(&b.1))
|
||||
});
|
||||
scored
|
||||
}
|
||||
|
||||
/// select_for_job port. Returns {tools, names, reason, task_family}.
|
||||
pub fn select_for_job(
|
||||
tools: &[Json],
|
||||
description: &str,
|
||||
need_tools: bool,
|
||||
) -> Json {
|
||||
pub fn select_for_job(tools: &[Json], description: &str, need_tools: bool) -> Json {
|
||||
let schemas = Json::Obj(
|
||||
tools
|
||||
.iter()
|
||||
@@ -170,21 +175,32 @@ pub fn select_for_job(
|
||||
|
||||
// Explicit shell request promotes run_command to the front.
|
||||
let d_lower = description.to_lowercase();
|
||||
let is_shell = ["bash", "shell", "terminal"]
|
||||
.iter()
|
||||
.any(|w| {
|
||||
// \b word boundary approximation: check with non-alnum neighbors
|
||||
if let Some(pos) = d_lower.find(w) {
|
||||
let before = d_lower[..pos].chars().last().map(|c| c.is_ascii_alphanumeric()).unwrap_or(false);
|
||||
let after = d_lower[pos + w.len()..].chars().next().map(|c| c.is_ascii_alphanumeric()).unwrap_or(false);
|
||||
!before && !after
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
let is_shell = ["bash", "shell", "terminal"].iter().any(|w| {
|
||||
// \b word boundary approximation: check with non-alnum neighbors
|
||||
if let Some(pos) = d_lower.find(w) {
|
||||
let before = d_lower[..pos]
|
||||
.chars()
|
||||
.last()
|
||||
.map(|c| c.is_ascii_alphanumeric())
|
||||
.unwrap_or(false);
|
||||
let after = d_lower[pos + w.len()..]
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_alphanumeric())
|
||||
.unwrap_or(false);
|
||||
!before && !after
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if is_shell && obj_get(&schemas, "run_command").is_some() {
|
||||
let mut reordered = vec!["run_command".to_string()];
|
||||
reordered.extend(matches.iter().filter(|(_, n)| n != "run_command").map(|(_, n)| n.clone()));
|
||||
reordered.extend(
|
||||
matches
|
||||
.iter()
|
||||
.filter(|(_, n)| n != "run_command")
|
||||
.map(|(_, n)| n.clone()),
|
||||
);
|
||||
matches = reordered.into_iter().map(|n| (1.0, n)).collect();
|
||||
}
|
||||
|
||||
@@ -201,7 +217,11 @@ pub fn select_for_job(
|
||||
format!(
|
||||
"floor + {} match(es): {}",
|
||||
matches.len(),
|
||||
matches.iter().map(|(_, n)| n.as_str()).collect::<Vec<_>>().join(", ")
|
||||
matches
|
||||
.iter()
|
||||
.map(|(_, n)| n.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
} else {
|
||||
"floor (no matches)".to_string()
|
||||
@@ -224,12 +244,7 @@ pub fn select_for_job(
|
||||
}
|
||||
|
||||
/// find_capability port. Returns [{name, description, risk, parameters?}].
|
||||
pub fn find_capability(
|
||||
tools: &[Json],
|
||||
cards: &[Json],
|
||||
query: &str,
|
||||
top_k: usize,
|
||||
) -> Json {
|
||||
pub fn find_capability(tools: &[Json], cards: &[Json], query: &str, top_k: usize) -> Json {
|
||||
let schemas = Json::Obj(
|
||||
tools
|
||||
.iter()
|
||||
@@ -246,10 +261,7 @@ pub fn find_capability(
|
||||
let mut scored: Vec<(f64, String, &Json)> = vec![];
|
||||
for card in cards {
|
||||
let name = as_str(obj_get(card, "name"));
|
||||
if name.is_empty()
|
||||
|| FLOOR_TOOLS.contains(&name)
|
||||
|| name == ESCAPE_HATCH_TOOL
|
||||
{
|
||||
if name.is_empty() || FLOOR_TOOLS.contains(&name) || name == ESCAPE_HATCH_TOOL {
|
||||
continue;
|
||||
}
|
||||
let score = score_card(card, query);
|
||||
@@ -257,7 +269,11 @@ pub fn find_capability(
|
||||
scored.push((score, name.to_string(), card));
|
||||
}
|
||||
}
|
||||
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal).then(a.1.cmp(&b.1)));
|
||||
scored.sort_by(|a, b| {
|
||||
b.0.partial_cmp(&a.0)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then(a.1.cmp(&b.1))
|
||||
});
|
||||
|
||||
let mut out: Vec<Json> = vec![];
|
||||
for (_, name, card) in scored.iter().take(top_k) {
|
||||
|
||||
@@ -28,38 +28,245 @@ use std::fmt::Write as _;
|
||||
|
||||
/// SEMANTIC_EXAMPLES — the oracle dict, insertion order preserved.
|
||||
pub const SEMANTIC_EXAMPLES: &[(&str, &[&str])] = &[
|
||||
("memory_recall", &["What do I remember about X?", "Have we discussed this before?", "Find previous decisions about Y.", "What do I know about Z?"]),
|
||||
("memory_write", &["Remember this fact.", "Store this for later.", "Save this decision."]),
|
||||
("memory_search", &["Search my memory for X.", "Look up anything about Y."]),
|
||||
("claim_verification", &["Is this claim supported?", "Verify this statement.", "Check if this is accurate."]),
|
||||
("warm_routing", &["What context is relevant?", "Prime my memory for this topic."]),
|
||||
("persona_compile", &["Who am I?", "What are my values?", "Load my identity."]),
|
||||
("drift_detection", &["Am I drifting from my identity?", "Check for persona drift."]),
|
||||
("growth_extraction", &["What should I learn from this?", "Extract behavioral deltas."]),
|
||||
("harness_validation", &["Is my harness compliant?", "Validate my runtime."]),
|
||||
("epub", &["Read this ebook.", "Extract text from this epub."]),
|
||||
("tasks", &["What are my current tasks?", "Persist this task.", "Show my task list."]),
|
||||
("politics", &["What do I know about this political topic?", "Absorb this political understanding."]),
|
||||
("abstraction", &["Extract facts from this text.", "What observations can you make?", "Synthesize knowledge from this.", "What patterns do you see?"]),
|
||||
("temporal", &["What was true then vs now?", "When did this change?", "Is this still current?", "Show me the history of this."]),
|
||||
("corrections", &["I corrected you on this before.", "Don't do that again.", "Remember this correction."]),
|
||||
("habits", &["You should do this automatically.", "Make this a habit.", "Remember this pattern."]),
|
||||
("cache", &["Cache this result.", "Is this cached?", "Clear the cache."]),
|
||||
("events", &["What events have happened?", "Show me the event log.", "What changed recently?"]),
|
||||
("benchmark", &["Run the benchmark.", "Test the system.", "Check for regressions."]),
|
||||
("identity", &["Sign this file.", "Verify this signature.", "Show my agent ID."]),
|
||||
("agent_profiles", &["Switch to blank agent.", "Create a test profile.", "Restore Rune profile."]),
|
||||
("voice_sync", &["Sync the voice.", "Update the system prompt.", "Is the prompt in sync?"]),
|
||||
("prompt_sync", &["Sync the system prompt.", "Derive prompt from store.", "Is the prompt current?"]),
|
||||
("context_pressure", &["How full is the context?", "Am I running out of context?", "Check context pressure."]),
|
||||
("model_routing", &["Which model should I use?", "Route this task.", "What's the best model for this?"]),
|
||||
("witness", &["What did you learn from this?", "Reflect on what happened.", "What signals did you detect?"]),
|
||||
("task_gate", &["I've told you this before.", "This should be automatic.", "Make this a standard process.", "Remember this instruction."]),
|
||||
("self_improve", &["What needs improvement?", "Analyze the system state.", "What's not working?", "Suggest improvements."]),
|
||||
("task_persist", &["Save this task for later.", "What tasks are pending?", "Show my task list.", "Update task status."]),
|
||||
("deep_research", &["Research this topic thoroughly.", "Help me investigate this.", "Find information about this.", "Look into this subject."]),
|
||||
("research_lens", &["Is this claim supported by evidence?", "Check this against the baloney detector.", "Verify this research."]),
|
||||
("epistemic_verify", &["Is this actually true?", "Verify this claim.", "Check the evidence."]),
|
||||
(
|
||||
"memory_recall",
|
||||
&[
|
||||
"What do I remember about X?",
|
||||
"Have we discussed this before?",
|
||||
"Find previous decisions about Y.",
|
||||
"What do I know about Z?",
|
||||
],
|
||||
),
|
||||
(
|
||||
"memory_write",
|
||||
&[
|
||||
"Remember this fact.",
|
||||
"Store this for later.",
|
||||
"Save this decision.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"memory_search",
|
||||
&["Search my memory for X.", "Look up anything about Y."],
|
||||
),
|
||||
(
|
||||
"claim_verification",
|
||||
&[
|
||||
"Is this claim supported?",
|
||||
"Verify this statement.",
|
||||
"Check if this is accurate.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"warm_routing",
|
||||
&[
|
||||
"What context is relevant?",
|
||||
"Prime my memory for this topic.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"persona_compile",
|
||||
&["Who am I?", "What are my values?", "Load my identity."],
|
||||
),
|
||||
(
|
||||
"drift_detection",
|
||||
&[
|
||||
"Am I drifting from my identity?",
|
||||
"Check for persona drift.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"growth_extraction",
|
||||
&[
|
||||
"What should I learn from this?",
|
||||
"Extract behavioral deltas.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"harness_validation",
|
||||
&["Is my harness compliant?", "Validate my runtime."],
|
||||
),
|
||||
(
|
||||
"epub",
|
||||
&["Read this ebook.", "Extract text from this epub."],
|
||||
),
|
||||
(
|
||||
"tasks",
|
||||
&[
|
||||
"What are my current tasks?",
|
||||
"Persist this task.",
|
||||
"Show my task list.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"politics",
|
||||
&[
|
||||
"What do I know about this political topic?",
|
||||
"Absorb this political understanding.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"abstraction",
|
||||
&[
|
||||
"Extract facts from this text.",
|
||||
"What observations can you make?",
|
||||
"Synthesize knowledge from this.",
|
||||
"What patterns do you see?",
|
||||
],
|
||||
),
|
||||
(
|
||||
"temporal",
|
||||
&[
|
||||
"What was true then vs now?",
|
||||
"When did this change?",
|
||||
"Is this still current?",
|
||||
"Show me the history of this.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"corrections",
|
||||
&[
|
||||
"I corrected you on this before.",
|
||||
"Don't do that again.",
|
||||
"Remember this correction.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"habits",
|
||||
&[
|
||||
"You should do this automatically.",
|
||||
"Make this a habit.",
|
||||
"Remember this pattern.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"cache",
|
||||
&["Cache this result.", "Is this cached?", "Clear the cache."],
|
||||
),
|
||||
(
|
||||
"events",
|
||||
&[
|
||||
"What events have happened?",
|
||||
"Show me the event log.",
|
||||
"What changed recently?",
|
||||
],
|
||||
),
|
||||
(
|
||||
"benchmark",
|
||||
&[
|
||||
"Run the benchmark.",
|
||||
"Test the system.",
|
||||
"Check for regressions.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"identity",
|
||||
&[
|
||||
"Sign this file.",
|
||||
"Verify this signature.",
|
||||
"Show my agent ID.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"agent_profiles",
|
||||
&[
|
||||
"Switch to blank agent.",
|
||||
"Create a test profile.",
|
||||
"Restore Rune profile.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"voice_sync",
|
||||
&[
|
||||
"Sync the voice.",
|
||||
"Update the system prompt.",
|
||||
"Is the prompt in sync?",
|
||||
],
|
||||
),
|
||||
(
|
||||
"prompt_sync",
|
||||
&[
|
||||
"Sync the system prompt.",
|
||||
"Derive prompt from store.",
|
||||
"Is the prompt current?",
|
||||
],
|
||||
),
|
||||
(
|
||||
"context_pressure",
|
||||
&[
|
||||
"How full is the context?",
|
||||
"Am I running out of context?",
|
||||
"Check context pressure.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"model_routing",
|
||||
&[
|
||||
"Which model should I use?",
|
||||
"Route this task.",
|
||||
"What's the best model for this?",
|
||||
],
|
||||
),
|
||||
(
|
||||
"witness",
|
||||
&[
|
||||
"What did you learn from this?",
|
||||
"Reflect on what happened.",
|
||||
"What signals did you detect?",
|
||||
],
|
||||
),
|
||||
(
|
||||
"task_gate",
|
||||
&[
|
||||
"I've told you this before.",
|
||||
"This should be automatic.",
|
||||
"Make this a standard process.",
|
||||
"Remember this instruction.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"self_improve",
|
||||
&[
|
||||
"What needs improvement?",
|
||||
"Analyze the system state.",
|
||||
"What's not working?",
|
||||
"Suggest improvements.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"task_persist",
|
||||
&[
|
||||
"Save this task for later.",
|
||||
"What tasks are pending?",
|
||||
"Show my task list.",
|
||||
"Update task status.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"deep_research",
|
||||
&[
|
||||
"Research this topic thoroughly.",
|
||||
"Help me investigate this.",
|
||||
"Find information about this.",
|
||||
"Look into this subject.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"research_lens",
|
||||
&[
|
||||
"Is this claim supported by evidence?",
|
||||
"Check this against the baloney detector.",
|
||||
"Verify this research.",
|
||||
],
|
||||
),
|
||||
(
|
||||
"epistemic_verify",
|
||||
&[
|
||||
"Is this actually true?",
|
||||
"Verify this claim.",
|
||||
"Check the evidence.",
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
/// RISK_MAP — the oracle dict, insertion order preserved.
|
||||
@@ -106,9 +313,8 @@ pub const RISK_MAP: &[(&str, &str)] = &[
|
||||
/// generate_card, as whole-`\w`-token equals (each alternative is a
|
||||
/// complete word token, so token-level equality is exact).
|
||||
pub const CARD_VERBS: &[&str] = &[
|
||||
"recall", "search", "verify", "check", "write", "store", "detect", "audit",
|
||||
"extract", "route", "compile", "validate", "assess", "build", "list", "show",
|
||||
"find",
|
||||
"recall", "search", "verify", "check", "write", "store", "detect", "audit", "extract", "route",
|
||||
"compile", "validate", "assess", "build", "list", "show", "find",
|
||||
];
|
||||
|
||||
// ── the core probe (facade `_cs_core` mirror, _ss_core law) ─────────────────
|
||||
@@ -412,7 +618,10 @@ fn cap_iter(caps: &Json) -> Result<Vec<String>, String> {
|
||||
|
||||
fn hooks_keys(hooks: &Json) -> Result<(bool, Vec<String>), String> {
|
||||
match hooks {
|
||||
Json::Obj(pairs) => Ok((!pairs.is_empty(), pairs.iter().map(|(k, _)| k.clone()).collect())),
|
||||
Json::Obj(pairs) => Ok((
|
||||
!pairs.is_empty(),
|
||||
pairs.iter().map(|(k, _)| k.clone()).collect(),
|
||||
)),
|
||||
// non-dict hooks: bool() is legal but the oracle crashes on
|
||||
// hooks.keys() in the card build — the TypeError arm.
|
||||
_ => Err("'hooks' object has no attribute 'keys'".into()),
|
||||
@@ -679,10 +888,7 @@ pub fn match_capabilities(cards: &[Json], query: &str, top_k: usize) -> Json {
|
||||
scored.push((score, card.clone()));
|
||||
}
|
||||
}
|
||||
scored.sort_by(|a, b| {
|
||||
b.0.partial_cmp(&a.0)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
|
||||
Json::Arr(scored.into_iter().take(top_k).map(|(_, c)| c).collect())
|
||||
}
|
||||
|
||||
@@ -806,4 +1012,4 @@ fn py_type_name(v: &Json) -> &'static str {
|
||||
Json::Arr(_) => "list",
|
||||
Json::Obj(_) => "dict",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,11 +141,8 @@ fn gap_replays_frozen_truth_verbatim() {
|
||||
.and_then(|c| case_field(c, "expected"))
|
||||
.cloned()
|
||||
.expect("load_registry case present");
|
||||
std::fs::write(
|
||||
&home.registry_path,
|
||||
pyjson::dump_indent2(®istry),
|
||||
)
|
||||
.expect("write fixture registry");
|
||||
std::fs::write(&home.registry_path, pyjson::dump_indent2(®istry))
|
||||
.expect("write fixture registry");
|
||||
// pinned staleness law (mirrors the generator's os.utime pins)
|
||||
pin_mtime(&home.cards_path, 1_000_000_000);
|
||||
pin_mtime(&home.registry_path, 1_000_000_100);
|
||||
@@ -181,13 +178,8 @@ fn gap_replays_frozen_truth_verbatim() {
|
||||
assert_json_eq(&label, &Json::Bool(stale), expected);
|
||||
}
|
||||
"load_cards" => {
|
||||
let out = gap::load_cards(
|
||||
&home.cards_path,
|
||||
&home.registry_path,
|
||||
&tools,
|
||||
&io,
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("{}: load_cards errored: {}", label, e));
|
||||
let out = gap::load_cards(&home.cards_path, &home.registry_path, &tools, &io)
|
||||
.unwrap_or_else(|e| panic!("{}: load_cards errored: {}", label, e));
|
||||
cards = match &out {
|
||||
Json::Arr(a) => a.clone(),
|
||||
_ => vec![],
|
||||
@@ -209,8 +201,11 @@ fn gap_replays_frozen_truth_verbatim() {
|
||||
assert_json_eq(&label, &out, expected);
|
||||
}
|
||||
"generate_card" => {
|
||||
let out = gap::generate_card(&as_string(args.first()), args.get(1).unwrap_or(&Json::Obj(vec![])))
|
||||
.unwrap_or_else(|e| panic!("{}: generate_card errored: {}", label, e));
|
||||
let out = gap::generate_card(
|
||||
&as_string(args.first()),
|
||||
args.get(1).unwrap_or(&Json::Obj(vec![])),
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("{}: generate_card errored: {}", label, e));
|
||||
assert_json_eq(&label, &out, expected);
|
||||
}
|
||||
"_py_score_card" => {
|
||||
@@ -261,7 +256,9 @@ impl gap::CbIo for FailIo {
|
||||
fn mtime(&self, path: &str) -> Option<f64> {
|
||||
std::fs::metadata(path).ok().and_then(|m| {
|
||||
m.modified().ok().and_then(|t| {
|
||||
t.duration_since(std::time::UNIX_EPOCH).ok().map(|d| d.as_secs_f64())
|
||||
t.duration_since(std::time::UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|d| d.as_secs_f64())
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -310,7 +307,7 @@ fn load_registry_missing_and_value_law() {
|
||||
let home = fixture_home();
|
||||
let io = gap::StdIo;
|
||||
let _ = std::fs::remove_file(&home.registry_path); // never created by fixture_home
|
||||
// missing registry → the empty dict (never an Err)
|
||||
// missing registry → the empty dict (never an Err)
|
||||
let empty = gap::load_registry(&home.registry_path, &io).unwrap();
|
||||
assert_eq!(j(&empty), "{}");
|
||||
// corrupt registry text surfaces as an envelope err (oracle ValueError)
|
||||
@@ -322,9 +319,12 @@ fn load_registry_missing_and_value_law() {
|
||||
fn load_cards_rebuild_and_degrade_arms() {
|
||||
let home = fixture_home();
|
||||
let io = gap::StdIo;
|
||||
let registry: Json = pyjson::parse(r#"{"plugins": {"only": {"manifest": {
|
||||
let registry: Json = pyjson::parse(
|
||||
r#"{"plugins": {"only": {"manifest": {
|
||||
"name": "only", "description": "route this", "capabilities": ["warm_routing"],
|
||||
"priority": 1}}}}"#).expect("registry fixture");
|
||||
"priority": 1}}}}"#,
|
||||
)
|
||||
.expect("registry fixture");
|
||||
std::fs::write(&home.registry_path, pyjson::dump_indent2(®istry)).expect("registry");
|
||||
|
||||
// missing cards + missing dir → makedirs law creates the dir, rebuild writes
|
||||
@@ -353,7 +353,8 @@ fn load_cards_rebuild_and_degrade_arms() {
|
||||
fail_replace: true,
|
||||
};
|
||||
std::fs::write(&home.cards_path, "").expect("clobber to empty cards");
|
||||
let degraded = gap::load_cards(&home.cards_path, &home.registry_path, &[], &fail_io).expect("degrade");
|
||||
let degraded =
|
||||
gap::load_cards(&home.cards_path, &home.registry_path, &[], &fail_io).expect("degrade");
|
||||
assert_eq!(j(°raded), j(&out));
|
||||
let on_disk = std::fs::read_to_string(&home.cards_path).expect("cards readable");
|
||||
assert_eq!(on_disk, "", "write failure leaves no fresh index behind");
|
||||
@@ -367,9 +368,9 @@ fn load_cards_corrupt_and_non_list_arms() {
|
||||
std::fs::write(&home.registry_path, pyjson::dump_indent2(®istry)).expect("registry");
|
||||
|
||||
for (label, seeds) in [
|
||||
("corrupt", "{\"name\": true"), // JSONDecodeError → rebuild
|
||||
("scalar", "42"), // valid JSON, not a list → rebuild
|
||||
("empty_list", "[]"), // empty list → rebuild
|
||||
("corrupt", "{\"name\": true"), // JSONDecodeError → rebuild
|
||||
("scalar", "42"), // valid JSON, not a list → rebuild
|
||||
("empty_list", "[]"), // empty list → rebuild
|
||||
] {
|
||||
std::fs::write(&home.cards_path, seeds).expect("seed");
|
||||
let out = gap::load_cards(&home.cards_path, &home.registry_path, &[], &io).expect(label);
|
||||
@@ -377,13 +378,20 @@ fn load_cards_corrupt_and_non_list_arms() {
|
||||
Json::Arr(a) => a.clone(),
|
||||
_ => panic!("{}", label),
|
||||
};
|
||||
assert!(arr.is_empty(), "{}: plugin-less rebuild has no cards", label);
|
||||
assert!(
|
||||
arr.is_empty(),
|
||||
"{}: plugin-less rebuild has no cards",
|
||||
label
|
||||
);
|
||||
// the rebuild replaced the bad index on disk (atomic replace)
|
||||
let on_disk = pyjson::parse(
|
||||
&std::fs::read_to_string(&home.cards_path).expect("rewritten"),
|
||||
)
|
||||
.expect("valid JSON index");
|
||||
assert_eq!(j(&out), j(&on_disk), "{}: rebuilt index == returned cards", label);
|
||||
let on_disk = pyjson::parse(&std::fs::read_to_string(&home.cards_path).expect("rewritten"))
|
||||
.expect("valid JSON index");
|
||||
assert_eq!(
|
||||
j(&out),
|
||||
j(&on_disk),
|
||||
"{}: rebuilt index == returned cards",
|
||||
label
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,4 +525,4 @@ fn failio_seam_declared() {
|
||||
fail_replace: true,
|
||||
};
|
||||
assert!(io.read("/nope").is_err());
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,9 @@
|
||||
//! facade does: mutators return the updated doc, queries don't mutate.
|
||||
|
||||
use crate::{
|
||||
op_active, op_attach_spec, op_bump_version, op_by_component,
|
||||
op_classify_version_bump, op_context_packet, op_define, op_get,
|
||||
op_material_change, op_move, op_new, op_retire, op_revise_spec,
|
||||
op_save, op_spec_context, op_spec_drift_status, op_supersede,
|
||||
op_active, op_attach_spec, op_bump_version, op_by_component, op_classify_version_bump,
|
||||
op_context_packet, op_define, op_get, op_material_change, op_move, op_new, op_retire,
|
||||
op_revise_spec, op_save, op_spec_context, op_spec_drift_status, op_supersede,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
@@ -84,7 +83,8 @@ fn parity_battery() {
|
||||
let mut contract = envelope.clone();
|
||||
contract.as_object_mut().unwrap().remove("doc");
|
||||
assert_eq!(
|
||||
&contract, exp,
|
||||
&contract,
|
||||
exp,
|
||||
"seq '{name}' op {i} {:?}",
|
||||
serde_json::to_string(op).unwrap_or_default()
|
||||
);
|
||||
@@ -121,7 +121,9 @@ fn save_bytes_pinned() {
|
||||
serde_json::to_string_pretty(&sorted)
|
||||
.map(pretty_to_py_indent)
|
||||
.ok(),
|
||||
seq["saved_files"]["capability_ledger.json"].as_str().map(str::to_string),
|
||||
seq["saved_files"]["capability_ledger.json"]
|
||||
.as_str()
|
||||
.map(str::to_string),
|
||||
"seq '{}': save() byte parity",
|
||||
seq["name"].as_str().unwrap()
|
||||
);
|
||||
|
||||
@@ -40,28 +40,81 @@ const SPEC_SCHEMA_VERSION: i64 = 1;
|
||||
/// with digit value = codepoint - run start (verified against
|
||||
/// unicodedata across the whole BMP+ planes).
|
||||
const ND_RANGES: &[(u32, u32)] = &[
|
||||
(0x30, 0x39), (0x660, 0x669), (0x6F0, 0x6F9), (0x7C0, 0x7C9),
|
||||
(0x966, 0x96F), (0x9E6, 0x9EF), (0xA66, 0xA6F), (0xAE6, 0xAEF),
|
||||
(0xB66, 0xB6F), (0xBE6, 0xBEF), (0xC66, 0xC6F), (0xCE6, 0xCEF),
|
||||
(0xD66, 0xD6F), (0xDE6, 0xDEF), (0xE50, 0xE59), (0xED0, 0xED9),
|
||||
(0xF20, 0xF29), (0x1040, 0x1049), (0x1090, 0x1099), (0x17E0, 0x17E9),
|
||||
(0x1810, 0x1819), (0x1946, 0x194F), (0x19D0, 0x19D9), (0x1A80, 0x1A89),
|
||||
(0x1A90, 0x1A99), (0x1B50, 0x1B59), (0x1BB0, 0x1BB9), (0x1C40, 0x1C49),
|
||||
(0x1C50, 0x1C59), (0xA620, 0xA629), (0xA8D0, 0xA8D9), (0xA900, 0xA909),
|
||||
(0xA9D0, 0xA9D9), (0xA9F0, 0xA9F9), (0xAA50, 0xAA59), (0xABF0, 0xABF9),
|
||||
(0xFF10, 0xFF19), (0x104A0, 0x104A9), (0x10D30, 0x10D39),
|
||||
(0x10D40, 0x10D49), (0x11066, 0x1106F), (0x110F0, 0x110F9),
|
||||
(0x11136, 0x1113F), (0x111D0, 0x111D9), (0x112F0, 0x112F9),
|
||||
(0x11450, 0x11459), (0x114D0, 0x114D9), (0x11650, 0x11659),
|
||||
(0x116C0, 0x116C9), (0x116D0, 0x116D9), (0x116DA, 0x116E3),
|
||||
(0x11730, 0x11739), (0x118E0, 0x118E9), (0x11950, 0x11959),
|
||||
(0x11BF0, 0x11BF9), (0x11C50, 0x11C59), (0x11D50, 0x11D59),
|
||||
(0x11DA0, 0x11DA9), (0x11F50, 0x11F59), (0x16130, 0x16139),
|
||||
(0x16A60, 0x16A69), (0x16AC0, 0x16AC9), (0x16B50, 0x16B59),
|
||||
(0x16D70, 0x16D79), (0x1CCF0, 0x1CCF9), (0x1D7CE, 0x1D7D7),
|
||||
(0x1D7D8, 0x1D7E1), (0x1D7E2, 0x1D7EB), (0x1D7EC, 0x1D7F5),
|
||||
(0x1D7F6, 0x1D7FF), (0x1E140, 0x1E149), (0x1E2F0, 0x1E2F9),
|
||||
(0x1E4F0, 0x1E4F9), (0x1E5F1, 0x1E5FA), (0x1E950, 0x1E959),
|
||||
(0x30, 0x39),
|
||||
(0x660, 0x669),
|
||||
(0x6F0, 0x6F9),
|
||||
(0x7C0, 0x7C9),
|
||||
(0x966, 0x96F),
|
||||
(0x9E6, 0x9EF),
|
||||
(0xA66, 0xA6F),
|
||||
(0xAE6, 0xAEF),
|
||||
(0xB66, 0xB6F),
|
||||
(0xBE6, 0xBEF),
|
||||
(0xC66, 0xC6F),
|
||||
(0xCE6, 0xCEF),
|
||||
(0xD66, 0xD6F),
|
||||
(0xDE6, 0xDEF),
|
||||
(0xE50, 0xE59),
|
||||
(0xED0, 0xED9),
|
||||
(0xF20, 0xF29),
|
||||
(0x1040, 0x1049),
|
||||
(0x1090, 0x1099),
|
||||
(0x17E0, 0x17E9),
|
||||
(0x1810, 0x1819),
|
||||
(0x1946, 0x194F),
|
||||
(0x19D0, 0x19D9),
|
||||
(0x1A80, 0x1A89),
|
||||
(0x1A90, 0x1A99),
|
||||
(0x1B50, 0x1B59),
|
||||
(0x1BB0, 0x1BB9),
|
||||
(0x1C40, 0x1C49),
|
||||
(0x1C50, 0x1C59),
|
||||
(0xA620, 0xA629),
|
||||
(0xA8D0, 0xA8D9),
|
||||
(0xA900, 0xA909),
|
||||
(0xA9D0, 0xA9D9),
|
||||
(0xA9F0, 0xA9F9),
|
||||
(0xAA50, 0xAA59),
|
||||
(0xABF0, 0xABF9),
|
||||
(0xFF10, 0xFF19),
|
||||
(0x104A0, 0x104A9),
|
||||
(0x10D30, 0x10D39),
|
||||
(0x10D40, 0x10D49),
|
||||
(0x11066, 0x1106F),
|
||||
(0x110F0, 0x110F9),
|
||||
(0x11136, 0x1113F),
|
||||
(0x111D0, 0x111D9),
|
||||
(0x112F0, 0x112F9),
|
||||
(0x11450, 0x11459),
|
||||
(0x114D0, 0x114D9),
|
||||
(0x11650, 0x11659),
|
||||
(0x116C0, 0x116C9),
|
||||
(0x116D0, 0x116D9),
|
||||
(0x116DA, 0x116E3),
|
||||
(0x11730, 0x11739),
|
||||
(0x118E0, 0x118E9),
|
||||
(0x11950, 0x11959),
|
||||
(0x11BF0, 0x11BF9),
|
||||
(0x11C50, 0x11C59),
|
||||
(0x11D50, 0x11D59),
|
||||
(0x11DA0, 0x11DA9),
|
||||
(0x11F50, 0x11F59),
|
||||
(0x16130, 0x16139),
|
||||
(0x16A60, 0x16A69),
|
||||
(0x16AC0, 0x16AC9),
|
||||
(0x16B50, 0x16B59),
|
||||
(0x16D70, 0x16D79),
|
||||
(0x1CCF0, 0x1CCF9),
|
||||
(0x1D7CE, 0x1D7D7),
|
||||
(0x1D7D8, 0x1D7E1),
|
||||
(0x1D7E2, 0x1D7EB),
|
||||
(0x1D7EC, 0x1D7F5),
|
||||
(0x1D7F6, 0x1D7FF),
|
||||
(0x1E140, 0x1E149),
|
||||
(0x1E2F0, 0x1E2F9),
|
||||
(0x1E4F0, 0x1E4F9),
|
||||
(0x1E5F1, 0x1E5FA),
|
||||
(0x1E950, 0x1E959),
|
||||
(0x1FBF0, 0x1FBF9),
|
||||
];
|
||||
|
||||
@@ -238,9 +291,12 @@ impl Maturity {
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_raw(contract_changed: bool, behaviour_added: bool,
|
||||
consumer_break: bool, implementation_only: bool)
|
||||
-> Result<BumpKind, String> {
|
||||
fn classify_raw(
|
||||
contract_changed: bool,
|
||||
behaviour_added: bool,
|
||||
consumer_break: bool,
|
||||
implementation_only: bool,
|
||||
) -> Result<BumpKind, String> {
|
||||
if consumer_break || contract_changed {
|
||||
return Ok(BumpKind::Major);
|
||||
}
|
||||
@@ -265,7 +321,11 @@ fn bump_version_str(version: &str, kind: &str) -> Result<String, String> {
|
||||
}
|
||||
// int() semantics on every component: leading zeros collapse whether
|
||||
// or not that component is the one being bumped
|
||||
let [maj, min, pat] = [norm_component(parts[0]), norm_component(parts[1]), norm_component(parts[2])];
|
||||
let [maj, min, pat] = [
|
||||
norm_component(parts[0]),
|
||||
norm_component(parts[1]),
|
||||
norm_component(parts[2]),
|
||||
];
|
||||
let kind = BumpKind::from_oracle(kind).map_err(|repr| {
|
||||
// bump_version's own branch ladder raises ValueError("unknown bump
|
||||
// kind {kind!r}") — different error text from the revise_spec
|
||||
@@ -294,8 +354,7 @@ fn ledger_get<'a>(doc: &'a Value, capability_id: &str) -> Option<&'a Value> {
|
||||
doc.get("capabilities").and_then(|c| c.get(&key))
|
||||
}
|
||||
|
||||
fn ledger_take(doc: &mut Value, capability_id: &str)
|
||||
-> Result<(String, Value), String> {
|
||||
fn ledger_take(doc: &mut Value, capability_id: &str) -> Result<(String, Value), String> {
|
||||
let key = resolve_key(doc, capability_id);
|
||||
match doc.get("capabilities").and_then(|c| c.get(&key)) {
|
||||
Some(rec) => Ok((key, rec.clone())),
|
||||
@@ -385,8 +444,7 @@ pub fn op_get(a: &Value) -> OpResult {
|
||||
pub fn op_material_change(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let (key, mut rec) = ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
// T21.1 §21.1D: deterministic revision counter, not wall-clock ordering
|
||||
let rev = rec
|
||||
.get("material_revision")
|
||||
@@ -423,8 +481,7 @@ pub fn op_material_change(a: &Value) -> OpResult {
|
||||
pub fn op_move(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let (key, mut rec) = ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let old = rec["source_locations"].clone();
|
||||
let new_locations = a.get("new_locations").cloned().unwrap_or(json!([]));
|
||||
rec["source_locations"] = new_locations.clone();
|
||||
@@ -446,8 +503,7 @@ pub fn op_move(a: &Value) -> OpResult {
|
||||
pub fn op_supersede(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let (key, mut rec) = ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let by_in = arg_str(a, "by", "");
|
||||
let by = if by_in.contains(':') {
|
||||
by_in.to_string()
|
||||
@@ -474,8 +530,7 @@ pub fn op_supersede(a: &Value) -> OpResult {
|
||||
pub fn op_retire(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let (key, mut rec) = ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
rec["maturity"] = json!("retired");
|
||||
rec["updated_at"] = json!(now);
|
||||
push_history(
|
||||
@@ -537,9 +592,7 @@ pub fn op_by_component(a: &Value) -> OpResult {
|
||||
Value::Array(
|
||||
caps.values()
|
||||
.filter(|r| is_active(r))
|
||||
.filter(|r| {
|
||||
r.get("component").and_then(|v| v.as_str()) == Some(component)
|
||||
})
|
||||
.filter(|r| r.get("component").and_then(|v| v.as_str()) == Some(component))
|
||||
.cloned()
|
||||
.collect(),
|
||||
),
|
||||
@@ -551,27 +604,33 @@ pub fn op_context_packet(a: &Value) -> OpResult {
|
||||
let doc = &a["doc"];
|
||||
let cid = arg_str(a, "capability_id", "");
|
||||
let Some(rec) = ledger_get(doc, cid) else {
|
||||
return Ok((json!({"error": format!("unknown capability {}", py_repr(cid))}), None));
|
||||
return Ok((
|
||||
json!({"error": format!("unknown capability {}", py_repr(cid))}),
|
||||
None,
|
||||
));
|
||||
};
|
||||
let g = |k: &str| rec.get(k).cloned().unwrap_or(Value::Null);
|
||||
Ok((json!({
|
||||
"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"capability_id": g("capability_id"),
|
||||
"name": g("name"),
|
||||
"purpose": g("purpose"),
|
||||
"component": g("component"),
|
||||
"source_locations": g("source_locations"),
|
||||
"contract": g("contract"),
|
||||
"state_touched": g("state_touched"),
|
||||
"dependencies": g("dependencies"),
|
||||
"consumers": g("consumers"),
|
||||
"invariants": g("invariants"),
|
||||
"behaviour_specs": g("behaviour_specs"),
|
||||
"maturity": g("maturity"),
|
||||
"constraints": g("constraints"),
|
||||
"generation": g("generation"),
|
||||
"limitations": g("limitations"),
|
||||
}), None))
|
||||
Ok((
|
||||
json!({
|
||||
"schema_version": LEDGER_SCHEMA_VERSION,
|
||||
"capability_id": g("capability_id"),
|
||||
"name": g("name"),
|
||||
"purpose": g("purpose"),
|
||||
"component": g("component"),
|
||||
"source_locations": g("source_locations"),
|
||||
"contract": g("contract"),
|
||||
"state_touched": g("state_touched"),
|
||||
"dependencies": g("dependencies"),
|
||||
"consumers": g("consumers"),
|
||||
"invariants": g("invariants"),
|
||||
"behaviour_specs": g("behaviour_specs"),
|
||||
"maturity": g("maturity"),
|
||||
"constraints": g("constraints"),
|
||||
"generation": g("generation"),
|
||||
"limitations": g("limitations"),
|
||||
}),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn op_save(a: &Value) -> OpResult {
|
||||
@@ -588,14 +647,18 @@ pub fn op_classify_version_bump(a: &Value) -> OpResult {
|
||||
arg_bool(a, "behaviour_added"),
|
||||
arg_bool(a, "consumer_break"),
|
||||
arg_bool(a, "implementation_only"),
|
||||
)?.as_str()),
|
||||
)?
|
||||
.as_str()),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn op_bump_version(a: &Value) -> OpResult {
|
||||
Ok((
|
||||
json!(bump_version_str(arg_str(a, "version", ""), arg_str(a, "kind", ""))?),
|
||||
json!(bump_version_str(
|
||||
arg_str(a, "version", ""),
|
||||
arg_str(a, "kind", "")
|
||||
)?),
|
||||
None,
|
||||
))
|
||||
}
|
||||
@@ -603,8 +666,7 @@ pub fn op_bump_version(a: &Value) -> OpResult {
|
||||
pub fn op_attach_spec(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let (key, mut rec) = ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let baseline_version = arg_str(a, "baseline_version", "0.1.0").to_string();
|
||||
if !is_semver(&baseline_version) {
|
||||
return Err(format!(
|
||||
@@ -642,8 +704,7 @@ pub fn op_attach_spec(a: &Value) -> OpResult {
|
||||
pub fn op_revise_spec(a: &Value) -> OpResult {
|
||||
let mut doc = a.get("doc").cloned().unwrap_or(Value::Null);
|
||||
let now = arg_str(a, "now", "");
|
||||
let (key, mut rec) =
|
||||
ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let (key, mut rec) = ledger_take(&mut doc, arg_str(a, "capability_id", ""))?;
|
||||
let cid = arg_str(a, "capability_id", "");
|
||||
let mut spec = match rec.get("spec") {
|
||||
Some(s) if !s.is_null() => s.clone(),
|
||||
@@ -674,24 +735,21 @@ pub fn op_revise_spec(a: &Value) -> OpResult {
|
||||
let old = spec["version"].as_str().unwrap_or("").to_string();
|
||||
let new = bump_version_str(&old, kind.as_str())?;
|
||||
spec["version"] = json!(new);
|
||||
spec["version_history"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!({
|
||||
"at": now,
|
||||
"version": new,
|
||||
"kind": kind.as_str(),
|
||||
"reason": py_slice(arg_str(a, "reason", ""), 200),
|
||||
"changed_sections": arg_arr(a, "changed_sections"),
|
||||
"affected_consumers": arg_arr(a, "affected_consumers"),
|
||||
"validation_consequences": arg_arr(a, "validation_consequences"),
|
||||
// the governed decision COVERS all material revisions up to
|
||||
// and including the current one — drift closes deterministically
|
||||
"covers_material_revision": rec
|
||||
.get("material_revision")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0),
|
||||
}));
|
||||
spec["version_history"].as_array_mut().unwrap().push(json!({
|
||||
"at": now,
|
||||
"version": new,
|
||||
"kind": kind.as_str(),
|
||||
"reason": py_slice(arg_str(a, "reason", ""), 200),
|
||||
"changed_sections": arg_arr(a, "changed_sections"),
|
||||
"affected_consumers": arg_arr(a, "affected_consumers"),
|
||||
"validation_consequences": arg_arr(a, "validation_consequences"),
|
||||
// the governed decision COVERS all material revisions up to
|
||||
// and including the current one — drift closes deterministically
|
||||
"covers_material_revision": rec
|
||||
.get("material_revision")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0),
|
||||
}));
|
||||
rec["spec"] = spec;
|
||||
rec["updated_at"] = json!(now);
|
||||
put_rec(&mut doc, &key, rec);
|
||||
@@ -705,16 +763,22 @@ pub fn op_spec_drift_status(a: &Value) -> OpResult {
|
||||
let doc = &a["doc"];
|
||||
let cid = arg_str(a, "capability_id", "");
|
||||
let Some(rec) = ledger_get(doc, cid) else {
|
||||
return Ok((json!({
|
||||
"drift": false, "obligation": Value::Null,
|
||||
"detail": format!("unknown capability {}", py_repr(cid)),
|
||||
}), None));
|
||||
return Ok((
|
||||
json!({
|
||||
"drift": false, "obligation": Value::Null,
|
||||
"detail": format!("unknown capability {}", py_repr(cid)),
|
||||
}),
|
||||
None,
|
||||
));
|
||||
};
|
||||
let Some(spec) = rec.get("spec").filter(|s| !s.is_null()) else {
|
||||
return Ok((json!({
|
||||
"drift": false, "obligation": Value::Null,
|
||||
"detail": "no spec attached — nothing to drift from",
|
||||
}), None));
|
||||
return Ok((
|
||||
json!({
|
||||
"drift": false, "obligation": Value::Null,
|
||||
"detail": "no spec attached — nothing to drift from",
|
||||
}),
|
||||
None,
|
||||
));
|
||||
};
|
||||
let hist = spec.get("version_history").and_then(|v| v.as_array());
|
||||
let covered = match hist {
|
||||
@@ -735,18 +799,24 @@ pub fn op_spec_drift_status(a: &Value) -> OpResult {
|
||||
"spec_version": spec.get("version").cloned().unwrap_or(Value::Null),
|
||||
});
|
||||
if current > covered {
|
||||
Ok((json!({
|
||||
"drift": true,
|
||||
"obligation": format!(
|
||||
"SPEC_VERSION_DRIFT: {cid} has {} uncovered material revision(s) \
|
||||
(material rev {current} > governed rev {covered}) — \
|
||||
run revise_spec (PATCH/MINOR/MAJOR) to close",
|
||||
current - covered
|
||||
),
|
||||
"detail": detail,
|
||||
}), None))
|
||||
Ok((
|
||||
json!({
|
||||
"drift": true,
|
||||
"obligation": format!(
|
||||
"SPEC_VERSION_DRIFT: {cid} has {} uncovered material revision(s) \
|
||||
(material rev {current} > governed rev {covered}) — \
|
||||
run revise_spec (PATCH/MINOR/MAJOR) to close",
|
||||
current - covered
|
||||
),
|
||||
"detail": detail,
|
||||
}),
|
||||
None,
|
||||
))
|
||||
} else {
|
||||
Ok((json!({"drift": false, "obligation": Value::Null, "detail": detail}), None))
|
||||
Ok((
|
||||
json!({"drift": false, "obligation": Value::Null, "detail": detail}),
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,7 +859,9 @@ fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
|
||||
@@ -45,9 +45,13 @@ fn as_arr(v: &Json) -> Vec<Json> {
|
||||
fn run_case(fn_name: &str, args: &Json) -> Json {
|
||||
match fn_name {
|
||||
"new_topology" => topology::new_topology(),
|
||||
"cofire" => topology::cofire(&as_obj(&args.get("topology").cloned().unwrap_or(Json::Null)),
|
||||
&as_arr(&args.get("topics").cloned().unwrap_or(Json::Arr(vec![])))),
|
||||
"decay" => topology::decay(&as_obj(&args.get("topology").cloned().unwrap_or(Json::Null))),
|
||||
"cofire" => topology::cofire(
|
||||
&as_obj(&args.get("topology").cloned().unwrap_or(Json::Null)),
|
||||
&as_arr(&args.get("topics").cloned().unwrap_or(Json::Arr(vec![]))),
|
||||
),
|
||||
"decay" => topology::decay(&as_obj(
|
||||
&args.get("topology").cloned().unwrap_or(Json::Null),
|
||||
)),
|
||||
"clusters" => {
|
||||
let t = as_obj(&args.get("topology").cloned().unwrap_or(Json::Null));
|
||||
let threshold = match args.get("threshold") {
|
||||
@@ -56,8 +60,15 @@ fn run_case(fn_name: &str, args: &Json) -> Json {
|
||||
};
|
||||
Json::Arr(topology::clusters(&t, threshold))
|
||||
}
|
||||
"suggest" => topology::suggest(&as_obj(&args.get("topology").cloned().unwrap_or(Json::Null)),
|
||||
&as_arr(&args.get("fact_topics").cloned().unwrap_or(Json::Arr(vec![])))),
|
||||
"suggest" => topology::suggest(
|
||||
&as_obj(&args.get("topology").cloned().unwrap_or(Json::Null)),
|
||||
&as_arr(
|
||||
&args
|
||||
.get("fact_topics")
|
||||
.cloned()
|
||||
.unwrap_or(Json::Arr(vec![])),
|
||||
),
|
||||
),
|
||||
other => panic!("unknown frozen fn {other}"),
|
||||
}
|
||||
}
|
||||
@@ -66,7 +77,11 @@ fn run_case(fn_name: &str, args: &Json) -> Json {
|
||||
fn frozen_truth_replays_verbatim() {
|
||||
let truth = parse(TRUTH).expect("frozen_ct_truth.json parses");
|
||||
let cases = as_arr(truth.get("cases").expect("cases key"));
|
||||
assert!(cases.len() >= 36, "expected the full frozen battery, got {}", cases.len());
|
||||
assert!(
|
||||
cases.len() >= 36,
|
||||
"expected the full frozen battery, got {}",
|
||||
cases.len()
|
||||
);
|
||||
let mut failures: Vec<String> = vec![];
|
||||
for (i, case) in cases.iter().enumerate() {
|
||||
let fn_name = match case.get("fn") {
|
||||
|
||||
@@ -38,10 +38,7 @@ fn read_arg(ptr: *const c_char) -> Result<Json, String> {
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Ok(x) => Json::Obj(vec![("ok".into(), Json::Bool(true)), ("value".into(), x)]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
@@ -80,8 +77,7 @@ pub extern "C" fn ct_cofire(arg: *const c_char) -> *mut c_char {
|
||||
/// ct_decay({"topology": {...}})
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ct_decay(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg)
|
||||
.map(|a| topology::decay(&obj_arg(&a, "topology")));
|
||||
let r = read_arg(arg).map(|a| topology::decay(&obj_arg(&a, "topology")));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,9 @@ pub fn decay(topology: &Json) -> Json {
|
||||
let kept: Vec<(String, Json)> = obj_pairs(topology.get("edges"))
|
||||
.iter()
|
||||
.filter_map(|(k, v)| match v {
|
||||
Json::Num(n) if n * DECAY_PER_TICK >= 0.5 => Some((k.clone(), Json::Num(n * DECAY_PER_TICK))),
|
||||
Json::Num(n) if n * DECAY_PER_TICK >= 0.5 => {
|
||||
Some((k.clone(), Json::Num(n * DECAY_PER_TICK)))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
@@ -205,7 +207,10 @@ impl Suggestion {
|
||||
"suggestion".into(),
|
||||
Json::Arr(names.iter().cloned().map(Json::Str).collect()),
|
||||
),
|
||||
("reason".into(), Json::Str(format!("shares {overlap} topic(s)"))),
|
||||
(
|
||||
"reason".into(),
|
||||
Json::Str(format!("shares {overlap} topic(s)")),
|
||||
),
|
||||
]),
|
||||
Suggestion::NewTopic => Json::Obj(vec![
|
||||
("suggestion".into(), Json::Str("new_topic".into())),
|
||||
|
||||
@@ -83,4 +83,4 @@ fn dispatch_finish_parity_battery() {
|
||||
i.get("env").unwrap_or(&Value::Null),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -324,8 +324,7 @@ fn _validate(
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|entities| {
|
||||
entities.iter().find(|e| {
|
||||
e.get("id").and_then(Value::as_str)
|
||||
== Some(&format!("contract:{construction_id}"))
|
||||
e.get("id").and_then(Value::as_str) == Some(&format!("contract:{construction_id}"))
|
||||
})
|
||||
});
|
||||
let contract = match contract {
|
||||
@@ -621,7 +620,10 @@ pub fn dispatch(
|
||||
};
|
||||
format!(
|
||||
"command:{}{suffix}",
|
||||
intent.get("command_id").and_then(Value::as_str).unwrap_or("")
|
||||
intent
|
||||
.get("command_id")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
)
|
||||
}
|
||||
Op::Resume | Op::AmendScope => String::new(),
|
||||
@@ -639,7 +641,12 @@ pub fn dispatch(
|
||||
/// Final dispatch envelope assembly after the facade executed the intent
|
||||
/// through the Construction Interface. `success` is Python bool() of the
|
||||
/// interface envelope's success (truthiness, not identity).
|
||||
pub fn dispatch_finish(command_type: &str, construction_id: &str, intent: &Value, env: &Value) -> Value {
|
||||
pub fn dispatch_finish(
|
||||
command_type: &str,
|
||||
construction_id: &str,
|
||||
intent: &Value,
|
||||
env: &Value,
|
||||
) -> Value {
|
||||
json!({
|
||||
"accepted": true,
|
||||
"executed": true,
|
||||
@@ -766,7 +773,13 @@ mod tests {
|
||||
#[test]
|
||||
fn start_in_draft_accepts_transition() {
|
||||
let r = command(
|
||||
"START", "c1", "corr", "cmd-1", &json!({}), NOW, None,
|
||||
"START",
|
||||
"c1",
|
||||
"corr",
|
||||
"cmd-1",
|
||||
&json!({}),
|
||||
NOW,
|
||||
None,
|
||||
Some(&draft_doc("c1")),
|
||||
);
|
||||
assert_eq!(r["accepted"], true);
|
||||
@@ -777,7 +790,16 @@ mod tests {
|
||||
#[test]
|
||||
fn start_in_building_refused() {
|
||||
let doc = json!({"entities": [{"id": "contract:c1", "phase": "building"}]});
|
||||
let r = command("START", "c1", "corr", "cmd-1", &json!({}), NOW, None, Some(&doc));
|
||||
let r = command(
|
||||
"START",
|
||||
"c1",
|
||||
"corr",
|
||||
"cmd-1",
|
||||
&json!({}),
|
||||
NOW,
|
||||
None,
|
||||
Some(&doc),
|
||||
);
|
||||
assert_eq!(r["accepted"], false);
|
||||
assert_eq!(
|
||||
r["reason"],
|
||||
@@ -802,9 +824,21 @@ mod tests {
|
||||
("REQUEST_STATE", CommandType::RequestState, Op::Resume),
|
||||
("START", CommandType::Start, Op::Transition),
|
||||
("RESUME", CommandType::Resume, Op::Resume),
|
||||
("ACKNOWLEDGE_BLOCKER", CommandType::AcknowledgeBlocker, Op::Transition),
|
||||
("PROVIDE_APPROVAL", CommandType::ProvideApproval, Op::Transition),
|
||||
("PROVIDE_CONTEXT", CommandType::ProvideContext, Op::AmendScope),
|
||||
(
|
||||
"ACKNOWLEDGE_BLOCKER",
|
||||
CommandType::AcknowledgeBlocker,
|
||||
Op::Transition,
|
||||
),
|
||||
(
|
||||
"PROVIDE_APPROVAL",
|
||||
CommandType::ProvideApproval,
|
||||
Op::Transition,
|
||||
),
|
||||
(
|
||||
"PROVIDE_CONTEXT",
|
||||
CommandType::ProvideContext,
|
||||
Op::AmendScope,
|
||||
),
|
||||
] {
|
||||
assert_eq!(CommandType::parse(name), Some(cmd));
|
||||
assert_eq!(cmd.to_op(), op);
|
||||
@@ -830,7 +864,13 @@ mod tests {
|
||||
let prior = json!({"accepted": true, "op": "resume", "command_id": "cmd-1",
|
||||
"idempotent_replay": false});
|
||||
let r = command(
|
||||
"RESUME", "c1", "corr", "cmd-1", &json!({}), NOW, Some(&prior),
|
||||
"RESUME",
|
||||
"c1",
|
||||
"corr",
|
||||
"cmd-1",
|
||||
&json!({}),
|
||||
NOW,
|
||||
Some(&prior),
|
||||
Some(&draft_doc("c1")),
|
||||
);
|
||||
assert_eq!(r["idempotent_replay"], true);
|
||||
@@ -841,8 +881,15 @@ mod tests {
|
||||
fn dispatch_exec_decision_carries_command_provenance() {
|
||||
let doc = draft_doc("c1");
|
||||
let prep = dispatch(
|
||||
"PROVIDE_APPROVAL", "c1", "corr", "cmd-9",
|
||||
&json!({"approved_by": "user"}), NOW, None, None, Some(&doc),
|
||||
"PROVIDE_APPROVAL",
|
||||
"c1",
|
||||
"corr",
|
||||
"cmd-9",
|
||||
&json!({"approved_by": "user"}),
|
||||
NOW,
|
||||
None,
|
||||
None,
|
||||
Some(&doc),
|
||||
);
|
||||
assert_eq!(prep["kind"], "exec");
|
||||
assert_eq!(prep["evidence_ref"], "command:cmd-9 approved_by:user");
|
||||
@@ -860,8 +907,17 @@ mod tests {
|
||||
#[test]
|
||||
fn serving_unavailable_truncates_to_200_chars() {
|
||||
let exc = "x".repeat(500);
|
||||
let prep = dispatch("START", "c1", "corr", "cmd-1", &json!({}), NOW, None,
|
||||
Some(&exc), None);
|
||||
let prep = dispatch(
|
||||
"START",
|
||||
"c1",
|
||||
"corr",
|
||||
"cmd-1",
|
||||
&json!({}),
|
||||
NOW,
|
||||
None,
|
||||
Some(&exc),
|
||||
None,
|
||||
);
|
||||
let err = prep["envelope"]["error"].as_str().unwrap();
|
||||
assert_eq!(err.chars().count(), 200);
|
||||
assert!(err.starts_with("serving unavailable: "));
|
||||
@@ -869,4 +925,4 @@ mod tests {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
mod branch_battery;
|
||||
@@ -92,7 +92,10 @@ fn normalize_need_battery() {
|
||||
let need = &case["args"][0];
|
||||
let out = normalize_need(need);
|
||||
check(
|
||||
&format!("normalize {}", serde_json::to_string(&need["need_id"]).unwrap()),
|
||||
&format!(
|
||||
"normalize {}",
|
||||
serde_json::to_string(&need["need_id"]).unwrap()
|
||||
),
|
||||
out,
|
||||
&case["expected"],
|
||||
);
|
||||
@@ -114,8 +117,8 @@ fn language_placement_battery() {
|
||||
.iter()
|
||||
.map(|v| v.as_str().unwrap())
|
||||
.collect();
|
||||
let actual = language_placement(project, &chars)
|
||||
.unwrap_or_else(|e| serde_json::json!({"error": e}));
|
||||
let actual =
|
||||
language_placement(project, &chars).unwrap_or_else(|e| serde_json::json!({"error": e}));
|
||||
check(
|
||||
&format!("placement {} {:?}", project, chars),
|
||||
actual,
|
||||
@@ -167,7 +170,11 @@ fn ss_entity_battery() {
|
||||
.filter(|c| c["fn"] == "ss_entity")
|
||||
{
|
||||
let actual = ss_entity_public(&case["args"][0], str_arg(case, 1)).unwrap_or(Value::Null);
|
||||
check(&format!("entity {:?}", str_arg(case, 1)), actual, &case["expected"]);
|
||||
check(
|
||||
&format!("entity {:?}", str_arg(case, 1)),
|
||||
actual,
|
||||
&case["expected"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,11 +193,7 @@ fn implemented_contracts_battery() {
|
||||
.map(Value::String)
|
||||
.collect(),
|
||||
);
|
||||
check(
|
||||
&format!("contracts {eid:?}"),
|
||||
actual,
|
||||
&case["expected"],
|
||||
);
|
||||
check(&format!("contracts {eid:?}"), actual, &case["expected"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +300,12 @@ fn needs_user_approval_battery() {
|
||||
#[test]
|
||||
fn project_exists_battery() {
|
||||
// The oracle returns a tuple; frozen as [exists, evidence].
|
||||
for (i, case) in truth_doc()["pe_cases"].as_array().unwrap().iter().enumerate() {
|
||||
for (i, case) in truth_doc()["pe_cases"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let project = case["args"][0].as_str();
|
||||
let (exists, evidence) = _project_exists_evidence(project, &case["args"][1]);
|
||||
let actual = serde_json::json!([exists, evidence]);
|
||||
@@ -350,10 +358,7 @@ fn make_decision_battery() {
|
||||
.enumerate()
|
||||
{
|
||||
let k = &case["kwargs"];
|
||||
let extra: Option<Vec<Value>> = k
|
||||
.get("extra_patterns")
|
||||
.and_then(|x| x.as_array())
|
||||
.cloned();
|
||||
let extra: Option<Vec<Value>> = k.get("extra_patterns").and_then(|x| x.as_array()).cloned();
|
||||
let actual = make_decision(
|
||||
&case["args"][0],
|
||||
&k["authorities"],
|
||||
@@ -376,7 +381,11 @@ fn need_view_helpers_battery() {
|
||||
// the full projection round-trips), exactly like the oracle's
|
||||
// `dict(need)`.
|
||||
for case in cases.iter().filter(|c| c["fn"] == "_need_dict") {
|
||||
check("_need_dict", _need_dict(&case["args"][0]), &case["expected"]);
|
||||
check(
|
||||
"_need_dict",
|
||||
_need_dict(&case["args"][0]),
|
||||
&case["expected"],
|
||||
);
|
||||
}
|
||||
// _cdec — frozen under the harness pin: the oracle returned None.
|
||||
for case in cases.iter().filter(|c| c["fn"] == "_cdec") {
|
||||
|
||||
@@ -11,9 +11,9 @@ use serde_json::{json, Value};
|
||||
use crate::{
|
||||
_project_exists_evidence, _py_language_placement, check_completeness, classify_surface,
|
||||
implemented_contracts, language_placement, layout_obligations_for, make_decision,
|
||||
merge_resolutions, need_new, need_to_dict, normalize_need, obligations_from_trace,
|
||||
record_proposed_requirement, resolve_affected_surface, resolve_surface, research_route,
|
||||
needs_user_approval as approval_gate,
|
||||
merge_resolutions, need_new, need_to_dict, needs_user_approval as approval_gate,
|
||||
normalize_need, obligations_from_trace, record_proposed_requirement, research_route,
|
||||
resolve_affected_surface, resolve_surface,
|
||||
};
|
||||
|
||||
fn read_arg(arg: *const c_char) -> Result<Value, String> {
|
||||
@@ -56,7 +56,10 @@ pub extern "C" fn cdec_need_new(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_normalize_need(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let need = a.get("need").cloned().ok_or_else(|| "need required".to_string())?;
|
||||
let need = a
|
||||
.get("need")
|
||||
.cloned()
|
||||
.ok_or_else(|| "need required".to_string())?;
|
||||
Ok(normalize_need(&need))
|
||||
});
|
||||
envelope(r)
|
||||
@@ -144,7 +147,10 @@ pub extern "C" fn cdec_resolve_surface(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_ss_entity(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or_else(|| "doc required".to_string())?;
|
||||
let doc = a
|
||||
.get("doc")
|
||||
.cloned()
|
||||
.ok_or_else(|| "doc required".to_string())?;
|
||||
let eid = a
|
||||
.get("eid")
|
||||
.and_then(|s| s.as_str())
|
||||
@@ -158,7 +164,10 @@ pub extern "C" fn cdec_ss_entity(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_implemented_contracts(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or_else(|| "doc required".to_string())?;
|
||||
let doc = a
|
||||
.get("doc")
|
||||
.cloned()
|
||||
.ok_or_else(|| "doc required".to_string())?;
|
||||
let entity_id = a
|
||||
.get("entity_id")
|
||||
.and_then(|s| s.as_str())
|
||||
@@ -172,7 +181,10 @@ pub extern "C" fn cdec_implemented_contracts(arg: *const c_char) -> *mut c_char
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_resolve_affected_surface(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let need = a.get("need").cloned().ok_or_else(|| "need required".to_string())?;
|
||||
let need = a
|
||||
.get("need")
|
||||
.cloned()
|
||||
.ok_or_else(|| "need required".to_string())?;
|
||||
let authorities = a.get("authorities").cloned().unwrap_or(json!({}));
|
||||
Ok(resolve_affected_surface(&need, &authorities))
|
||||
});
|
||||
@@ -183,14 +195,25 @@ pub extern "C" fn cdec_resolve_affected_surface(arg: *const c_char) -> *mut c_ch
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_check_completeness(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let req = a.get("req").cloned().ok_or_else(|| "req required".to_string())?;
|
||||
let need = a.get("need").cloned().ok_or_else(|| "need required".to_string())?;
|
||||
let req = a
|
||||
.get("req")
|
||||
.cloned()
|
||||
.ok_or_else(|| "req required".to_string())?;
|
||||
let need = a
|
||||
.get("need")
|
||||
.cloned()
|
||||
.ok_or_else(|| "need required".to_string())?;
|
||||
let authorities = a.get("authorities").cloned().unwrap_or(json!({}));
|
||||
let surface_result = a
|
||||
.get("surface_result")
|
||||
.cloned()
|
||||
.ok_or_else(|| "surface_result required".to_string())?;
|
||||
Ok(check_completeness(&req, &need, &authorities, &surface_result))
|
||||
Ok(check_completeness(
|
||||
&req,
|
||||
&need,
|
||||
&authorities,
|
||||
&surface_result,
|
||||
))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
@@ -199,10 +222,7 @@ pub extern "C" fn cdec_check_completeness(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_record_proposed_requirement(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let contract_id = a
|
||||
.get("contract_id")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
let contract_id = a.get("contract_id").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let provider = a.get("provider").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let model = a.get("model").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let evidence: Vec<Value> = a
|
||||
@@ -223,7 +243,10 @@ pub extern "C" fn cdec_record_proposed_requirement(arg: *const c_char) -> *mut c
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_research_route(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let req = a.get("req").cloned().ok_or_else(|| "req required".to_string())?;
|
||||
let req = a
|
||||
.get("req")
|
||||
.cloned()
|
||||
.ok_or_else(|| "req required".to_string())?;
|
||||
let decision = a
|
||||
.get("decision")
|
||||
.cloned()
|
||||
@@ -251,10 +274,7 @@ pub extern "C" fn cdec_merge_resolutions(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_needs_user_approval(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let resolution = a
|
||||
.get("resolution")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
let resolution = a.get("resolution").and_then(|s| s.as_str()).unwrap_or("");
|
||||
let toolchain_decision = a
|
||||
.get("toolchain_decision")
|
||||
.cloned()
|
||||
@@ -264,8 +284,16 @@ pub extern "C" fn cdec_needs_user_approval(arg: *const c_char) -> *mut c_char {
|
||||
.and_then(|x| x.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let need = a.get("need").cloned().ok_or_else(|| "need required".to_string())?;
|
||||
Ok(approval_gate(resolution, &toolchain_decision, &layout_obligations, &need))
|
||||
let need = a
|
||||
.get("need")
|
||||
.cloned()
|
||||
.ok_or_else(|| "need required".to_string())?;
|
||||
Ok(approval_gate(
|
||||
resolution,
|
||||
&toolchain_decision,
|
||||
&layout_obligations,
|
||||
&need,
|
||||
))
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
@@ -292,14 +320,15 @@ pub extern "C" fn cdec_project_exists_evidence(arg: *const c_char) -> *mut c_cha
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_make_decision(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let need = a.get("need").cloned().ok_or_else(|| "need required".to_string())?;
|
||||
let need = a
|
||||
.get("need")
|
||||
.cloned()
|
||||
.ok_or_else(|| "need required".to_string())?;
|
||||
let authorities = a.get("authorities").cloned().unwrap_or(json!({}));
|
||||
let project_toolchain = a.get("project_toolchain");
|
||||
let prior_iterations = a.get("prior_iterations");
|
||||
let extra_patterns: Option<Vec<Value>> = a
|
||||
.get("extra_patterns")
|
||||
.and_then(|x| x.as_array())
|
||||
.cloned();
|
||||
let extra_patterns: Option<Vec<Value>> =
|
||||
a.get("extra_patterns").and_then(|x| x.as_array()).cloned();
|
||||
make_decision(
|
||||
&need,
|
||||
&authorities,
|
||||
@@ -315,7 +344,10 @@ pub extern "C" fn cdec_make_decision(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_layout_obligations_for(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let doc = a.get("doc").cloned().ok_or_else(|| "doc required".to_string())?;
|
||||
let doc = a
|
||||
.get("doc")
|
||||
.cloned()
|
||||
.ok_or_else(|| "doc required".to_string())?;
|
||||
let affected: Vec<Value> = a
|
||||
.get("affected")
|
||||
.and_then(|x| x.as_array())
|
||||
@@ -330,7 +362,10 @@ pub extern "C" fn cdec_layout_obligations_for(arg: *const c_char) -> *mut c_char
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cdec_obligations_from_trace(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let trace = a.get("trace").cloned().ok_or_else(|| "trace required".to_string())?;
|
||||
let trace = a
|
||||
.get("trace")
|
||||
.cloned()
|
||||
.ok_or_else(|| "trace required".to_string())?;
|
||||
let contracts: Vec<String> = a
|
||||
.get("contracts")
|
||||
.and_then(|x| x.as_array())
|
||||
|
||||
@@ -150,8 +150,7 @@ pub fn need_new(d: &Value) -> Result<Value, String> {
|
||||
let objective = obj.get("objective").and_then(|x| x.as_str()).unwrap_or("");
|
||||
if objective.is_empty() {
|
||||
return Err(
|
||||
"objective (required effect) required — do not normalize a need we cannot state"
|
||||
.into(),
|
||||
"objective (required effect) required — do not normalize a need we cannot state".into(),
|
||||
);
|
||||
}
|
||||
Ok(need_to_dict(d))
|
||||
@@ -432,8 +431,8 @@ const STRUCTURAL_PREFIXES: [&str; 7] = [
|
||||
];
|
||||
const WORKFLOW_EXTS: [&str; 4] = [".yml", ".yaml", ".json", ".toml"];
|
||||
const SRC_EXTS: [&str; 20] = [
|
||||
".py", ".js", ".ts", ".jsx", ".tsx", ".rs", ".md", ".go", ".java", ".c", ".cpp", ".h",
|
||||
".css", ".html", ".yaml", ".yml", ".mjs", ".cjs", ".mts", ".cts",
|
||||
".py", ".js", ".ts", ".jsx", ".tsx", ".rs", ".md", ".go", ".java", ".c", ".cpp", ".h", ".css",
|
||||
".html", ".yaml", ".yml", ".mjs", ".cjs", ".mts", ".cts",
|
||||
];
|
||||
|
||||
impl SurfaceKind {
|
||||
@@ -471,7 +470,8 @@ pub fn classify_surface(surface: &str) -> &'static str {
|
||||
/// record HOW the mapping was established (identity | path_attribute |
|
||||
/// layout_contract).
|
||||
pub fn resolve_surface(surface: &str, kind: &str, authorities: &Value) -> Value {
|
||||
let mut out = json!({"surface": surface, "kind": kind, "resolved": [], "unresolved_reason": ""});
|
||||
let mut out =
|
||||
json!({"surface": surface, "kind": kind, "resolved": [], "unresolved_reason": ""});
|
||||
let binding = empty_map();
|
||||
let auth_map = authorities.as_object().unwrap_or(&binding);
|
||||
match SurfaceKind::from_str_kind(kind) {
|
||||
@@ -607,7 +607,10 @@ pub fn resolve_affected_surface(need: &Value, authorities: &Value) -> Value {
|
||||
for surface in &affected_surface {
|
||||
let kind = SurfaceKind::classify(surface);
|
||||
let res = resolve_surface(surface, kind.as_str(), authorities);
|
||||
result["resolutions"].as_array_mut().unwrap().push(res.clone());
|
||||
result["resolutions"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(res.clone());
|
||||
match kind {
|
||||
SurfaceKind::SourcePath => result["affected_paths"]
|
||||
.as_array_mut()
|
||||
@@ -1013,10 +1016,7 @@ pub fn needs_user_approval(
|
||||
let mut covered = !approval_context.is_empty() && reasons.is_empty();
|
||||
if !reasons.is_empty()
|
||||
&& !approval_context.is_empty()
|
||||
&& constraints
|
||||
.get("approval_covers")
|
||||
.and_then(|x| x.as_str())
|
||||
== Some("all")
|
||||
&& constraints.get("approval_covers").and_then(|x| x.as_str()) == Some("all")
|
||||
{
|
||||
covered = true;
|
||||
reasons = reasons
|
||||
@@ -1131,7 +1131,13 @@ fn consumers_of(doc: &Value, eid: &str) -> Vec<String> {
|
||||
/// (open-world kinds stay discoverable). Names are NOT fit.
|
||||
fn cf_discover_candidates(doc: &Value, environment: &Value) -> Vec<Value> {
|
||||
const BASE_KINDS: [&str; 7] = [
|
||||
"capability", "module", "component", "service", "cli", "tool", "workflow",
|
||||
"capability",
|
||||
"module",
|
||||
"component",
|
||||
"service",
|
||||
"cli",
|
||||
"tool",
|
||||
"workflow",
|
||||
];
|
||||
let mut candidates: Vec<Value> = vec![];
|
||||
let Some(ents) = doc.get("entities").and_then(|e| e.as_array()) else {
|
||||
@@ -1292,7 +1298,10 @@ fn cf_decide(
|
||||
let mut best = exact[0];
|
||||
for f in &exact[1..] {
|
||||
if f.get("maturity_rank").and_then(|x| x.as_i64()).unwrap_or(0)
|
||||
> best.get("maturity_rank").and_then(|x| x.as_i64()).unwrap_or(0)
|
||||
> best
|
||||
.get("maturity_rank")
|
||||
.and_then(|x| x.as_i64())
|
||||
.unwrap_or(0)
|
||||
{
|
||||
best = f;
|
||||
}
|
||||
@@ -1409,7 +1418,11 @@ fn cf_decide(
|
||||
json!({"incompatible": incompatible.iter().map(|f| f["candidate"].clone()).collect::<Vec<_>>()}),
|
||||
);
|
||||
}
|
||||
result("INSUFFICIENT_EVIDENCE", "no usable fit evidence".into(), json!({}))
|
||||
result(
|
||||
"INSUFFICIENT_EVIDENCE",
|
||||
"no usable fit evidence".into(),
|
||||
json!({}),
|
||||
)
|
||||
}
|
||||
|
||||
// ── layout obligations (§19-20) ─────────────────────────────────────────────
|
||||
@@ -1433,7 +1446,10 @@ pub fn layout_obligations_for(doc: &Value, affected: &[Value]) -> Vec<Value> {
|
||||
continue;
|
||||
}
|
||||
let ident = e.get("identities").cloned().unwrap_or(json!({}));
|
||||
let comp = ident.get("component").and_then(|x| x.as_str()).unwrap_or("");
|
||||
let comp = ident
|
||||
.get("component")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("");
|
||||
let region = ident.get("region").and_then(|x| x.as_str()).unwrap_or("");
|
||||
let impl_path = ident
|
||||
.get("implementation")
|
||||
@@ -1532,7 +1548,9 @@ pub fn obligations_from_trace(trace: &Value, req_contracts: &[String]) -> Value
|
||||
obligations["verification"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!("regression proof: original capability's consumers still work"));
|
||||
.push(json!(
|
||||
"regression proof: original capability's consumers still work"
|
||||
));
|
||||
}
|
||||
if resolution == "COMPOSE" {
|
||||
let components = merged.get("components").cloned().unwrap_or(json!(selected));
|
||||
@@ -1554,7 +1572,9 @@ pub fn obligations_from_trace(trace: &Value, req_contracts: &[String]) -> Value
|
||||
obligations["verification"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!("external acquisition requires research/approval step before install"));
|
||||
.push(json!(
|
||||
"external acquisition requires research/approval step before install"
|
||||
));
|
||||
}
|
||||
if resolution == "BUILD_NEW" {
|
||||
obligations["verification"]
|
||||
@@ -1634,7 +1654,10 @@ pub fn make_decision(
|
||||
None,
|
||||
);
|
||||
for ev in &existence_evidence {
|
||||
mode_decision["evidence"].as_array_mut().unwrap().push(json!(ev));
|
||||
mode_decision["evidence"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!(ev));
|
||||
}
|
||||
trace["construction_mode"] = mode_decision;
|
||||
trace["project_existence"] = json!({
|
||||
@@ -1698,14 +1721,23 @@ pub fn make_decision(
|
||||
}
|
||||
if CompletenessVerdict::from_verdict(completeness["verdict"].as_str().unwrap_or(""))
|
||||
== Some(CompletenessVerdict::MaterialGap)
|
||||
&& ["SATISFIED", "REUSE", "COMPOSE", "ADAPT_EXISTING", "CONFIGURE"]
|
||||
.contains(&merged["resolution"].as_str().unwrap_or(""))
|
||||
&& [
|
||||
"SATISFIED",
|
||||
"REUSE",
|
||||
"COMPOSE",
|
||||
"ADAPT_EXISTING",
|
||||
"CONFIGURE",
|
||||
]
|
||||
.contains(&merged["resolution"].as_str().unwrap_or(""))
|
||||
{
|
||||
// PREMATURE-FIT GUARD (Tranche 5 §6-7): the fit engine was correct
|
||||
// GIVEN its requirement — but the requirement is materially
|
||||
// incomplete (affected surface carries unrepresented obligations).
|
||||
// A concrete verdict on an incomplete requirement is not trustworthy.
|
||||
let gap_count = completeness["gaps"].as_array().map(|g| g.len()).unwrap_or(0);
|
||||
let gap_count = completeness["gaps"]
|
||||
.as_array()
|
||||
.map(|g| g.len())
|
||||
.unwrap_or(0);
|
||||
let would_have_been = merged["resolution"].clone();
|
||||
merged = json!({
|
||||
"resolution": "INSUFFICIENT_EVIDENCE",
|
||||
@@ -1777,7 +1809,10 @@ pub fn make_decision(
|
||||
// Project evidence > pattern assumption (§3).
|
||||
let mut pattern_candidates: Vec<Value> = construction_patterns_builtin::registry::patterns()
|
||||
.iter()
|
||||
.map(|p| serde_json::from_str::<Value>(&capability_scoring::pyjson::dumps(p)).unwrap_or(Value::Null))
|
||||
.map(|p| {
|
||||
serde_json::from_str::<Value>(&capability_scoring::pyjson::dumps(p))
|
||||
.unwrap_or(Value::Null)
|
||||
})
|
||||
.collect();
|
||||
if let Some(extra) = extra_patterns {
|
||||
pattern_candidates.extend(extra.iter().cloned());
|
||||
@@ -1792,9 +1827,15 @@ pub fn make_decision(
|
||||
let mut patterns_considered: Vec<Value> = vec![];
|
||||
let mut derivations: Vec<Value> = vec![];
|
||||
for pattern in &pattern_candidates {
|
||||
let Some(p_obj) = pattern.as_object() else { continue };
|
||||
let app =
|
||||
construction_pattern::evaluate_applicability(p_obj, &need_map, &surface_map, &auth_map2);
|
||||
let Some(p_obj) = pattern.as_object() else {
|
||||
continue;
|
||||
};
|
||||
let app = construction_pattern::evaluate_applicability(
|
||||
p_obj,
|
||||
&need_map,
|
||||
&surface_map,
|
||||
&auth_map2,
|
||||
);
|
||||
patterns_considered.push(app.clone());
|
||||
// APPLIES and PARTIAL both reach obligation derivation: applicability
|
||||
// gates whether the pattern is CONSULTED; the per-obligation `when`
|
||||
@@ -1842,7 +1883,9 @@ pub fn make_decision(
|
||||
obligations["verification"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!(format!("[pattern:{pattern}@r{revision}] {obligation}")));
|
||||
.push(json!(format!(
|
||||
"[pattern:{pattern}@r{revision}] {obligation}"
|
||||
)));
|
||||
}
|
||||
trace["obligations"] = obligations;
|
||||
Ok(trace)
|
||||
@@ -1862,10 +1905,7 @@ pub struct CdecCore;
|
||||
/// None; every other value resolves the core. The oracle's load-failure
|
||||
/// branch can't fire here — in-process is always loaded.
|
||||
pub fn _cdec() -> Option<CdecCore> {
|
||||
if std::env::var("RUNE_CONSTRUCTION_DECISION_CORE")
|
||||
.unwrap_or_default()
|
||||
== "python"
|
||||
{
|
||||
if std::env::var("RUNE_CONSTRUCTION_DECISION_CORE").unwrap_or_default() == "python" {
|
||||
None
|
||||
} else {
|
||||
Some(CdecCore)
|
||||
|
||||
@@ -236,7 +236,9 @@ pub fn select_depth(
|
||||
ceremony.insert(0, "evidence probe".into());
|
||||
}
|
||||
if affected_surface_len > 5 {
|
||||
ev.push(format!("wide affected surface: {affected_surface_len} entries"));
|
||||
ev.push(format!(
|
||||
"wide affected surface: {affected_surface_len} entries"
|
||||
));
|
||||
level = Level::Full;
|
||||
reason = "wide surface — full depth warranted".into();
|
||||
}
|
||||
@@ -268,10 +270,7 @@ fn read_arg(ptr: *const c_char) -> Result<Json, String> {
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Ok(x) => Json::Obj(vec![("ok".into(), Json::Bool(true)), ("value".into(), x)]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
|
||||
@@ -114,12 +114,27 @@ fn audit_missing_fns_are_real_ports() {
|
||||
})];
|
||||
let (cont, after) = kernel::programme_continuation(&progs, "S1", "2026-10-13T09:41:23Z");
|
||||
assert_eq!(cont["stage_marked_complete"], serde_json::json!("S1"));
|
||||
assert_eq!(cont["canary"]["verdict"], serde_json::json!("PROGRAMME_OPEN"));
|
||||
assert_eq!(cont["canary"]["next_action"], serde_json::json!(["start", "S2"]));
|
||||
assert_eq!(cont["canary"]["reason"], serde_json::json!("1/2 complete; 1 eligible — continue immediately"));
|
||||
assert_eq!(
|
||||
cont["canary"]["verdict"],
|
||||
serde_json::json!("PROGRAMME_OPEN")
|
||||
);
|
||||
assert_eq!(
|
||||
cont["canary"]["next_action"],
|
||||
serde_json::json!(["start", "S2"])
|
||||
);
|
||||
assert_eq!(
|
||||
cont["canary"]["reason"],
|
||||
serde_json::json!("1/2 complete; 1 eligible — continue immediately")
|
||||
);
|
||||
let after = after.as_array().unwrap();
|
||||
assert_eq!(after[0]["stages"][0]["state"], serde_json::json!("COMPLETE"));
|
||||
assert_eq!(
|
||||
after[0]["stages"][0]["state"],
|
||||
serde_json::json!("COMPLETE")
|
||||
);
|
||||
// save drops the in-memory result — the persisted shape is 5-key stages
|
||||
assert!(after[0]["stages"][0].get("result").is_none());
|
||||
assert_eq!(after[0]["saved_at"], serde_json::json!("2026-10-13T09:41:23Z"));
|
||||
assert_eq!(
|
||||
after[0]["saved_at"],
|
||||
serde_json::json!("2026-10-13T09:41:23Z")
|
||||
);
|
||||
}
|
||||
@@ -67,13 +67,10 @@ fn find_contract<'a>(doc: &'a Value, contract_id: &str) -> Option<&'a Value> {
|
||||
/// callers re-find rather than cache).
|
||||
fn contract_pos(doc: &Value, contract_id: &str) -> Option<usize> {
|
||||
let want = format!("contract:{}", contract_id);
|
||||
doc.get("entities")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.position(|e| {
|
||||
e.get("kind").and_then(|k| k.as_str()) == Some("construction_contract")
|
||||
&& e.get("id").and_then(|i| i.as_str()) == Some(want.as_str())
|
||||
})
|
||||
doc.get("entities")?.as_array()?.iter().position(|e| {
|
||||
e.get("kind").and_then(|k| k.as_str()) == Some("construction_contract")
|
||||
&& e.get("id").and_then(|i| i.as_str()) == Some(want.as_str())
|
||||
})
|
||||
}
|
||||
|
||||
/// The single-error failure envelope every contract-scoped operation
|
||||
@@ -128,14 +125,16 @@ pub fn record_session(
|
||||
.expect("doc is an object")
|
||||
.entry("observations")
|
||||
.or_insert(json!([]));
|
||||
obs.as_array_mut().expect("observations is a list").push(json!({
|
||||
"kind": "builder_session",
|
||||
"builder": builder,
|
||||
"mode": mode,
|
||||
"operation": operation,
|
||||
"detail": projection::py_truncate(detail, 200),
|
||||
"observed_at": now,
|
||||
}));
|
||||
obs.as_array_mut()
|
||||
.expect("observations is a list")
|
||||
.push(json!({
|
||||
"kind": "builder_session",
|
||||
"builder": builder,
|
||||
"mode": mode,
|
||||
"operation": operation,
|
||||
"detail": projection::py_truncate(detail, 200),
|
||||
"observed_at": now,
|
||||
}));
|
||||
doc
|
||||
}
|
||||
|
||||
@@ -175,10 +174,7 @@ pub fn inspect_decision(doc: &Value, contract_id: &str, builder: &str) -> Value
|
||||
let obl = t.get("obligations").cloned().unwrap_or(json!({}));
|
||||
let mut structural = serde_json::Map::new();
|
||||
for k in ["regression", "verification", "composition", "acquisition"] {
|
||||
structural.insert(
|
||||
k.to_string(),
|
||||
obl.get(k).cloned().unwrap_or(json!([])),
|
||||
);
|
||||
structural.insert(k.to_string(), obl.get(k).cloned().unwrap_or(json!([])));
|
||||
}
|
||||
let state = json!({
|
||||
"requirement": t.get("requirement").cloned().unwrap_or(json!({})),
|
||||
@@ -276,13 +272,23 @@ pub fn report_observation(
|
||||
}
|
||||
let Some(items_in) = observations.as_array() else {
|
||||
return (
|
||||
fail_envelope("report_observation", contract_id, "NEED_INCOMPLETE", "observations list required"),
|
||||
fail_envelope(
|
||||
"report_observation",
|
||||
contract_id,
|
||||
"NEED_INCOMPLETE",
|
||||
"observations list required",
|
||||
),
|
||||
vec![],
|
||||
);
|
||||
};
|
||||
if items_in.is_empty() {
|
||||
return (
|
||||
fail_envelope("report_observation", contract_id, "NEED_INCOMPLETE", "observations list required"),
|
||||
fail_envelope(
|
||||
"report_observation",
|
||||
contract_id,
|
||||
"NEED_INCOMPLETE",
|
||||
"observations list required",
|
||||
),
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
@@ -291,14 +297,16 @@ pub fn report_observation(
|
||||
let entity = o.get("entity").and_then(|e| e.as_str()).unwrap_or("");
|
||||
if !o.is_object() || entity.is_empty() {
|
||||
return (
|
||||
fail_envelope("report_observation", contract_id, "NEED_INCOMPLETE", "each observation needs an entity"),
|
||||
fail_envelope(
|
||||
"report_observation",
|
||||
contract_id,
|
||||
"NEED_INCOMPLETE",
|
||||
"each observation needs an entity",
|
||||
),
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
let detail = o
|
||||
.get("detail")
|
||||
.map(projection::py_str)
|
||||
.unwrap_or_default();
|
||||
let detail = o.get("detail").map(projection::py_str).unwrap_or_default();
|
||||
items.push(json!({
|
||||
"entity": entity,
|
||||
"source": o.get("source").and_then(|s| s.as_str())
|
||||
@@ -355,7 +363,8 @@ pub fn amend_scope(
|
||||
let mut doc = doc.clone();
|
||||
let Some(pos) = contract_pos(&doc, contract_id) else {
|
||||
return (unknown_contract("amend_scope", contract_id), doc);
|
||||
}; let mut scope: Vec<String> = doc["entities"][pos]["identities"]["scope"]
|
||||
};
|
||||
let mut scope: Vec<String> = doc["entities"][pos]["identities"]["scope"]
|
||||
.as_array()
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
@@ -423,9 +432,7 @@ pub fn construction_context(
|
||||
.and_then(|e| e.as_array())
|
||||
.map(|ents| {
|
||||
ents.iter()
|
||||
.filter(|e| {
|
||||
e.get("kind").and_then(|k| k.as_str()) == Some("construction_contract")
|
||||
})
|
||||
.filter(|e| e.get("kind").and_then(|k| k.as_str()) == Some("construction_contract"))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
@@ -436,9 +443,9 @@ pub fn construction_context(
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
e.get("status").and_then(|s| s.as_str()) == Some("open")
|
||||
&& !terminal_phases.iter().any(|p| {
|
||||
Some(p.as_str()) == e.get("phase").and_then(|x| x.as_str())
|
||||
})
|
||||
&& !terminal_phases
|
||||
.iter()
|
||||
.any(|p| Some(p.as_str()) == e.get("phase").and_then(|x| x.as_str()))
|
||||
})
|
||||
.collect();
|
||||
open.sort_by_key(|e| {
|
||||
@@ -484,7 +491,10 @@ pub fn construction_context(
|
||||
.map(|(_, rest)| rest)
|
||||
.unwrap_or("")),
|
||||
);
|
||||
ctx.insert("need".into(), json!(projection::py_truncate(&need_obj, 200)));
|
||||
ctx.insert(
|
||||
"need".into(),
|
||||
json!(projection::py_truncate(&need_obj, 200)),
|
||||
);
|
||||
ctx.insert(
|
||||
"resolution".into(),
|
||||
t.get("resolution").cloned().unwrap_or(json!("")),
|
||||
@@ -496,8 +506,14 @@ pub fn construction_context(
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
ctx.insert("phase".into(), target.get("phase").cloned().unwrap_or(Value::Null));
|
||||
ctx.insert("status".into(), target.get("status").cloned().unwrap_or(Value::Null));
|
||||
ctx.insert(
|
||||
"phase".into(),
|
||||
target.get("phase").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
ctx.insert(
|
||||
"status".into(),
|
||||
target.get("status").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
ctx.insert(
|
||||
"scope".into(),
|
||||
target
|
||||
@@ -518,7 +534,10 @@ pub fn construction_context(
|
||||
.cloned()
|
||||
.unwrap_or(json!([])),
|
||||
);
|
||||
ctx.insert("approval".into(), t.get("approval").cloned().unwrap_or(json!({})));
|
||||
ctx.insert(
|
||||
"approval".into(),
|
||||
t.get("approval").cloned().unwrap_or(json!({})),
|
||||
);
|
||||
ctx.insert(
|
||||
"completeness".into(),
|
||||
t.get("completeness")
|
||||
@@ -744,17 +763,18 @@ pub fn discover_active_contract(candidates: &Value, root_name: &str) -> Value {
|
||||
if e.get("kind").and_then(|k| k.as_str()) != Some("construction_contract") {
|
||||
continue;
|
||||
}
|
||||
let proj = project
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| {
|
||||
e.get("identities")
|
||||
.and_then(|i| i.get("project"))
|
||||
.and_then(|p| p.as_str())
|
||||
.unwrap_or(root_name)
|
||||
.to_string()
|
||||
});
|
||||
let proj = project.map(str::to_string).unwrap_or_else(|| {
|
||||
e.get("identities")
|
||||
.and_then(|i| i.get("project"))
|
||||
.and_then(|p| p.as_str())
|
||||
.unwrap_or(root_name)
|
||||
.to_string()
|
||||
});
|
||||
let key = (
|
||||
e.get("observed_at").and_then(|o| o.as_str()).unwrap_or("").to_string(),
|
||||
e.get("observed_at")
|
||||
.and_then(|o| o.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
proj.clone(),
|
||||
e.get("id")
|
||||
.and_then(|i| i.as_str())
|
||||
@@ -763,7 +783,11 @@ pub fn discover_active_contract(candidates: &Value, root_name: &str) -> Value {
|
||||
.map(|(_, rest)| rest.to_string())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
if best.as_ref().map(|b| key > (b.0.clone(), b.1.clone(), b.2.clone())).unwrap_or(true) {
|
||||
if best
|
||||
.as_ref()
|
||||
.map(|b| key > (b.0.clone(), b.1.clone(), b.2.clone()))
|
||||
.unwrap_or(true)
|
||||
{
|
||||
best = Some(key);
|
||||
best_project = proj;
|
||||
}
|
||||
@@ -854,10 +878,7 @@ pub fn submit_need(
|
||||
)
|
||||
.unwrap_or(json!({}));
|
||||
let mut ids = serde_json::Map::new();
|
||||
ids.insert(
|
||||
"need_id".to_string(),
|
||||
need["need_id"].clone(),
|
||||
);
|
||||
ids.insert("need_id".to_string(), need["need_id"].clone());
|
||||
let resolution = trace
|
||||
.get("resolution")
|
||||
.and_then(|r| r.as_str())
|
||||
@@ -947,7 +968,14 @@ pub fn submit_need(
|
||||
need["need_id"].as_str().unwrap_or(""),
|
||||
resolution
|
||||
);
|
||||
let doc = record_session(&doc, "submit_need", &detail, builder, "external_project", now);
|
||||
let doc = record_session(
|
||||
&doc,
|
||||
"submit_need",
|
||||
&detail,
|
||||
builder,
|
||||
"external_project",
|
||||
now,
|
||||
);
|
||||
let doc = normalize_sorted(doc);
|
||||
let env = projection::envelope(
|
||||
"submit_need",
|
||||
@@ -965,7 +993,13 @@ pub fn submit_need(
|
||||
/// `generate_contract(doc, trace, contract_id, now, builder)` — the
|
||||
/// construction_decision.generate_contract + scaffold.new_contract twin
|
||||
/// (mutates the carried doc).
|
||||
pub fn generate_contract(doc: &mut Value, trace: &Value, contract_id: &str, now: &str, builder: &str) {
|
||||
pub fn generate_contract(
|
||||
doc: &mut Value,
|
||||
trace: &Value,
|
||||
contract_id: &str,
|
||||
now: &str,
|
||||
builder: &str,
|
||||
) {
|
||||
let merged = trace.get("_merged").cloned().unwrap_or(json!({}));
|
||||
let req = trace.get("requirement").cloned().unwrap_or(json!({}));
|
||||
let surfaces = trace.get("affected_surfaces").cloned().unwrap_or(json!({}));
|
||||
@@ -1197,7 +1231,10 @@ pub fn generate_contract(doc: &mut Value, trace: &Value, contract_id: &str, now:
|
||||
}
|
||||
|
||||
fn resolution_of(trace: &Value) -> &str {
|
||||
trace.get("resolution").and_then(|r| r.as_str()).unwrap_or("")
|
||||
trace
|
||||
.get("resolution")
|
||||
.and_then(|r| r.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
fn sorted_strs(v: Option<&Value>) -> Vec<String> {
|
||||
@@ -1268,9 +1305,11 @@ pub fn check_scope(doc: &Value, contract_id: &str, changed_files: &[String]) ->
|
||||
let mut in_scope = Vec::new();
|
||||
let mut out_scope = Vec::new();
|
||||
for f in changed_files {
|
||||
let ok = scope
|
||||
.iter()
|
||||
.any(|s| f == s || f.starts_with(&format!("{}/", s.trim_end_matches('/'))) || f.contains(s.as_str()));
|
||||
let ok = scope.iter().any(|s| {
|
||||
f == s
|
||||
|| f.starts_with(&format!("{}/", s.trim_end_matches('/')))
|
||||
|| f.contains(s.as_str())
|
||||
});
|
||||
if ok {
|
||||
in_scope.push(f.clone());
|
||||
} else {
|
||||
@@ -1335,7 +1374,10 @@ pub fn verify(
|
||||
}
|
||||
if let Some(obs) = layout_observations {
|
||||
let gate = layout_contract::check_layout(
|
||||
layout_contracts.as_array().map(|a| a.as_slice()).unwrap_or(&[]),
|
||||
layout_contracts
|
||||
.as_array()
|
||||
.map(|a| a.as_slice())
|
||||
.unwrap_or(&[]),
|
||||
obs,
|
||||
);
|
||||
state.insert(
|
||||
@@ -1481,7 +1523,12 @@ pub fn transition_doc(
|
||||
let current = c.get("phase").and_then(|p| p.as_str()).unwrap_or("draft");
|
||||
if !PHASES.contains(&phase) {
|
||||
return (
|
||||
fail_envelope("transition", contract_id, "UNKNOWN_CONTRACT", &format!("unknown phase: {}", phase)),
|
||||
fail_envelope(
|
||||
"transition",
|
||||
contract_id,
|
||||
"UNKNOWN_CONTRACT",
|
||||
&format!("unknown phase: {}", phase),
|
||||
),
|
||||
doc.clone(),
|
||||
);
|
||||
}
|
||||
@@ -1506,7 +1553,11 @@ pub fn transition_doc(
|
||||
"illegal transition: {} → {} (legal: [{}])",
|
||||
current,
|
||||
phase,
|
||||
legal.iter().map(|s| format!("'{}'", s)).collect::<Vec<_>>().join(", ")
|
||||
legal
|
||||
.iter()
|
||||
.map(|s| format!("'{}'", s))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
),
|
||||
),
|
||||
doc.clone(),
|
||||
@@ -1617,7 +1668,12 @@ fn normalize_sorted_ref(v: &Value) -> Value {
|
||||
|
||||
/// `record_observed_impact(doc, contract_id, items, now)` — the
|
||||
/// structural_store.record_observed_impact twin (mutates the carried doc).
|
||||
pub fn record_observed_impact(doc: &mut Value, contract_id: &str, items: &[Value], now: &str) -> Value {
|
||||
pub fn record_observed_impact(
|
||||
doc: &mut Value,
|
||||
contract_id: &str,
|
||||
items: &[Value],
|
||||
now: &str,
|
||||
) -> Value {
|
||||
let mut implicated: Vec<String> = Vec::new();
|
||||
for item in items {
|
||||
let eid = item.get("entity").and_then(|e| e.as_str()).unwrap_or("");
|
||||
@@ -1693,7 +1749,11 @@ pub fn close_quality(inputs: &Value) -> Value {
|
||||
let expected_new_missing = set_minus(&expected_new, &obs_set);
|
||||
let matched_runtime: Vec<String> = obs_set
|
||||
.iter()
|
||||
.filter(|o| expected_runtime.iter().any(|eff| eff.starts_with(o.as_str())))
|
||||
.filter(|o| {
|
||||
expected_runtime
|
||||
.iter()
|
||||
.any(|eff| eff.starts_with(o.as_str()))
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let matched_runtime = sorted_set(matched_runtime);
|
||||
@@ -1909,7 +1969,8 @@ pub fn close(
|
||||
) -> (Value, Value, Value, Value) {
|
||||
let mut doc = doc.clone();
|
||||
let scope_vec: Vec<String> = scope_files.map(|s| s.to_vec()).unwrap_or_default();
|
||||
let (material, ledger_after) = auto_material_change(&doc, &scope_vec, ledger_doc, builder, project, now);
|
||||
let (material, ledger_after) =
|
||||
auto_material_change(&doc, &scope_vec, ledger_doc, builder, project, now);
|
||||
let (continuation, programmes_after) = programme_continuation(programmes, contract_id, now);
|
||||
match close_contract(
|
||||
&mut doc,
|
||||
@@ -2004,7 +2065,10 @@ pub fn auto_material_change(
|
||||
result.insert("changed".to_string(), json!(changed));
|
||||
result.insert("unmodeled".to_string(), json!(unmodeled));
|
||||
if changed.is_empty() && scope_files.is_empty() {
|
||||
result.insert("note".to_string(), json!("no capability ledger for this project"));
|
||||
result.insert(
|
||||
"note".to_string(),
|
||||
json!("no capability ledger for this project"),
|
||||
);
|
||||
}
|
||||
(Value::Object(result), doc)
|
||||
}
|
||||
@@ -2042,9 +2106,8 @@ pub fn continuation_canary(prog: &Value) -> Value {
|
||||
.get("depends_on")
|
||||
.and_then(|d| d.as_array())
|
||||
.map(|deps| {
|
||||
deps.iter().all(|d| {
|
||||
d.as_str().map(|id| done.contains(&id)).unwrap_or(false)
|
||||
})
|
||||
deps.iter()
|
||||
.all(|d| d.as_str().map(|id| done.contains(&id)).unwrap_or(false))
|
||||
})
|
||||
.unwrap_or(true)
|
||||
})
|
||||
@@ -2052,7 +2115,10 @@ pub fn continuation_canary(prog: &Value) -> Value {
|
||||
eligible.sort_by_key(|s| {
|
||||
(
|
||||
s.get("priority").and_then(|p| p.as_i64()).unwrap_or(0),
|
||||
s.get("stage_id").and_then(|x| x.as_str()).unwrap_or("").to_string(),
|
||||
s.get("stage_id")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
if let Some(nxt) = eligible.first() {
|
||||
@@ -2069,7 +2135,10 @@ pub fn continuation_canary(prog: &Value) -> Value {
|
||||
.filter(|s| s.get("state").and_then(|x| x.as_str()) == Some("WAITING_EVENT"))
|
||||
.collect();
|
||||
if !waiting.is_empty() {
|
||||
let sid = waiting[0].get("stage_id").and_then(|x| x.as_str()).unwrap_or("");
|
||||
let sid = waiting[0]
|
||||
.get("stage_id")
|
||||
.and_then(|x| x.as_str())
|
||||
.unwrap_or("");
|
||||
return json!({
|
||||
"verdict": "PROGRAMME_OPEN_WAITING",
|
||||
"next_action": ["await_event", sid],
|
||||
@@ -2115,14 +2184,20 @@ pub fn continuation_canary(prog: &Value) -> Value {
|
||||
});
|
||||
}
|
||||
let terminal = ["COMPLETE", "BLOCKED", "APPROVAL_REQUIRED", "FAILED"];
|
||||
if stages
|
||||
.iter()
|
||||
.all(|s| s.get("state").and_then(|x| x.as_str()).map(|st| terminal.contains(&st)).unwrap_or(false))
|
||||
{
|
||||
if stages.iter().all(|s| {
|
||||
s.get("state")
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|st| terminal.contains(&st))
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
let failed: Vec<String> = stages
|
||||
.iter()
|
||||
.filter(|s| s.get("state").and_then(|x| x.as_str()) == Some("FAILED"))
|
||||
.filter_map(|s| s.get("stage_id").and_then(|x| x.as_str()).map(str::to_string))
|
||||
.filter_map(|s| {
|
||||
s.get("stage_id")
|
||||
.and_then(|x| x.as_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
.collect();
|
||||
return json!({
|
||||
"verdict": "PROGRAMME_COMPLETE_WITH_FAILURES",
|
||||
@@ -2143,15 +2218,17 @@ pub fn continuation_canary(prog: &Value) -> Value {
|
||||
/// the persisted-after shape is the Programme.save projection (5-key
|
||||
/// stages, fallbacks, saved_at — the in-memory result never persists).
|
||||
/// Returns (continuation|null, programmes_after|null).
|
||||
pub fn programme_continuation(programmes: &[Value], contract_id: &str, now: &str) -> (Value, Value) {
|
||||
pub fn programme_continuation(
|
||||
programmes: &[Value],
|
||||
contract_id: &str,
|
||||
now: &str,
|
||||
) -> (Value, Value) {
|
||||
if programmes.is_empty() {
|
||||
return (Value::Null, Value::Null);
|
||||
}
|
||||
let mut loaded: Vec<Value> = programmes.iter().map(load_programme).collect();
|
||||
for prog in loaded.iter_mut() {
|
||||
let stages = prog
|
||||
.get_mut("stages")
|
||||
.and_then(|s| s.as_array_mut());
|
||||
let stages = prog.get_mut("stages").and_then(|s| s.as_array_mut());
|
||||
let Some(stages) = stages else { continue };
|
||||
let hit = stages.iter_mut().find(|s| {
|
||||
s.get("stage_id").and_then(|x| x.as_str()) == Some(contract_id)
|
||||
@@ -2250,7 +2327,8 @@ fn save_programme(p: &Value, now: &str) -> Value {
|
||||
/// `classify(objective, builder)` — the envelope assembly over the
|
||||
/// canonical construction_mode classifier (its port surface).
|
||||
pub fn classify_objective(objective: &str, builder: &str) -> Value {
|
||||
let d = construction_mode::classify_mode(objective, None, None, None, None, Some(builder), None);
|
||||
let d =
|
||||
construction_mode::classify_mode(objective, None, None, None, None, Some(builder), None);
|
||||
let mut state = d.clone();
|
||||
state
|
||||
.as_object_mut()
|
||||
@@ -2302,7 +2380,11 @@ pub fn main_dispatch(argv: &[String]) -> Value {
|
||||
/// battery and the ci_replay FFI: {fn, args, kwargs} → expected.
|
||||
pub fn dispatch(case: &Value) -> Value {
|
||||
let name = case.get("fn").and_then(|f| f.as_str()).unwrap_or("");
|
||||
let args = case.get("args").and_then(|a| a.as_array()).cloned().unwrap_or_default();
|
||||
let args = case
|
||||
.get("args")
|
||||
.and_then(|a| a.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let kwargs = case.get("kwargs").cloned().unwrap_or(json!({}));
|
||||
let get = |i: usize| args.get(i).cloned().unwrap_or(Value::Null);
|
||||
let kw = |k: &str| kwargs.get(k).cloned().unwrap_or(Value::Null);
|
||||
@@ -2332,14 +2414,13 @@ pub fn dispatch(case: &Value) -> Value {
|
||||
Value::Array(projection::guard_specs_from_obligations(&get(0)))
|
||||
}
|
||||
"t_layout" => projection::t_layout(&get(0)),
|
||||
"_obligations_projection" => {
|
||||
Value::Array(projection::obligations_projection(&get(0)))
|
||||
}
|
||||
"_obligation_scope_files" => {
|
||||
Value::Array(projection::obligation_scope_files(&get(0)))
|
||||
}
|
||||
"_obligations_projection" => Value::Array(projection::obligations_projection(&get(0))),
|
||||
"_obligation_scope_files" => Value::Array(projection::obligation_scope_files(&get(0))),
|
||||
"_authorities" => {
|
||||
let mode = kw("mode").as_str().unwrap_or("external_project").to_string();
|
||||
let mode = kw("mode")
|
||||
.as_str()
|
||||
.unwrap_or("external_project")
|
||||
.to_string();
|
||||
authorities(&get(0), &get(1), &mode)
|
||||
}
|
||||
"_record_session" => record_session(
|
||||
@@ -2350,10 +2431,7 @@ pub fn dispatch(case: &Value) -> Value {
|
||||
"external_project",
|
||||
"2026-10-13T09:41:23Z",
|
||||
),
|
||||
"classify" => classify(
|
||||
&get(0),
|
||||
kw("builder").as_str().unwrap_or("factory-gap-ci"),
|
||||
),
|
||||
"classify" => classify(&get(0), kw("builder").as_str().unwrap_or("factory-gap-ci")),
|
||||
"construction_context" => construction_context(
|
||||
&get(1),
|
||||
get(2).as_str(),
|
||||
@@ -2372,28 +2450,60 @@ pub fn dispatch(case: &Value) -> Value {
|
||||
let objective = get(2).as_str().unwrap_or("").to_string();
|
||||
let origin = kw("origin").as_str().unwrap_or("builder").to_string();
|
||||
let constraints = kw("constraints");
|
||||
let constraints = if constraints.is_null() { json!({}) } else { constraints };
|
||||
let constraints = if constraints.is_null() {
|
||||
json!({})
|
||||
} else {
|
||||
constraints
|
||||
};
|
||||
let affected = kw("affected_surface");
|
||||
let affected = if affected.is_null() { json!([]) } else { affected };
|
||||
let affected = if affected.is_null() {
|
||||
json!([])
|
||||
} else {
|
||||
affected
|
||||
};
|
||||
let evidence = kw("evidence_refs");
|
||||
let evidence = if evidence.is_null() { json!([]) } else { evidence };
|
||||
let evidence = if evidence.is_null() {
|
||||
json!([])
|
||||
} else {
|
||||
evidence
|
||||
};
|
||||
let approval = kw("approval_context").as_str().unwrap_or("").to_string();
|
||||
let task_ref = kw("task_ref").as_str().unwrap_or("").to_string();
|
||||
let generate = kw("generate").as_bool().unwrap_or(true);
|
||||
let cid = kw("contract_id");
|
||||
let cid = cid.as_str();
|
||||
let (env, _) = submit_need(
|
||||
doc, &objective, &origin, &constraints, &affected, &evidence,
|
||||
&approval, &task_ref, generate, cid,
|
||||
"factory-gap-ci", "gap-ci-proj", "2026-10-13T09:41:23Z", "000001",
|
||||
doc,
|
||||
&objective,
|
||||
&origin,
|
||||
&constraints,
|
||||
&affected,
|
||||
&evidence,
|
||||
&approval,
|
||||
&task_ref,
|
||||
generate,
|
||||
cid,
|
||||
"factory-gap-ci",
|
||||
"gap-ci-proj",
|
||||
"2026-10-13T09:41:23Z",
|
||||
"000001",
|
||||
);
|
||||
env
|
||||
}
|
||||
"inspect_decision" => inspect_decision(&get(1), get(2).as_str().unwrap_or(""), "factory-gap-ci"),
|
||||
"inspect_decision" => {
|
||||
inspect_decision(&get(1), get(2).as_str().unwrap_or(""), "factory-gap-ci")
|
||||
}
|
||||
"get_scope" => get_scope(&get(1), get(2).as_str().unwrap_or(""), "factory-gap-ci"),
|
||||
"get_obligations" => get_obligations(&get(1), get(2).as_str().unwrap_or(""), "factory-gap-ci"),
|
||||
"get_obligations" => {
|
||||
get_obligations(&get(1), get(2).as_str().unwrap_or(""), "factory-gap-ci")
|
||||
}
|
||||
"report_observation" => {
|
||||
let (env, _) = report_observation(&get(1), get(2).as_str().unwrap_or(""), &get(3), "factory-gap-ci");
|
||||
let (env, _) = report_observation(
|
||||
&get(1),
|
||||
get(2).as_str().unwrap_or(""),
|
||||
&get(3),
|
||||
"factory-gap-ci",
|
||||
);
|
||||
env
|
||||
}
|
||||
"verify" => {
|
||||
@@ -2447,22 +2557,42 @@ pub fn dispatch(case: &Value) -> Value {
|
||||
let mini_doc = format!("/tmp/ci_gap_freeze_home/cases/{}/docs/{}.md", case, cid);
|
||||
let evidence: Vec<Value> = get(3).as_array().cloned().unwrap_or_default();
|
||||
let (env, _, _, _) = close(
|
||||
&get(1), &cid, &evidence, scope.as_deref(), &mini_doc,
|
||||
None, &[], "factory-gap-ci", "external_project", "gap-ci-proj",
|
||||
&get(1),
|
||||
&cid,
|
||||
&evidence,
|
||||
scope.as_deref(),
|
||||
&mini_doc,
|
||||
None,
|
||||
&[],
|
||||
"factory-gap-ci",
|
||||
"external_project",
|
||||
"gap-ci-proj",
|
||||
"2026-10-13T09:41:23Z",
|
||||
);
|
||||
env
|
||||
}
|
||||
"_programme_continuation" => {
|
||||
let programmes: Vec<Value> = kw("programmes").as_array().cloned().unwrap_or_default();
|
||||
let (cont, after) = programme_continuation(&programmes, get(2).as_str().unwrap_or(""), "2026-10-13T09:41:23Z");
|
||||
let (cont, after) = programme_continuation(
|
||||
&programmes,
|
||||
get(2).as_str().unwrap_or(""),
|
||||
"2026-10-13T09:41:23Z",
|
||||
);
|
||||
json!({"continuation": cont, "programmes_after": after})
|
||||
}
|
||||
"_auto_material_change" => {
|
||||
let scope = strargs(&get(2));
|
||||
let caps = if kw("capabilities").is_null() { None } else { Some(kw("capabilities")) };
|
||||
let caps = if kw("capabilities").is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(kw("capabilities"))
|
||||
};
|
||||
let (result, after) = auto_material_change(
|
||||
&get(1), &scope, caps.as_ref(), "factory-gap-ci", "gap-ci-proj",
|
||||
&get(1),
|
||||
&scope,
|
||||
caps.as_ref(),
|
||||
"factory-gap-ci",
|
||||
"gap-ci-proj",
|
||||
"2026-10-13T09:41:23Z",
|
||||
);
|
||||
json!({"result": result, "ledger_after": after})
|
||||
@@ -2480,14 +2610,25 @@ pub fn dispatch(case: &Value) -> Value {
|
||||
.collect();
|
||||
discover_active_contract(&Value::Array(rows), "store")
|
||||
}
|
||||
"resume" => resume(&get(1), get(2).as_str().unwrap_or(""), "factory-gap-ci", "external_project"),
|
||||
"resume" => resume(
|
||||
&get(1),
|
||||
get(2).as_str().unwrap_or(""),
|
||||
"factory-gap-ci",
|
||||
"external_project",
|
||||
),
|
||||
"amend_scope" => {
|
||||
let paths = strargs(&get(3));
|
||||
let reason = kw("reason").as_str().unwrap_or("").to_string();
|
||||
let authority = kw("authority").as_str().unwrap_or("user").to_string();
|
||||
let (env, after) = amend_scope(
|
||||
&get(1), get(2).as_str().unwrap_or(""), &paths, &reason,
|
||||
&authority, "factory-gap-ci", "external_project", "2026-10-13T09:41:23Z",
|
||||
&get(1),
|
||||
get(2).as_str().unwrap_or(""),
|
||||
&paths,
|
||||
&reason,
|
||||
&authority,
|
||||
"factory-gap-ci",
|
||||
"external_project",
|
||||
"2026-10-13T09:41:23Z",
|
||||
);
|
||||
let cid = get(2).as_str().unwrap_or("").to_string();
|
||||
json!({"envelope": env, "contract_after": contract_of(&after, &cid).cloned().unwrap_or(Value::Null)})
|
||||
@@ -2504,7 +2645,11 @@ pub fn dispatch(case: &Value) -> Value {
|
||||
main_exit_code(&argv)
|
||||
}
|
||||
"ladder_walk" => {
|
||||
let obj = if args.len() > 1 { get(1).as_str().map(str::to_string) } else { None };
|
||||
let obj = if args.len() > 1 {
|
||||
get(1).as_str().map(str::to_string)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ladder_walk_replay(&get(0), obj.as_deref())
|
||||
}
|
||||
_ => json!({"error": format!("unknown frozen fn {}", name)}),
|
||||
@@ -2610,8 +2755,15 @@ fn ladder_walk_replay(case: &Value, objective: Option<&str>) -> Value {
|
||||
out.push(json!({"step": "verify", "envelope": verify(&doc, &cid, None, &json!([]), None, "factory-gap-ci")}));
|
||||
for ph in ["approved", "building", "verifying"] {
|
||||
let (env, after) = transition_doc(
|
||||
&doc, &cid, ph, "", None, &json!({}),
|
||||
"factory-gap-ci", "external_project", "2026-10-13T09:41:23Z",
|
||||
&doc,
|
||||
&cid,
|
||||
ph,
|
||||
"",
|
||||
None,
|
||||
&json!({}),
|
||||
"factory-gap-ci",
|
||||
"external_project",
|
||||
"2026-10-13T09:41:23Z",
|
||||
);
|
||||
out.push(json!({"step": format!("transition:{}", ph), "envelope": env.clone()}));
|
||||
if env.get("success").and_then(|s| s.as_bool()) != Some(true) {
|
||||
@@ -2620,18 +2772,27 @@ fn ladder_walk_replay(case: &Value, objective: Option<&str>) -> Value {
|
||||
doc = after;
|
||||
}
|
||||
let case_name = case.as_str().unwrap_or("walk");
|
||||
let mini_doc = format!("/tmp/ci_gap_freeze_home/cases/{}/docs/{}.md", case_name, cid);
|
||||
let mini_doc = format!(
|
||||
"/tmp/ci_gap_freeze_home/cases/{}/docs/{}.md",
|
||||
case_name, cid
|
||||
);
|
||||
let (env, _, _, _) = close(
|
||||
&doc, &cid,
|
||||
&doc,
|
||||
&cid,
|
||||
&[json!({"kind": "port_evidence", "detail": "gap closed with proof"})],
|
||||
None, &mini_doc, None, &[],
|
||||
"factory-gap-ci", "external_project", "gap-ci-proj", "2026-10-13T09:41:23Z",
|
||||
None,
|
||||
&mini_doc,
|
||||
None,
|
||||
&[],
|
||||
"factory-gap-ci",
|
||||
"external_project",
|
||||
"gap-ci-proj",
|
||||
"2026-10-13T09:41:23Z",
|
||||
);
|
||||
out.push(json!({"step": "close", "envelope": env}));
|
||||
Value::Array(out)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -2674,8 +2835,8 @@ mod tests {
|
||||
assert_eq!(e["errors"][0]["error"], json!("NEED_INCOMPLETE"));
|
||||
let (e, _) = report_observation(&doc, "C-1", &json!([{"detail": "x"}]), "b");
|
||||
assert_eq!(e["errors"][0]["error"], json!("NEED_INCOMPLETE"));
|
||||
let (e, items) = report_observation(
|
||||
&doc, "C-1", &json!([{"entity": "e", "detail": "d"}]), "b");
|
||||
let (e, items) =
|
||||
report_observation(&doc, "C-1", &json!([{"entity": "e", "detail": "d"}]), "b");
|
||||
assert_eq!(e["success"], json!(true));
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0]["source"], json!("builder:b"));
|
||||
@@ -2684,13 +2845,34 @@ mod tests {
|
||||
#[test]
|
||||
fn amend_requires_user() {
|
||||
let doc = contract_doc("C-1");
|
||||
let (e, _) = amend_scope(&doc, "C-1", &["x".to_string()], "r", "builder", "b", "m", "n");
|
||||
let (e, _) = amend_scope(
|
||||
&doc,
|
||||
"C-1",
|
||||
&["x".to_string()],
|
||||
"r",
|
||||
"builder",
|
||||
"b",
|
||||
"m",
|
||||
"n",
|
||||
);
|
||||
assert_eq!(e["success"], json!(false));
|
||||
assert_eq!(e["errors"][0]["error"], json!("APPROVAL_REQUIRED"));
|
||||
let (e, after) = amend_scope(&doc, "C-1", &["src/b.py".to_string()], "user asked", "user", "b", "m", "n");
|
||||
let (e, after) = amend_scope(
|
||||
&doc,
|
||||
"C-1",
|
||||
&["src/b.py".to_string()],
|
||||
"user asked",
|
||||
"user",
|
||||
"b",
|
||||
"m",
|
||||
"n",
|
||||
);
|
||||
assert_eq!(e["success"], json!(true));
|
||||
assert!(e["state"]["scope"].as_array().unwrap().len() == 2);
|
||||
assert_eq!(after["entities"][0]["ext"]["scope_amendments"][0]["authorized_by"], json!("user"));
|
||||
assert_eq!(
|
||||
after["entities"][0]["ext"]["scope_amendments"][0]["authorized_by"],
|
||||
json!("user")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -306,8 +306,7 @@ pub extern "C" fn ci_replay(arg: *const u8, arg_len: usize) -> *mut u8 {
|
||||
unsafe {
|
||||
let slice = std::slice::from_raw_parts(arg, arg_len);
|
||||
let text = std::str::from_utf8(slice).unwrap_or("{}");
|
||||
let case: serde_json::Value =
|
||||
serde_json::from_str(text).unwrap_or(serde_json::json!({}));
|
||||
let case: serde_json::Value = serde_json::from_str(text).unwrap_or(serde_json::json!({}));
|
||||
let result = kernel::dispatch(&case);
|
||||
to_cstring(result.to_string())
|
||||
}
|
||||
|
||||
@@ -219,15 +219,29 @@ pub fn envelope(
|
||||
env.insert("operation".into(), json!(operation));
|
||||
env.insert("success".into(), json!(success));
|
||||
env.insert("ids".into(), if ids.is_null() { json!({}) } else { ids });
|
||||
env.insert("state".into(), if state.is_null() { json!({}) } else { state });
|
||||
env.insert(
|
||||
"state".into(),
|
||||
if state.is_null() { json!({}) } else { state },
|
||||
);
|
||||
env.insert(
|
||||
"evidence".into(),
|
||||
if evidence.is_null() { json!([]) } else { evidence },
|
||||
if evidence.is_null() {
|
||||
json!([])
|
||||
} else {
|
||||
evidence
|
||||
},
|
||||
);
|
||||
env.insert(
|
||||
"errors".into(),
|
||||
if errors.is_null() { json!([]) } else { errors },
|
||||
);
|
||||
env.insert("errors".into(), if errors.is_null() { json!([]) } else { errors });
|
||||
env.insert(
|
||||
"warnings".into(),
|
||||
if warnings.is_null() { json!([]) } else { warnings },
|
||||
if warnings.is_null() {
|
||||
json!([])
|
||||
} else {
|
||||
warnings
|
||||
},
|
||||
);
|
||||
if let Value::Object(extra_map) = extra {
|
||||
for (k, v) in extra_map {
|
||||
|
||||
@@ -95,7 +95,8 @@ fn classify_battery() {
|
||||
}
|
||||
let got = run_classify(case);
|
||||
assert_eq!(
|
||||
got, case["expected"],
|
||||
got,
|
||||
case["expected"],
|
||||
"case {:?} (kwargs {}): classify diverged from frozen oracle truth",
|
||||
case["args"].get(0).and_then(|v| v.as_str()),
|
||||
case["kwargs"]
|
||||
@@ -131,7 +132,10 @@ fn to_dict_battery() {
|
||||
fn from_dict_battery() {
|
||||
// Auxiliary from_dict_cases column: known-key projection + defaults,
|
||||
// unknown keys dropped, explicit nulls honored verbatim.
|
||||
for entry in env_doc()["from_dict_cases"].as_array().expect("from_dict_cases") {
|
||||
for entry in env_doc()["from_dict_cases"]
|
||||
.as_array()
|
||||
.expect("from_dict_cases")
|
||||
{
|
||||
let got = ConstructionMode::from_dict(&entry["args"])
|
||||
.expect("from_dict must construct")
|
||||
.to_dict();
|
||||
@@ -148,25 +152,29 @@ fn error_shapes_battery() {
|
||||
// Crash parity: the oracle's TypeError/AttributeError messages are
|
||||
// the contract (str(e) strips the type name) — frozen in the envelope.
|
||||
let shapes = &env_doc()["error_shapes"];
|
||||
let err = ConstructionMode::from_dict(&json!({"evidence": []}))
|
||||
.expect_err("missing mode must error");
|
||||
let err =
|
||||
ConstructionMode::from_dict(&json!({"evidence": []})).expect_err("missing mode must error");
|
||||
assert_eq!(
|
||||
err,
|
||||
shapes["from_dict_missing_mode"].as_str().expect("frozen message"),
|
||||
shapes["from_dict_missing_mode"]
|
||||
.as_str()
|
||||
.expect("frozen message"),
|
||||
"from_dict missing-mode message"
|
||||
);
|
||||
let err = ConstructionMode::from_dict(&json!(123))
|
||||
.expect_err("non-dict must error");
|
||||
let err = ConstructionMode::from_dict(&json!(123)).expect_err("non-dict must error");
|
||||
assert_eq!(
|
||||
err,
|
||||
shapes["from_dict_non_dict"].as_str().expect("frozen message"),
|
||||
shapes["from_dict_non_dict"]
|
||||
.as_str()
|
||||
.expect("frozen message"),
|
||||
"from_dict non-dict message"
|
||||
);
|
||||
let err = to_dict_of(&json!({"mode": "X"}))
|
||||
.expect_err("missing attribute must error");
|
||||
let err = to_dict_of(&json!({"mode": "X"})).expect_err("missing attribute must error");
|
||||
assert_eq!(
|
||||
err,
|
||||
shapes["to_dict_missing_attribute"].as_str().expect("frozen message"),
|
||||
shapes["to_dict_missing_attribute"]
|
||||
.as_str()
|
||||
.expect("frozen message"),
|
||||
"to_dict missing-attribute message"
|
||||
);
|
||||
}
|
||||
@@ -44,40 +44,123 @@ impl Mode {
|
||||
// multi-hit evidence slices ([..:3]) and their formatted strings are
|
||||
// order-sensitive; the arrays must reproduce the oracle's order exactly.
|
||||
const STANDALONE_WORDS: &[&str] = &[
|
||||
"separate app", "own repo", "scaffold a new", "greenfield", "new tool",
|
||||
"new project", "standalone", "new service", "new web", "new application",
|
||||
"separate service", "new repository", "from scratch", "new app",
|
||||
"separate app",
|
||||
"own repo",
|
||||
"scaffold a new",
|
||||
"greenfield",
|
||||
"new tool",
|
||||
"new project",
|
||||
"standalone",
|
||||
"new service",
|
||||
"new web",
|
||||
"new application",
|
||||
"separate service",
|
||||
"new repository",
|
||||
"from scratch",
|
||||
"new app",
|
||||
];
|
||||
const KNOWN_FILES: &[&str] = &[
|
||||
"readme", "makefile", "dockerfile", "changelog", "license", "gemfile",
|
||||
"procfile", "jenkinsfile", "justfile",
|
||||
"readme",
|
||||
"makefile",
|
||||
"dockerfile",
|
||||
"changelog",
|
||||
"license",
|
||||
"gemfile",
|
||||
"procfile",
|
||||
"jenkinsfile",
|
||||
"justfile",
|
||||
];
|
||||
const LOCAL_WORDS: &[&str] = &[
|
||||
"one-liner", "tiny fix", "typo", "small fix", "comment fix",
|
||||
"quick fix", "patch", "small correction", "cosmetic", "minor",
|
||||
"one-liner",
|
||||
"tiny fix",
|
||||
"typo",
|
||||
"small fix",
|
||||
"comment fix",
|
||||
"quick fix",
|
||||
"patch",
|
||||
"small correction",
|
||||
"cosmetic",
|
||||
"minor",
|
||||
];
|
||||
const SCOPING_WORDS: &[&str] = &[
|
||||
"the project", "this app", "current", "the module", "the service",
|
||||
"this module", "that module", "this project", "the app", "existing",
|
||||
"the project",
|
||||
"this app",
|
||||
"current",
|
||||
"the module",
|
||||
"the service",
|
||||
"this module",
|
||||
"that module",
|
||||
"this project",
|
||||
"the app",
|
||||
"existing",
|
||||
"the codebase",
|
||||
];
|
||||
const CONSTRUCTION_WORDS: &[&str] = &[
|
||||
"build", "create", "make", "implement", "add", "write", "develop",
|
||||
"construct", "fix", "repair", "refactor", "migrate", "change", "extend",
|
||||
"replace", "feature", "export", "module", "service", "app", "tool",
|
||||
"component", "schema", "api",
|
||||
"build",
|
||||
"create",
|
||||
"make",
|
||||
"implement",
|
||||
"add",
|
||||
"write",
|
||||
"develop",
|
||||
"construct",
|
||||
"fix",
|
||||
"repair",
|
||||
"refactor",
|
||||
"migrate",
|
||||
"change",
|
||||
"extend",
|
||||
"replace",
|
||||
"feature",
|
||||
"export",
|
||||
"module",
|
||||
"service",
|
||||
"app",
|
||||
"tool",
|
||||
"component",
|
||||
"schema",
|
||||
"api",
|
||||
];
|
||||
const NOT_CONSTRUCTION_HINTS: &[&str] = &[
|
||||
"explain", "teach me", "confidence", "describe", "list", "why does",
|
||||
"what is", "how does", "improvisation", "summarize",
|
||||
"explain",
|
||||
"teach me",
|
||||
"confidence",
|
||||
"describe",
|
||||
"list",
|
||||
"why does",
|
||||
"what is",
|
||||
"how does",
|
||||
"improvisation",
|
||||
"summarize",
|
||||
];
|
||||
const CREATION_VERBS: &[&str] = &["build", "create", "make", "write", "develop"];
|
||||
const ARTIFACT_NOUNS: &[&str] = &[
|
||||
"program", "cli", "tool", "app", "script", "service", "library",
|
||||
"package", "module", "dashboard", "website", "bot", "generator",
|
||||
"tracker", "game", "application", "command",
|
||||
"program",
|
||||
"cli",
|
||||
"tool",
|
||||
"app",
|
||||
"script",
|
||||
"service",
|
||||
"library",
|
||||
"package",
|
||||
"module",
|
||||
"dashboard",
|
||||
"website",
|
||||
"bot",
|
||||
"generator",
|
||||
"tracker",
|
||||
"game",
|
||||
"application",
|
||||
"command",
|
||||
];
|
||||
const NON_CONSTRUCTIBLE: &[&str] = &[
|
||||
"list",
|
||||
"confidence",
|
||||
"explanation",
|
||||
"summary",
|
||||
"chord",
|
||||
"note",
|
||||
];
|
||||
const NON_CONSTRUCTIBLE: &[&str] = &["list", "confidence", "explanation", "summary", "chord", "note"];
|
||||
|
||||
/// `_tokens` (construction_mode.py:190-191) — codepoint set split.
|
||||
fn tokens(text: &str) -> Vec<String> {
|
||||
@@ -180,15 +263,14 @@ impl ConstructionMode {
|
||||
))
|
||||
}
|
||||
};
|
||||
let mode = match obj.get("mode") {
|
||||
Some(v) => v.clone(),
|
||||
None => {
|
||||
return Err(
|
||||
let mode =
|
||||
match obj.get("mode") {
|
||||
Some(v) => v.clone(),
|
||||
None => return Err(
|
||||
"ConstructionMode.__init__() missing 1 required positional argument: 'mode'"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
),
|
||||
};
|
||||
Ok(ConstructionMode {
|
||||
mode,
|
||||
evidence: obj.get("evidence").cloned().unwrap_or(json!([])),
|
||||
@@ -200,7 +282,10 @@ impl ConstructionMode {
|
||||
.cloned()
|
||||
.unwrap_or(json!(false)),
|
||||
probe_questions: obj.get("probe_questions").cloned().unwrap_or(json!([])),
|
||||
bootstrap_required: obj.get("bootstrap_required").cloned().unwrap_or(json!(false)),
|
||||
bootstrap_required: obj
|
||||
.get("bootstrap_required")
|
||||
.cloned()
|
||||
.unwrap_or(json!(false)),
|
||||
r#override: obj.get("override").cloned().unwrap_or(Value::Null),
|
||||
proposed_by_model: obj.get("proposed_by_model").cloned().unwrap_or(Value::Null),
|
||||
})
|
||||
@@ -250,7 +335,9 @@ pub fn to_dict_of(d: &Value) -> Result<Value, String> {
|
||||
|
||||
fn has_verb(obj: &str) -> bool {
|
||||
let t = tokens(obj);
|
||||
CONSTRUCTION_WORDS.iter().any(|w| t.iter().any(|tok| tok == *w))
|
||||
CONSTRUCTION_WORDS
|
||||
.iter()
|
||||
.any(|w| t.iter().any(|tok| tok == *w))
|
||||
}
|
||||
|
||||
/// classify_surface lexical fallback (construction_decision.classify_surface):
|
||||
@@ -262,7 +349,10 @@ fn classify_surface_lexical(w: &str) -> Option<&'static str> {
|
||||
}
|
||||
if w.contains('.') || w.contains('/') {
|
||||
// dotted/path: source-ish extensions and known config names
|
||||
let src_exts = ["py", "rs", "go", "js", "ts", "c", "cpp", "h", "md", "yml", "yaml", "json", "toml", "sh"];
|
||||
let src_exts = [
|
||||
"py", "rs", "go", "js", "ts", "c", "cpp", "h", "md", "yml", "yaml", "json", "toml",
|
||||
"sh",
|
||||
];
|
||||
if let Some(ext) = w.rsplit('.').next() {
|
||||
if src_exts.contains(&ext) {
|
||||
return Some("source_path");
|
||||
@@ -302,7 +392,10 @@ pub fn classify_mode(
|
||||
"override": null, "proposed_by_model": model_proposal,
|
||||
});
|
||||
}
|
||||
ev.push(format!("explicit override by {}", override_authority.unwrap_or("operator")));
|
||||
ev.push(format!(
|
||||
"explicit override by {}",
|
||||
override_authority.unwrap_or("operator")
|
||||
));
|
||||
// construction_mode.py:249-259: the override branch passes ONLY
|
||||
// mode/evidence/target_project/confidence/bootstrap_required/override —
|
||||
// project_authority and proposed_by_model keep their dataclass defaults.
|
||||
@@ -330,13 +423,19 @@ pub fn classify_mode(
|
||||
if !notc.is_empty() {
|
||||
let obj_tokens = tokens(obj);
|
||||
let standalone_evidence = !_contains_any(obj, STANDALONE_WORDS).is_empty();
|
||||
let non_constructible_hit = obj_tokens.iter().any(|t| NON_CONSTRUCTIBLE.contains(&t.as_str()));
|
||||
let non_constructible_hit = obj_tokens
|
||||
.iter()
|
||||
.any(|t| NON_CONSTRUCTIBLE.contains(&t.as_str()));
|
||||
if !has_v || (non_constructible_hit && !standalone_evidence) {
|
||||
ev.push(format!("non-construction intent markers: {}", py_repr_list(¬c[..notc.len().min(3)])));
|
||||
ev.push(
|
||||
if has_v { "construction-verb object is not a buildable artifact".to_string() }
|
||||
else { "no construction verb present".to_string() }
|
||||
);
|
||||
ev.push(format!(
|
||||
"non-construction intent markers: {}",
|
||||
py_repr_list(¬c[..notc.len().min(3)])
|
||||
));
|
||||
ev.push(if has_v {
|
||||
"construction-verb object is not a buildable artifact".to_string()
|
||||
} else {
|
||||
"no construction verb present".to_string()
|
||||
});
|
||||
// construction_mode.py:276: only mode/evidence/confidence are
|
||||
// passed — target_project/project_authority/proposed_by_model
|
||||
// stay at their dataclass defaults.
|
||||
@@ -368,14 +467,27 @@ pub fn classify_mode(
|
||||
let scoped = _contains_any(obj, SCOPING_WORDS);
|
||||
let mut file_scoped = false;
|
||||
for w in obj_lower.split(|c: char| {
|
||||
!(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '.' || c == '/' || c == '-')
|
||||
!(c.is_ascii_lowercase()
|
||||
|| c.is_ascii_digit()
|
||||
|| c == '_'
|
||||
|| c == '.'
|
||||
|| c == '/'
|
||||
|| c == '-')
|
||||
}) {
|
||||
if w.len() < 3 { continue; }
|
||||
if w.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
if KNOWN_FILES.contains(&w) || w.contains('.') || w.contains('/') {
|
||||
match classify_surface_lexical(w) {
|
||||
Some("source_path") | Some("workflow_config") => { file_scoped = true; break; }
|
||||
Some("source_path") | Some("workflow_config") => {
|
||||
file_scoped = true;
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
if KNOWN_FILES.contains(&w) { file_scoped = true; break; }
|
||||
if KNOWN_FILES.contains(&w) {
|
||||
file_scoped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,8 +497,12 @@ pub fn classify_mode(
|
||||
let article_re = regex_lite_article(obj_lower.as_str());
|
||||
let mut article_standalone = false;
|
||||
if article_re {
|
||||
let creation_verb = tokens(obj).iter().any(|t| CREATION_VERBS.contains(&t.as_str()));
|
||||
let artifact_noun = tokens(obj).iter().any(|t| ARTIFACT_NOUNS.contains(&t.as_str()));
|
||||
let creation_verb = tokens(obj)
|
||||
.iter()
|
||||
.any(|t| CREATION_VERBS.contains(&t.as_str()));
|
||||
let artifact_noun = tokens(obj)
|
||||
.iter()
|
||||
.any(|t| ARTIFACT_NOUNS.contains(&t.as_str()));
|
||||
if (creation_verb || artifact_noun)
|
||||
&& local.is_empty()
|
||||
&& scoped.is_empty()
|
||||
@@ -401,52 +517,82 @@ pub fn classify_mode(
|
||||
|
||||
let (mut mode, mut conf): (Mode, f64);
|
||||
if !standalone.is_empty() || article_standalone {
|
||||
ev.push(format!("standalone/new-lifecycle language: {}", py_repr_list(&standalone[..standalone.len().min(3)])));
|
||||
ev.push(format!(
|
||||
"standalone/new-lifecycle language: {}",
|
||||
py_repr_list(&standalone[..standalone.len().min(3)])
|
||||
));
|
||||
if project_exists == Some(true) {
|
||||
ev.push("BUT target project already exists (structural evidence contradicts standalone language)".to_string());
|
||||
mode = Mode::Unknown; conf = 0.3; clarification = true;
|
||||
probes.push("should this extend the existing project or start a genuinely separate one?".to_string());
|
||||
mode = Mode::Unknown;
|
||||
conf = 0.3;
|
||||
clarification = true;
|
||||
probes.push(
|
||||
"should this extend the existing project or start a genuinely separate one?"
|
||||
.to_string(),
|
||||
);
|
||||
} else if project_exists.is_none() {
|
||||
ev.push("no project-existence evidence for the target".to_string());
|
||||
mode = Mode::Unknown; conf = 0.4; clarification = true;
|
||||
mode = Mode::Unknown;
|
||||
conf = 0.4;
|
||||
clarification = true;
|
||||
probes.push("does a project for this already exist?".to_string());
|
||||
} else {
|
||||
mode = Mode::ProjectBuild; conf = 0.9;
|
||||
mode = Mode::ProjectBuild;
|
||||
conf = 0.9;
|
||||
}
|
||||
} else if !local.is_empty()
|
||||
&& (!scoped.is_empty() || file_scoped || target_project.is_some() || project_exists.is_some())
|
||||
&& (!scoped.is_empty()
|
||||
|| file_scoped
|
||||
|| target_project.is_some()
|
||||
|| project_exists.is_some())
|
||||
{
|
||||
ev.push(format!("smallness markers: {}", py_repr_list(&local[..local.len().min(3)])));
|
||||
ev.push(format!(
|
||||
"smallness markers: {}",
|
||||
py_repr_list(&local[..local.len().min(3)])
|
||||
));
|
||||
if !scoped.is_empty() {
|
||||
ev.push(format!("scoping words bind to known target: {}", py_repr_list(&scoped[..scoped.len().min(3)])));
|
||||
ev.push(format!(
|
||||
"scoping words bind to known target: {}",
|
||||
py_repr_list(&scoped[..scoped.len().min(3)])
|
||||
));
|
||||
}
|
||||
if file_scoped {
|
||||
ev.push("file mention binds scoped target".to_string());
|
||||
}
|
||||
mode = Mode::LocalChange; conf = 0.8;
|
||||
} else if (!scoped.is_empty() || target_project.is_some() || project_exists == Some(true)) && has_v {
|
||||
mode = Mode::LocalChange;
|
||||
conf = 0.8;
|
||||
} else if (!scoped.is_empty() || target_project.is_some() || project_exists == Some(true))
|
||||
&& has_v
|
||||
{
|
||||
ev.push("change scoped to an existing project/target".to_string());
|
||||
if let Some(tp) = target_project {
|
||||
ev.push(format!("target project: {}", tp));
|
||||
}
|
||||
if project_exists == Some(false) {
|
||||
ev.push("BUT structural evidence proves the target project does not exist — refusing to manufacture existing-project semantics".to_string());
|
||||
mode = Mode::Unknown; conf = 0.3; clarification = true;
|
||||
mode = Mode::Unknown;
|
||||
conf = 0.3;
|
||||
clarification = true;
|
||||
probes.push(format!(
|
||||
"project '{}' has no structural record — is it new (project build) or does it live somewhere not bound?",
|
||||
target_project.unwrap_or("the target")
|
||||
));
|
||||
} else {
|
||||
mode = Mode::ExistingProjectChange; conf = 0.8;
|
||||
mode = Mode::ExistingProjectChange;
|
||||
conf = 0.8;
|
||||
}
|
||||
} else if has_v {
|
||||
ev.push("construction verb present but no target-scope evidence and no standalone/new-lifecycle language".to_string());
|
||||
mode = Mode::Unknown; conf = 0.35; clarification = true;
|
||||
mode = Mode::Unknown;
|
||||
conf = 0.35;
|
||||
clarification = true;
|
||||
probes.push("is this a change to an existing project or a new standalone one?".to_string());
|
||||
probes.push("which project/module is the target?".to_string());
|
||||
} else {
|
||||
ev.push("insufficient lexical evidence either way".to_string());
|
||||
mode = Mode::Unknown; conf = 0.2; clarification = true;
|
||||
mode = Mode::Unknown;
|
||||
conf = 0.2;
|
||||
clarification = true;
|
||||
probes.push("is this a construction request?".to_string());
|
||||
}
|
||||
|
||||
@@ -456,9 +602,14 @@ pub fn classify_mode(
|
||||
Mode::LocalChange | Mode::ExistingProjectChange | Mode::Unknown
|
||||
) {
|
||||
let surface_refs: Vec<&str> = surface.iter().map(|s| s.as_str()).collect();
|
||||
ev.push(format!("affected surface known: {}", py_repr_list(&surface_refs[..surface_refs.len().min(2)])));
|
||||
ev.push(format!(
|
||||
"affected surface known: {}",
|
||||
py_repr_list(&surface_refs[..surface_refs.len().min(2)])
|
||||
));
|
||||
if mode == Mode::Unknown {
|
||||
mode = Mode::ExistingProjectChange; conf = 0.6; clarification = false;
|
||||
mode = Mode::ExistingProjectChange;
|
||||
conf = 0.6;
|
||||
clarification = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,13 +686,25 @@ fn regex_lite_article(text: &str) -> bool {
|
||||
while let Some(pos) = text[search_from..].find(article) {
|
||||
let abs = search_from + pos;
|
||||
let before_ok = abs == 0
|
||||
|| !text[..abs].chars().last().map(|c| c.is_ascii_alphanumeric()).unwrap_or(false);
|
||||
|| !text[..abs]
|
||||
.chars()
|
||||
.last()
|
||||
.map(|c| c.is_ascii_alphanumeric())
|
||||
.unwrap_or(false);
|
||||
let after_idx = abs + article.len();
|
||||
if before_ok && text[after_idx..].chars().next().map(|c| c.is_ascii_lowercase()).unwrap_or(false) {
|
||||
if before_ok
|
||||
&& text[after_idx..]
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_lowercase())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
search_from = abs + 1;
|
||||
if search_from >= text.len() { break; }
|
||||
if search_from >= text.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -558,7 +721,9 @@ fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -584,10 +749,14 @@ pub extern "C" fn cm_classify(arg: *const c_char) -> *mut c_char {
|
||||
Some(Value::Bool(b)) => Some(*b),
|
||||
_ => None,
|
||||
};
|
||||
let affected_surface = a
|
||||
.get("affected_surface")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr: &Vec<Value>| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect::<Vec<String>>());
|
||||
let affected_surface =
|
||||
a.get("affected_surface")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr: &Vec<Value>| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
let explicit_override = a.get("explicit_override").and_then(|v| v.as_str());
|
||||
let override_authority = a.get("override_authority").and_then(|v| v.as_str());
|
||||
let model_proposal = a.get("model_proposal");
|
||||
|
||||
@@ -14,7 +14,7 @@ mod battery {
|
||||
combine_pattern_obligations, construct_pattern, derive_pattern_obligations,
|
||||
evaluate_applicability, record_pattern_proposal, revise_pattern,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn truth() -> Value {
|
||||
serde_json::from_str(include_str!("frozen_cp_truth.json")).expect("frozen truth parses")
|
||||
@@ -41,9 +41,18 @@ mod battery {
|
||||
fn run_revise(c: &Value) -> Result<Value, String> {
|
||||
let i = &c["input"];
|
||||
let evidence: Vec<Value> = i["evidence"].as_array().cloned().unwrap_or_default();
|
||||
let changes = if i["changes"].is_null() { None } else { Some(obj(&i["changes"])) };
|
||||
revise_pattern(&obj(&i["pattern"]), i["reason"].as_str().unwrap_or(""), &evidence, changes.as_ref())
|
||||
.map(Value::Object)
|
||||
let changes = if i["changes"].is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(obj(&i["changes"]))
|
||||
};
|
||||
revise_pattern(
|
||||
&obj(&i["pattern"]),
|
||||
i["reason"].as_str().unwrap_or(""),
|
||||
&evidence,
|
||||
changes.as_ref(),
|
||||
)
|
||||
.map(Value::Object)
|
||||
}
|
||||
|
||||
fn run_propose(c: &Value) -> Result<Value, String> {
|
||||
@@ -61,8 +70,10 @@ mod battery {
|
||||
}
|
||||
return Ok(Value::Array(out));
|
||||
}
|
||||
let evidence: Vec<&str> =
|
||||
i["evidence"].as_array().map(|a| a.iter().filter_map(|e| e.as_str()).collect()).unwrap_or_default();
|
||||
let evidence: Vec<&str> = i["evidence"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().filter_map(|e| e.as_str()).collect())
|
||||
.unwrap_or_default();
|
||||
record_pattern_proposal(
|
||||
i["pattern_id"].as_str().unwrap_or(""),
|
||||
i["provider"].as_str().unwrap_or(""),
|
||||
@@ -74,18 +85,38 @@ mod battery {
|
||||
|
||||
fn run_evaluate(c: &Value) -> Value {
|
||||
let i = &c["input"];
|
||||
evaluate_applicability(&obj(&i["pattern"]), &obj(&i["need"]), &obj(&i["surface_result"]), &obj(&i["authorities"]))
|
||||
evaluate_applicability(
|
||||
&obj(&i["pattern"]),
|
||||
&obj(&i["need"]),
|
||||
&obj(&i["surface_result"]),
|
||||
&obj(&i["authorities"]),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_derive(c: &Value) -> Value {
|
||||
let i = &c["input"];
|
||||
let exc: Vec<Value> =
|
||||
if i["project_exceptions"].is_null() { vec![] } else { i["project_exceptions"].as_array().cloned().unwrap_or_default() };
|
||||
derive_pattern_obligations(&obj(&i["pattern"]), &obj(&i["need"]), &obj(&i["surface_result"]), &obj(&i["authorities"]), &exc)
|
||||
let exc: Vec<Value> = if i["project_exceptions"].is_null() {
|
||||
vec![]
|
||||
} else {
|
||||
i["project_exceptions"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
derive_pattern_obligations(
|
||||
&obj(&i["pattern"]),
|
||||
&obj(&i["need"]),
|
||||
&obj(&i["surface_result"]),
|
||||
&obj(&i["authorities"]),
|
||||
&exc,
|
||||
)
|
||||
}
|
||||
|
||||
fn run_combine(c: &Value) -> Value {
|
||||
let derivs: Vec<Value> = c["input"]["derivations"].as_array().cloned().unwrap_or_default();
|
||||
let derivs: Vec<Value> = c["input"]["derivations"]
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
combine_pattern_obligations(&derivs)
|
||||
}
|
||||
|
||||
@@ -113,16 +144,28 @@ mod battery {
|
||||
fn run_surface_text(c: &Value) -> Result<Value, String> {
|
||||
let i = &c["input"];
|
||||
let need = i["need"].as_object().expect("need object").clone();
|
||||
let surface = i["surface_result"].as_object().expect("surface object").clone();
|
||||
let surface = i["surface_result"]
|
||||
.as_object()
|
||||
.expect("surface object")
|
||||
.clone();
|
||||
Ok(_surface_text(&need, &surface))
|
||||
}
|
||||
|
||||
fn run_when_hit(c: &Value) -> Result<Value, String> {
|
||||
let i = &c["input"];
|
||||
let obligation = i["obligation"].as_object().expect("obligation object").clone();
|
||||
let obligation = i["obligation"]
|
||||
.as_object()
|
||||
.expect("obligation object")
|
||||
.clone();
|
||||
let need = i["need"].as_object().expect("need object").clone();
|
||||
let surface = i["surface_result"].as_object().expect("surface object").clone();
|
||||
let authorities = i["authorities"].as_object().expect("authorities object").clone();
|
||||
let surface = i["surface_result"]
|
||||
.as_object()
|
||||
.expect("surface object")
|
||||
.clone();
|
||||
let authorities = i["authorities"]
|
||||
.as_object()
|
||||
.expect("authorities object")
|
||||
.clone();
|
||||
let (verdict, reason) = _obligation_when_hit(&obligation, &need, &surface, &authorities);
|
||||
Ok(json!([verdict.as_str(), reason]))
|
||||
}
|
||||
@@ -145,7 +188,11 @@ mod battery {
|
||||
let cs = cases("construct");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_construct(c), &c["expected"]);
|
||||
check(
|
||||
c["name"].as_str().unwrap(),
|
||||
run_construct(c),
|
||||
&c["expected"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +249,11 @@ mod battery {
|
||||
let cs = cases("path_match");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_path_match(c), &c["expected"]);
|
||||
check(
|
||||
c["name"].as_str().unwrap(),
|
||||
run_path_match(c),
|
||||
&c["expected"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +262,11 @@ mod battery {
|
||||
let cs = cases("entity_match");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_entity_match(c), &c["expected"]);
|
||||
check(
|
||||
c["name"].as_str().unwrap(),
|
||||
run_entity_match(c),
|
||||
&c["expected"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +275,11 @@ mod battery {
|
||||
let cs = cases("need_dict");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_need_dict(c), &c["expected"]);
|
||||
check(
|
||||
c["name"].as_str().unwrap(),
|
||||
run_need_dict(c),
|
||||
&c["expected"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +288,11 @@ mod battery {
|
||||
let cs = cases("surface_text");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_surface_text(c), &c["expected"]);
|
||||
check(
|
||||
c["name"].as_str().unwrap(),
|
||||
run_surface_text(c),
|
||||
&c["expected"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,11 +309,26 @@ mod battery {
|
||||
|
||||
#[test]
|
||||
fn path_token_discipline() {
|
||||
assert!(!_path_matches("ui", "src/rune/rune_tui.py"), "'ui' is not a token in rune_tui.py");
|
||||
assert!(!_path_matches("ui", "src/rune/builtin.py"), "the 'ui'-in-'builtin' FP class");
|
||||
assert!(_path_matches("spine", "src/rune/spine.py"), "long signal: containment");
|
||||
assert!(_path_matches("tui", "src/rune/rune_tui.py"), "'tui' IS a token");
|
||||
assert!(!_path_matches("cfg", "config/settings.py"), "'cfg' not a token of config");
|
||||
assert!(
|
||||
!_path_matches("ui", "src/rune/rune_tui.py"),
|
||||
"'ui' is not a token in rune_tui.py"
|
||||
);
|
||||
assert!(
|
||||
!_path_matches("ui", "src/rune/builtin.py"),
|
||||
"the 'ui'-in-'builtin' FP class"
|
||||
);
|
||||
assert!(
|
||||
_path_matches("spine", "src/rune/spine.py"),
|
||||
"long signal: containment"
|
||||
);
|
||||
assert!(
|
||||
_path_matches("tui", "src/rune/rune_tui.py"),
|
||||
"'tui' IS a token"
|
||||
);
|
||||
assert!(
|
||||
!_path_matches("cfg", "config/settings.py"),
|
||||
"'cfg' not a token of config"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -259,13 +337,19 @@ mod battery {
|
||||
assert!(_entity_matches("module:", "module:spine"), "prefix match");
|
||||
assert!(!_entity_matches("ui:", "region:left"), "prefix mismatch");
|
||||
assert!(_entity_matches("spine", "module:spine"), "long containment");
|
||||
assert!(!_entity_matches("ui", "ui:chat"), "oracle truth: token discipline");
|
||||
assert!(
|
||||
!_entity_matches("ui", "ui:chat"),
|
||||
"oracle truth: token discipline"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_signal_containment() {
|
||||
assert!(_path_matches("structural", "src/rune/structural_store.py"));
|
||||
assert!(_entity_matches("construction", "module:construction_pattern"));
|
||||
assert!(_entity_matches(
|
||||
"construction",
|
||||
"module:construction_pattern"
|
||||
));
|
||||
assert!(!_path_matches("structural", "src/rune/other.py"));
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,10 @@ fn py_repr(v: &Value) -> String {
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let inner: Vec<String> = map.iter().map(|(k, v)| format!("{}: {}", py_quote(k), py_repr(v))).collect();
|
||||
let inner: Vec<String> = map
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}: {}", py_quote(k), py_repr(v)))
|
||||
.collect();
|
||||
format!("{{{}}}", inner.join(", "))
|
||||
}
|
||||
}
|
||||
@@ -224,7 +227,9 @@ pub fn _path_matches(signal: &str, path: &str) -> bool {
|
||||
if s.chars().count() >= 4 {
|
||||
return path.to_lowercase().contains(&s);
|
||||
}
|
||||
split_tokens(&path.to_lowercase(), false).iter().any(|t| *t == s)
|
||||
split_tokens(&path.to_lowercase(), false)
|
||||
.iter()
|
||||
.any(|t| *t == s)
|
||||
}
|
||||
|
||||
/// Same discipline for entity ids. Prefix signals ending in ':' (e.g.
|
||||
@@ -239,7 +244,9 @@ pub fn _entity_matches(signal: &str, entity_id: &str) -> bool {
|
||||
if s.chars().count() >= 4 {
|
||||
return entity_id.to_lowercase().contains(&s);
|
||||
}
|
||||
split_tokens(&entity_id.to_lowercase(), true).iter().any(|t| *t == s)
|
||||
split_tokens(&entity_id.to_lowercase(), true)
|
||||
.iter()
|
||||
.any(|t| *t == s)
|
||||
}
|
||||
|
||||
/// The FFI need envelope (Python `_need_dict`): a need is exactly its three
|
||||
@@ -247,14 +254,25 @@ pub fn _entity_matches(signal: &str, entity_id: &str) -> bool {
|
||||
/// else a caller attached is dropped at the kernel boundary.
|
||||
pub fn _need_dict(need: &Map<String, Value>) -> Map<String, Value> {
|
||||
let mut out = Map::with_capacity(3);
|
||||
out.insert("objective".into(), need.get("objective").cloned().unwrap_or(Value::Null));
|
||||
out.insert(
|
||||
"objective".into(),
|
||||
need.get("objective").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
out.insert(
|
||||
"affected_surface".into(),
|
||||
need.get("affected_surface").and_then(|v| v.as_array()).cloned().map(Value::Array).unwrap_or(Value::Null),
|
||||
need.get("affected_surface")
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned()
|
||||
.map(Value::Array)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
out.insert(
|
||||
"constraints".into(),
|
||||
need.get("constraints").and_then(|v| v.as_object()).cloned().map(Value::Object).unwrap_or(Value::Null),
|
||||
need.get("constraints")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
out
|
||||
}
|
||||
@@ -276,7 +294,11 @@ pub fn _surface_text(need: &Map<String, Value>, surface: &Map<String, Value>) ->
|
||||
] {
|
||||
tokens.extend(surface.get(k).map(str_list).unwrap_or_default());
|
||||
}
|
||||
tokens.extend(need.get("affected_surface").map(str_list).unwrap_or_default());
|
||||
tokens.extend(
|
||||
need.get("affected_surface")
|
||||
.map(str_list)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let lowered = need
|
||||
.get("objective")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -284,7 +306,12 @@ pub fn _surface_text(need: &Map<String, Value>, surface: &Map<String, Value>) ->
|
||||
.replace(['/', '-'], " ")
|
||||
.to_lowercase();
|
||||
tokens.extend(lowered.split_whitespace());
|
||||
Value::Array(tokens.into_iter().map(|t| Value::String(t.to_string())).collect())
|
||||
Value::Array(
|
||||
tokens
|
||||
.into_iter()
|
||||
.map(|t| Value::String(t.to_string()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── small JSON accessors (borrowed; owned only where the output needs it) ──
|
||||
@@ -307,7 +334,10 @@ fn relationships<'a>(doc: &'a Value, from_id: &str) -> Vec<&'a Map<String, Value
|
||||
.map(|rels| {
|
||||
rels.iter()
|
||||
.filter_map(|r| r.as_object())
|
||||
.filter(|r| from_id.is_empty() || r.get("from").and_then(|f| f.as_str()).unwrap_or("") == from_id)
|
||||
.filter(|r| {
|
||||
from_id.is_empty()
|
||||
|| r.get("from").and_then(|f| f.as_str()).unwrap_or("") == from_id
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
@@ -320,7 +350,10 @@ fn relationships<'a>(doc: &'a Value, from_id: &str) -> Vec<&'a Map<String, Value
|
||||
/// passthrough). The input and output share one shape — `to_dict` output
|
||||
/// round-trips through here unchanged.
|
||||
pub fn construct_pattern(input: &Map<String, Value>) -> Result<Map<String, Value>, String> {
|
||||
let pattern_id = input.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let pattern_id = input
|
||||
.get("pattern_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if pattern_id.is_empty() {
|
||||
return Err("pattern_id required".to_string());
|
||||
}
|
||||
@@ -332,7 +365,11 @@ pub fn construct_pattern(input: &Map<String, Value>) -> Result<Map<String, Value
|
||||
for o in obs {
|
||||
for key in ["id", "obligation", "when"] {
|
||||
if o.get(key).is_none() {
|
||||
return Err(format!("obligation missing {}: {}", py_quote(key), py_repr(o)));
|
||||
return Err(format!(
|
||||
"obligation missing {}: {}",
|
||||
py_quote(key),
|
||||
py_repr(o)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,13 +387,24 @@ pub fn construct_pattern(input: &Map<String, Value>) -> Result<Map<String, Value
|
||||
);
|
||||
out.insert(
|
||||
"signals".into(),
|
||||
input.get("signals").cloned().unwrap_or_else(|| Value::Object(Map::new())),
|
||||
input
|
||||
.get("signals")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Object(Map::new())),
|
||||
);
|
||||
out.insert(
|
||||
"obligations".into(),
|
||||
input.get("obligations").cloned().unwrap_or_else(|| Value::Array(vec![])),
|
||||
input
|
||||
.get("obligations")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Array(vec![])),
|
||||
);
|
||||
for key in ["provenance", "exclusions", "toolchain_notes", "runtime_effects"] {
|
||||
for key in [
|
||||
"provenance",
|
||||
"exclusions",
|
||||
"toolchain_notes",
|
||||
"runtime_effects",
|
||||
] {
|
||||
let mut vals = input.get(key).map(str_list_owned).unwrap_or_default();
|
||||
vals.sort_unstable();
|
||||
out.insert(key.into(), json!(vals));
|
||||
@@ -377,8 +425,15 @@ pub fn revise_pattern(
|
||||
evidence: &[Value],
|
||||
changes: Option<&Map<String, Value>>,
|
||||
) -> Result<Map<String, Value>, String> {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let revision = pattern.get("revision").and_then(|v| v.as_i64()).unwrap_or(1);
|
||||
let pattern_id = pattern
|
||||
.get("pattern_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let revision = pattern
|
||||
.get("revision")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(1);
|
||||
let mut merged = pattern.clone();
|
||||
merged.remove("pattern_id");
|
||||
if let Some(ch) = changes {
|
||||
@@ -387,9 +442,14 @@ pub fn revise_pattern(
|
||||
}
|
||||
}
|
||||
merged.insert("revision".into(), json!(revision + 1));
|
||||
let mut ev: Vec<String> = evidence.iter().filter_map(|e| e.as_str().map(String::from)).collect();
|
||||
let mut ev: Vec<String> = evidence
|
||||
.iter()
|
||||
.filter_map(|e| e.as_str().map(String::from))
|
||||
.collect();
|
||||
ev.sort_unstable();
|
||||
let mut changed: Vec<String> = changes.map(|ch| ch.keys().cloned().collect()).unwrap_or_default();
|
||||
let mut changed: Vec<String> = changes
|
||||
.map(|ch| ch.keys().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
changed.sort_unstable();
|
||||
let entry = json!({
|
||||
"prior_revision": revision,
|
||||
@@ -446,13 +506,21 @@ fn surface_kind_present(kind: &str, surface: &Map<String, Value>) -> bool {
|
||||
"layout_region" => "affected_regions",
|
||||
_ => "",
|
||||
};
|
||||
if surface.get(mapped).and_then(|v| v.as_array()).map(|a| !a.is_empty()).unwrap_or(false) {
|
||||
if surface
|
||||
.get(mapped)
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| !a.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
surface
|
||||
.get("resolutions")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|rs| rs.iter().any(|r| r.get("kind").and_then(|k| k.as_str()).unwrap_or("") == kind))
|
||||
.map(|rs| {
|
||||
rs.iter()
|
||||
.any(|r| r.get("kind").and_then(|k| k.as_str()).unwrap_or("") == kind)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
@@ -497,7 +565,10 @@ pub fn evaluate_applicability(
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
) -> Value {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let pattern_id = pattern
|
||||
.get("pattern_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let objective = need.get("objective").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let exclusions: Vec<&str> = pattern.get("exclusions").map(str_list).unwrap_or_default();
|
||||
let need_exclusions: Vec<&str> = need
|
||||
@@ -508,7 +579,11 @@ pub fn evaluate_applicability(
|
||||
|
||||
// §3/§18: PROJECT evidence overrides the pattern.
|
||||
let objective_low = objective.to_lowercase();
|
||||
for ex in exclusions.iter().copied().chain(need_exclusions.iter().copied()) {
|
||||
for ex in exclusions
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(need_exclusions.iter().copied())
|
||||
{
|
||||
if objective_low.contains(&ex.to_lowercase()) || need_exclusions.contains(&ex) {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
@@ -540,24 +615,61 @@ pub fn evaluate_applicability(
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("workflow_config_surfaces").map(str_list).unwrap_or_default())
|
||||
.chain(
|
||||
surface
|
||||
.get("workflow_config_surfaces")
|
||||
.map(str_list)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.collect();
|
||||
let entities: Vec<&str> = surface.get("affected_existing_entities").map(str_list).unwrap_or_default();
|
||||
let entities: Vec<&str> = surface
|
||||
.get("affected_existing_entities")
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut tally = SignalTally { groups_declared, groups_hit, evidence };
|
||||
let mut tally = SignalTally {
|
||||
groups_declared,
|
||||
groups_hit,
|
||||
evidence,
|
||||
};
|
||||
|
||||
let surface_kinds = signals.get("surface_kinds").map(str_list).unwrap_or_default();
|
||||
_check(&mut tally, "surface_kinds", &surface_kinds, &|c| surface_kind_present(c, surface));
|
||||
let path_signals = signals.get("path_contains").map(str_list).unwrap_or_default();
|
||||
_check(&mut tally, "path_contains", &path_signals, &|c| paths.iter().any(|p| _path_matches(c, p)));
|
||||
let entity_signals = signals.get("entities_match").map(str_list).unwrap_or_default();
|
||||
_check(&mut tally, "entities_match", &entity_signals, &|c| entities.iter().any(|e| _entity_matches(c, e)));
|
||||
let objective_words: Vec<String> =
|
||||
objective.replace('/', " ").split_whitespace().map(|w| w.to_lowercase()).collect();
|
||||
let objective_signals_owned: Vec<String> =
|
||||
signals.get("objective_any").map(str_list).unwrap_or_default().into_iter().map(str::to_lowercase).collect();
|
||||
let surface_kinds = signals
|
||||
.get("surface_kinds")
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
_check(&mut tally, "surface_kinds", &surface_kinds, &|c| {
|
||||
surface_kind_present(c, surface)
|
||||
});
|
||||
let path_signals = signals
|
||||
.get("path_contains")
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
_check(&mut tally, "path_contains", &path_signals, &|c| {
|
||||
paths.iter().any(|p| _path_matches(c, p))
|
||||
});
|
||||
let entity_signals = signals
|
||||
.get("entities_match")
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
_check(&mut tally, "entities_match", &entity_signals, &|c| {
|
||||
entities.iter().any(|e| _entity_matches(c, e))
|
||||
});
|
||||
let objective_words: Vec<String> = objective
|
||||
.replace('/', " ")
|
||||
.split_whitespace()
|
||||
.map(|w| w.to_lowercase())
|
||||
.collect();
|
||||
let objective_signals_owned: Vec<String> = signals
|
||||
.get("objective_any")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(str::to_lowercase)
|
||||
.collect();
|
||||
let objective_signals: Vec<&str> = objective_signals_owned.iter().map(String::as_str).collect();
|
||||
_check(&mut tally, "objective_any", &objective_signals, &|c| objective_words.iter().any(|w| w == c));
|
||||
_check(&mut tally, "objective_any", &objective_signals, &|c| {
|
||||
objective_words.iter().any(|w| w == c)
|
||||
});
|
||||
|
||||
if let Some(contracts) = signals.get("contracts_match").and_then(|v| v.as_array()) {
|
||||
if !contracts.is_empty() {
|
||||
@@ -567,18 +679,27 @@ pub fn evaluate_applicability(
|
||||
.values()
|
||||
.flat_map(|doc| entities.iter().map(move |&e| (doc, e)))
|
||||
.flat_map(|(doc, e)| relationships(doc, e).into_iter().map(move |r| (e, r)))
|
||||
.filter(|(_, r)| REL_TYPES.contains(&r.get("type").and_then(|t| t.as_str()).unwrap_or("")))
|
||||
.filter(|(_, r)| {
|
||||
REL_TYPES.contains(&r.get("type").and_then(|t| t.as_str()).unwrap_or(""))
|
||||
})
|
||||
.map(|(e, r)| (e, r.get("to").and_then(|t| t.as_str()).unwrap_or("")))
|
||||
.filter(|(_, to)| wanted.contains(to))
|
||||
.map(|(e, to)| format!("{e}->{to}"))
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
tally.record_hits("contracts_match", hits.iter().take(5).map(|s| s.as_str()).collect());
|
||||
tally.record_hits(
|
||||
"contracts_match",
|
||||
hits.iter().take(5).map(|s| s.as_str()).collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let SignalTally { groups_declared, groups_hit, evidence } = tally;
|
||||
let SignalTally {
|
||||
groups_declared,
|
||||
groups_hit,
|
||||
evidence,
|
||||
} = tally;
|
||||
|
||||
if groups_declared == 0 {
|
||||
return json!({
|
||||
@@ -597,7 +718,11 @@ pub fn evaluate_applicability(
|
||||
};
|
||||
let mut hit_sorted = groups_hit.clone();
|
||||
hit_sorted.sort_unstable();
|
||||
let detail = if hit_sorted.is_empty() { "none".to_string() } else { py_str_list(&hit_sorted) };
|
||||
let detail = if hit_sorted.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
py_str_list(&hit_sorted)
|
||||
};
|
||||
json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": verdict.as_str(),
|
||||
@@ -620,15 +745,30 @@ pub fn _obligation_when_hit(
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("workflow_config_surfaces").map(str_list).unwrap_or_default())
|
||||
.chain(
|
||||
surface
|
||||
.get("workflow_config_surfaces")
|
||||
.map(str_list)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.collect();
|
||||
let entities: Vec<&str> = surface
|
||||
.get("affected_existing_entities")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("affected_ui_surfaces").map(str_list).unwrap_or_default())
|
||||
.chain(surface.get("affected_regions").map(str_list).unwrap_or_default())
|
||||
.chain(
|
||||
surface
|
||||
.get("affected_ui_surfaces")
|
||||
.map(str_list)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.chain(
|
||||
surface
|
||||
.get("affected_regions")
|
||||
.map(str_list)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.collect();
|
||||
|
||||
match when_shape(obligation) {
|
||||
@@ -646,9 +786,18 @@ pub fn _obligation_when_hit(
|
||||
.flat_map(|sub| paths.iter().filter(|p| _path_matches(sub, p)).copied())
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
return (ObligationVerdict::Applicable, format!("affected path(s): {}", py_str_list(&hits[..3.min(hits.len())])));
|
||||
return (
|
||||
ObligationVerdict::Applicable,
|
||||
format!(
|
||||
"affected path(s): {}",
|
||||
py_str_list(&hits[..3.min(hits.len())])
|
||||
),
|
||||
);
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("no affected path matches {}", py_repr(subs)))
|
||||
(
|
||||
ObligationVerdict::NotApplicable,
|
||||
format!("no affected path matches {}", py_repr(subs)),
|
||||
)
|
||||
}
|
||||
WhenShape::EntitiesMatch(subs) => {
|
||||
let hits: Vec<&str> = str_list(subs)
|
||||
@@ -658,10 +807,16 @@ pub fn _obligation_when_hit(
|
||||
if !hits.is_empty() {
|
||||
return (
|
||||
ObligationVerdict::Applicable,
|
||||
format!("affected entity/entities: {}", py_str_list(&hits[..3.min(hits.len())])),
|
||||
format!(
|
||||
"affected entity/entities: {}",
|
||||
py_str_list(&hits[..3.min(hits.len())])
|
||||
),
|
||||
);
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("no affected entity matches {}", py_repr(subs)))
|
||||
(
|
||||
ObligationVerdict::NotApplicable,
|
||||
format!("no affected entity matches {}", py_repr(subs)),
|
||||
)
|
||||
}
|
||||
WhenShape::ContractsTouched(wanted) => {
|
||||
let mut touched: Vec<String> = need
|
||||
@@ -680,17 +835,28 @@ pub fn _obligation_when_hit(
|
||||
}
|
||||
}
|
||||
}
|
||||
let hits: Vec<&str> =
|
||||
str_list(wanted).into_iter().filter(|c| touched.iter().any(|t| t == c)).collect();
|
||||
let hits: Vec<&str> = str_list(wanted)
|
||||
.into_iter()
|
||||
.filter(|c| touched.iter().any(|t| t == c))
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
return (ObligationVerdict::Applicable, format!("contract(s) touched: {}", py_str_list(&hits)));
|
||||
return (
|
||||
ObligationVerdict::Applicable,
|
||||
format!("contract(s) touched: {}", py_str_list(&hits)),
|
||||
);
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("touched contracts do not include {}", py_repr(wanted)))
|
||||
(
|
||||
ObligationVerdict::NotApplicable,
|
||||
format!("touched contracts do not include {}", py_repr(wanted)),
|
||||
)
|
||||
}
|
||||
WhenShape::Unrecognized(keys) => {
|
||||
let mut keys = keys;
|
||||
keys.sort_unstable();
|
||||
(ObligationVerdict::Unknown, format!("unrecognized when-condition shape: {}", py_str_list(&keys)))
|
||||
(
|
||||
ObligationVerdict::Unknown,
|
||||
format!("unrecognized when-condition shape: {}", py_str_list(&keys)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -703,9 +869,16 @@ pub fn derive_pattern_obligations(
|
||||
authorities: &Map<String, Value>,
|
||||
project_exceptions: &[Value],
|
||||
) -> Value {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let pattern_id = pattern
|
||||
.get("pattern_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let revision = pattern.get("revision").cloned().unwrap_or(json!(1));
|
||||
let provenance = pattern.get("provenance").cloned().unwrap_or_else(|| Value::Array(vec![]));
|
||||
let provenance = pattern
|
||||
.get("provenance")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Array(vec![]));
|
||||
let mut accepted = Vec::new();
|
||||
let mut rejected = Vec::new();
|
||||
let mut unresolved = Vec::new();
|
||||
@@ -715,7 +888,10 @@ pub fn derive_pattern_obligations(
|
||||
let o_obj: &Map<String, Value> = o.as_object().unwrap_or(&empty);
|
||||
let mut entry = Map::new();
|
||||
entry.insert("id".into(), o.get("id").cloned().unwrap_or(Value::Null));
|
||||
entry.insert("obligation".into(), o.get("obligation").cloned().unwrap_or(Value::Null));
|
||||
entry.insert(
|
||||
"obligation".into(),
|
||||
o.get("obligation").cloned().unwrap_or(Value::Null),
|
||||
);
|
||||
entry.insert("pattern".into(), Value::String(pattern_id.clone()));
|
||||
entry.insert("revision".into(), revision.clone());
|
||||
entry.insert("provenance".into(), Value::String("PATTERN".to_string()));
|
||||
@@ -728,26 +904,37 @@ pub fn derive_pattern_obligations(
|
||||
entry.insert("verdict".into(), Value::String("EXCEPTED".to_string()));
|
||||
entry.insert(
|
||||
"reason".into(),
|
||||
exc.get("reason").cloned().unwrap_or_else(|| Value::String(String::new())),
|
||||
exc.get("reason")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String(String::new())),
|
||||
);
|
||||
// Oracle field names on the exception are `evidence` /
|
||||
// `authority`; they land on the entry as exception_*.
|
||||
entry.insert(
|
||||
"exception_evidence".into(),
|
||||
exc.get("evidence").cloned().unwrap_or_else(|| Value::String(String::new())),
|
||||
exc.get("evidence")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String(String::new())),
|
||||
);
|
||||
entry.insert(
|
||||
"exception_authority".into(),
|
||||
exc.get("authority").cloned().unwrap_or_else(|| Value::String("user".to_string())),
|
||||
exc.get("authority")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String("user".to_string())),
|
||||
);
|
||||
rejected.push(Value::Object(entry));
|
||||
continue;
|
||||
}
|
||||
let (verdict, reason) = _obligation_when_hit(o_obj, need, surface, authorities);
|
||||
entry.insert("verdict".into(), Value::String(verdict.as_str().to_string()));
|
||||
entry.insert(
|
||||
"verdict".into(),
|
||||
Value::String(verdict.as_str().to_string()),
|
||||
);
|
||||
entry.insert("reason".into(), Value::String(reason));
|
||||
match verdict {
|
||||
ObligationVerdict::Required | ObligationVerdict::Applicable => accepted.push(Value::Object(entry)),
|
||||
ObligationVerdict::Required | ObligationVerdict::Applicable => {
|
||||
accepted.push(Value::Object(entry))
|
||||
}
|
||||
ObligationVerdict::NotApplicable => rejected.push(Value::Object(entry)),
|
||||
ObligationVerdict::Unknown => unresolved.push(Value::Object(entry)),
|
||||
}
|
||||
@@ -772,10 +959,20 @@ pub fn combine_pattern_obligations(derivations: &[Value]) -> Value {
|
||||
for d in derivations {
|
||||
if let Some(acc) = d.get("accepted").and_then(|v| v.as_array()) {
|
||||
for entry in acc {
|
||||
let oid = entry.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let oid = entry
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if let Some(prev) = merged.get_mut(&oid) {
|
||||
let prev_text = prev.get("obligation").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let new_text = entry.get("obligation").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let prev_text = prev
|
||||
.get("obligation")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let new_text = entry
|
||||
.get("obligation")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if prev_text != new_text {
|
||||
conflicts.push(json!({
|
||||
"obligation_id": oid,
|
||||
@@ -807,7 +1004,10 @@ pub fn combine_pattern_obligations(derivations: &[Value]) -> Value {
|
||||
}
|
||||
}
|
||||
order.sort_unstable();
|
||||
let obligations: Vec<Value> = order.into_iter().filter_map(|k| merged.remove(&k)).collect();
|
||||
let obligations: Vec<Value> = order
|
||||
.into_iter()
|
||||
.filter_map(|k| merged.remove(&k))
|
||||
.collect();
|
||||
json!({"obligations": obligations, "conflicts": conflicts})
|
||||
}
|
||||
|
||||
@@ -820,7 +1020,9 @@ fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -835,13 +1037,17 @@ fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
}
|
||||
|
||||
fn obj_arg(a: &Value) -> Result<Map<String, Value>, String> {
|
||||
a.as_object().cloned().ok_or_else(|| "argument must be a JSON object".to_string())
|
||||
a.as_object()
|
||||
.cloned()
|
||||
.ok_or_else(|| "argument must be a JSON object".to_string())
|
||||
}
|
||||
|
||||
/// cp_construct({pattern dict}) → normalized pattern dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_construct(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| construct_pattern(&a).map(Value::Object));
|
||||
let r = read_arg(arg)
|
||||
.and_then(|a| obj_arg(&a))
|
||||
.and_then(|a| construct_pattern(&a).map(Value::Object));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
@@ -849,10 +1055,17 @@ pub extern "C" fn cp_construct(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_revise(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let pattern = a
|
||||
.get("pattern")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let reason = a.get("reason").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let evidence: Vec<Value> =
|
||||
a.get("evidence").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
let evidence: Vec<Value> = a
|
||||
.get("evidence")
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let changes = a.get("changes").and_then(|v| v.as_object()).cloned();
|
||||
revise_pattern(&pattern, reason, &evidence, changes.as_ref()).map(Value::Object)
|
||||
});
|
||||
@@ -864,7 +1077,10 @@ pub extern "C" fn cp_revise(arg: *const c_char) -> *mut c_char {
|
||||
pub extern "C" fn cp_propose(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| {
|
||||
let evidence = a.get("evidence").map(str_list).unwrap_or_default();
|
||||
let confidence = a.get("confidence").and_then(|v| v.as_f64()).unwrap_or(f64::NAN);
|
||||
let confidence = a
|
||||
.get("confidence")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(f64::NAN);
|
||||
record_pattern_proposal(
|
||||
a.get("pattern_id").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
a.get("provider").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
@@ -880,11 +1096,27 @@ pub extern "C" fn cp_propose(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_evaluate(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let raw_need = a.get("need").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let pattern = a
|
||||
.get("pattern")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let raw_need = a
|
||||
.get("need")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let need = _need_dict(&raw_need);
|
||||
let surface = a.get("surface_result").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let authorities = a.get("authorities").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let surface = a
|
||||
.get("surface_result")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let authorities = a
|
||||
.get("authorities")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
evaluate_applicability(&pattern, &need, &surface, &authorities)
|
||||
});
|
||||
envelope(r)
|
||||
@@ -895,13 +1127,32 @@ pub extern "C" fn cp_evaluate(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_derive(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let raw_need = a.get("need").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let pattern = a
|
||||
.get("pattern")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let raw_need = a
|
||||
.get("need")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let need = _need_dict(&raw_need);
|
||||
let surface = a.get("surface_result").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let authorities = a.get("authorities").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let exceptions: Vec<Value> =
|
||||
a.get("project_exceptions").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
let surface = a
|
||||
.get("surface_result")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let authorities = a
|
||||
.get("authorities")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let exceptions: Vec<Value> = a
|
||||
.get("project_exceptions")
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
derive_pattern_obligations(&pattern, &need, &surface, &authorities, &exceptions)
|
||||
});
|
||||
envelope(r)
|
||||
@@ -911,8 +1162,11 @@ pub extern "C" fn cp_derive(arg: *const c_char) -> *mut c_char {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_combine(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let derivs: Vec<Value> =
|
||||
a.get("derivations").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
let derivs: Vec<Value> = a
|
||||
.get("derivations")
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
combine_pattern_obligations(&derivs)
|
||||
});
|
||||
envelope(r)
|
||||
|
||||
@@ -281,9 +281,7 @@ pub fn _resolve_and_project(
|
||||
InvokeOutcome::ImportCrash(e) => (Phase::Crash, e, false, None),
|
||||
InvokeOutcome::NotFound => (Phase::NotFound, String::new(), false, None),
|
||||
InvokeOutcome::CallCrash { error, callable }
|
||||
| InvokeOutcome::ProjectCrash { error, callable } => {
|
||||
(Phase::Crash, error, callable, None)
|
||||
}
|
||||
| InvokeOutcome::ProjectCrash { error, callable } => (Phase::Crash, error, callable, None),
|
||||
InvokeOutcome::Found {
|
||||
callable,
|
||||
projection,
|
||||
@@ -932,7 +930,12 @@ mod tests {
|
||||
/// unrecognized phase to Found — the oracle's catch-all arm.
|
||||
#[test]
|
||||
fn test_phase_roundtrip_and_catchall() {
|
||||
for p in [Phase::ImportError, Phase::Crash, Phase::NotFound, Phase::Found] {
|
||||
for p in [
|
||||
Phase::ImportError,
|
||||
Phase::Crash,
|
||||
Phase::NotFound,
|
||||
Phase::Found,
|
||||
] {
|
||||
assert_eq!(Phase::parse(p.as_str()), p);
|
||||
assert_eq!(p.to_string(), p.as_str());
|
||||
}
|
||||
|
||||
@@ -26,10 +26,7 @@ fn cases(group: &str) -> Vec<Value> {
|
||||
}
|
||||
|
||||
fn entries_of(i: &Value) -> Map<String, Value> {
|
||||
i["entries"]
|
||||
.as_object()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
i["entries"].as_object().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
@@ -47,16 +44,18 @@ fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
|
||||
#[test]
|
||||
fn compute_priority_parity_battery() {
|
||||
replay("compute_priority", |i| {
|
||||
json!({"priority": compute_priority(&i["entry"])})
|
||||
});
|
||||
replay(
|
||||
"compute_priority",
|
||||
|i| json!({"priority": compute_priority(&i["entry"])}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualifies_for_cache_parity_battery() {
|
||||
replay("qualifies_for_cache", |i| {
|
||||
json!({"qualifies": qualifies_for_cache(&i["entry"])})
|
||||
});
|
||||
replay(
|
||||
"qualifies_for_cache",
|
||||
|i| json!({"qualifies": qualifies_for_cache(&i["entry"])}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -64,8 +63,12 @@ fn add_parity_battery() {
|
||||
replay("add", |i| {
|
||||
let entries = entries_of(i);
|
||||
let now = i["now"].as_str().unwrap();
|
||||
let (entries, admitted, evicted) =
|
||||
add(&entries, i["entry_id"].as_i64().unwrap_or(-1), &i["entry"], now);
|
||||
let (entries, admitted, evicted) = add(
|
||||
&entries,
|
||||
i["entry_id"].as_i64().unwrap_or(-1),
|
||||
&i["entry"],
|
||||
now,
|
||||
);
|
||||
json!({"entries": entries, "admitted": admitted, "evicted": evicted})
|
||||
});
|
||||
}
|
||||
@@ -74,24 +77,29 @@ fn add_parity_battery() {
|
||||
fn access_parity_battery() {
|
||||
replay("access", |i| {
|
||||
let entries = entries_of(i);
|
||||
let (entries, changed) =
|
||||
access(&entries, i["entry_id"].as_i64().unwrap_or(-1), i["now"].as_str().unwrap());
|
||||
let (entries, changed) = access(
|
||||
&entries,
|
||||
i["entry_id"].as_i64().unwrap_or(-1),
|
||||
i["now"].as_str().unwrap(),
|
||||
);
|
||||
json!({"entries": entries, "changed": changed})
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_parity_battery() {
|
||||
replay("contains", |i| {
|
||||
json!({"contains": contains(&entries_of(i), i["entry_id"].as_i64().unwrap_or(-1))})
|
||||
});
|
||||
replay(
|
||||
"contains",
|
||||
|i| json!({"contains": contains(&entries_of(i), i["entry_id"].as_i64().unwrap_or(-1))}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_priority_parity_battery() {
|
||||
replay("get_priority", |i| {
|
||||
json!({"priority": get_priority(&entries_of(i), i["entry_id"].as_i64().unwrap_or(-1))})
|
||||
});
|
||||
replay(
|
||||
"get_priority",
|
||||
|i| json!({"priority": get_priority(&entries_of(i), i["entry_id"].as_i64().unwrap_or(-1))}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -118,16 +126,27 @@ fn main_cli_mirror_battery() {
|
||||
let i = &case["input"];
|
||||
let argv: Vec<String> = i["argv"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut entries = entries_of(i);
|
||||
let sync_rows = i["sync_rows"].as_object().cloned();
|
||||
let add_row = i["add_row"].as_array().map(|a| Value::Array(a.clone()));
|
||||
let now = i["now"].as_str().unwrap_or("");
|
||||
let (stdout, stderr, code) = crate::main(&argv, &mut entries, sync_rows.as_ref(), add_row.as_ref(), now);
|
||||
let (stdout, stderr, code) = crate::main(
|
||||
&argv,
|
||||
&mut entries,
|
||||
sync_rows.as_ref(),
|
||||
add_row.as_ref(),
|
||||
now,
|
||||
);
|
||||
let got = json!({"stdout": stdout, "stderr": stderr, "exit": code});
|
||||
assert_eq!(
|
||||
&got, &case["expected"],
|
||||
&got,
|
||||
&case["expected"],
|
||||
"[main] case '{}' diverged",
|
||||
case["name"].as_str().unwrap()
|
||||
);
|
||||
|
||||
@@ -215,7 +215,10 @@ fn evict(entries: &mut Map<String, Value>) -> Vec<i64> {
|
||||
for k in &evicted {
|
||||
entries.remove(k);
|
||||
}
|
||||
evicted.iter().filter_map(|k| k.parse::<i64>().ok()).collect()
|
||||
evicted
|
||||
.iter()
|
||||
.filter_map(|k| k.parse::<i64>().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `add` — admit a proven-or-important entry, stamp it with `now`, and
|
||||
@@ -257,7 +260,11 @@ pub fn add(
|
||||
|
||||
/// `access` — record a cache access (reinforces the entry). Missing id is
|
||||
/// a silent no-op (`changed: false`, oracle saves nothing).
|
||||
pub fn access(entries: &Map<String, Value>, entry_id: i64, now: &str) -> (Map<String, Value>, bool) {
|
||||
pub fn access(
|
||||
entries: &Map<String, Value>,
|
||||
entry_id: i64,
|
||||
now: &str,
|
||||
) -> (Map<String, Value>, bool) {
|
||||
let mut out = entries.clone();
|
||||
{
|
||||
let key = entry_id.to_string();
|
||||
@@ -295,7 +302,10 @@ pub fn get_priority(entries: &Map<String, Value>, entry_id: i64) -> f64 {
|
||||
/// `row[1] or 0.5`, `row[2] or "washable"`. Each row coerces ONCE —
|
||||
/// the priority is computed straight off the borrowed fields, never via
|
||||
/// an intermediate JSON object re-parse.
|
||||
pub fn update_entries(entries: &Map<String, Value>, rows: &Map<String, Value>) -> Map<String, Value> {
|
||||
pub fn update_entries(
|
||||
entries: &Map<String, Value>,
|
||||
rows: &Map<String, Value>,
|
||||
) -> Map<String, Value> {
|
||||
let mut out = entries.clone();
|
||||
let keys: Vec<String> = out.keys().cloned().collect();
|
||||
for key in keys {
|
||||
@@ -342,12 +352,10 @@ pub fn get_stats(entries: &Map<String, Value>) -> Value {
|
||||
(0.0f64, f64::NEG_INFINITY, f64::INFINITY, 0usize, 0usize),
|
||||
|(sum, max, min, rock, est), e| {
|
||||
let p = e.get("priority").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let rock = rock
|
||||
+ usize::from(e.get("rock_status").and_then(Value::as_str) == Some("rock"));
|
||||
let rock =
|
||||
rock + usize::from(e.get("rock_status").and_then(Value::as_str) == Some("rock"));
|
||||
let est = est
|
||||
+ usize::from(
|
||||
e.get("rock_status").and_then(Value::as_str) == Some("established"),
|
||||
);
|
||||
+ usize::from(e.get("rock_status").and_then(Value::as_str) == Some("established"));
|
||||
(sum + p, max.max(p), min.min(p), rock, est)
|
||||
},
|
||||
);
|
||||
@@ -467,7 +475,11 @@ pub fn main(
|
||||
});
|
||||
let (out, _admitted, _evicted) = add(entries, entry_id, &entry, now);
|
||||
*entries = out;
|
||||
(format!("Added entry {entry_id} to cache\n"), String::new(), 0)
|
||||
(
|
||||
format!("Added entry {entry_id} to cache\n"),
|
||||
String::new(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
None => (format!("Entry {entry_id} not found\n"), String::new(), 0),
|
||||
}
|
||||
@@ -536,17 +548,20 @@ fn entry_id_of(a: &Value) -> i64 {
|
||||
/// dcc_compute_priority({entry}) → {priority}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dcc_compute_priority(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!({"priority": compute_priority(a.get("entry").unwrap_or(&Value::Null))})
|
||||
}))
|
||||
envelope(
|
||||
read_arg(arg)
|
||||
.map(|a| json!({"priority": compute_priority(a.get("entry").unwrap_or(&Value::Null))})),
|
||||
)
|
||||
}
|
||||
|
||||
/// dcc_qualifies_for_cache({entry}) → {qualifies}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dcc_qualifies_for_cache(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!({"qualifies": qualifies_for_cache(a.get("entry").unwrap_or(&Value::Null))})
|
||||
}))
|
||||
envelope(
|
||||
read_arg(arg).map(
|
||||
|a| json!({"qualifies": qualifies_for_cache(a.get("entry").unwrap_or(&Value::Null))}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// dcc_add({entries, entry_id, entry, now}) → {entries, admitted, evicted}
|
||||
@@ -575,17 +590,15 @@ pub extern "C" fn dcc_access(arg: *const c_char) -> *mut c_char {
|
||||
/// dcc_contains({entries, entry_id}) → {contains}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dcc_contains(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!({"contains": contains(&entries_of(&a), entry_id_of(&a))})
|
||||
}))
|
||||
envelope(read_arg(arg).map(|a| json!({"contains": contains(&entries_of(&a), entry_id_of(&a))})))
|
||||
}
|
||||
|
||||
/// dcc_get_priority({entries, entry_id}) → {priority}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dcc_get_priority(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!({"priority": get_priority(&entries_of(&a), entry_id_of(&a))})
|
||||
}))
|
||||
envelope(
|
||||
read_arg(arg).map(|a| json!({"priority": get_priority(&entries_of(&a), entry_id_of(&a))})),
|
||||
)
|
||||
}
|
||||
|
||||
/// dcc_update_entries({entries, rows}) → {entries}
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
//! are replayed byte-for-byte, the IO-seam groups ride along as provenance
|
||||
//! and are re-verified live by the generator's RUNE_DL_CORE=auto parity pass).
|
||||
|
||||
use crate::{_auto_fix_decision, anchor_record, check_alerts, diff_blocks,
|
||||
fix_decision, ledger_path_for, sha, snapshot_entry};
|
||||
use crate::{
|
||||
_auto_fix_decision, anchor_record, check_alerts, diff_blocks, fix_decision, ledger_path_for,
|
||||
sha, snapshot_entry,
|
||||
};
|
||||
|
||||
const TRUTH: &str = r###"{
|
||||
"frozen": "drift_ledger sha/diff/ledger_path/load_ledger/save_ledger/read_block/journal/surface_alert/snapshot/anchor/check/auto_fix/fix/restore_topic/handle_hook/main",
|
||||
@@ -2059,50 +2061,102 @@ fn truth() -> serde_json::Value {
|
||||
|
||||
#[test]
|
||||
fn sha_matches_oracle() {
|
||||
for (idx, case) in truth()["sha"].as_array().expect("sha array").iter().enumerate() {
|
||||
for (idx, case) in truth()["sha"]
|
||||
.as_array()
|
||||
.expect("sha array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = sha(case["input"]["text"].as_str().unwrap_or(""));
|
||||
assert_eq!(got, case["expected"].as_str().unwrap_or(""), "sha case {}", idx);
|
||||
assert_eq!(
|
||||
got,
|
||||
case["expected"].as_str().unwrap_or(""),
|
||||
"sha case {}",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_matches_oracle() {
|
||||
for (idx, case) in truth()["diff"].as_array().expect("diff array").iter().enumerate() {
|
||||
let got = diff_blocks(case["input"]["ref"].as_str().unwrap_or(""),
|
||||
case["input"]["cur"].as_str().unwrap_or(""));
|
||||
assert_eq!(got, case["expected"].as_str().unwrap_or(""), "diff case {}", idx);
|
||||
for (idx, case) in truth()["diff"]
|
||||
.as_array()
|
||||
.expect("diff array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = diff_blocks(
|
||||
case["input"]["ref"].as_str().unwrap_or(""),
|
||||
case["input"]["cur"].as_str().unwrap_or(""),
|
||||
);
|
||||
assert_eq!(
|
||||
got,
|
||||
case["expected"].as_str().unwrap_or(""),
|
||||
"diff case {}",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ledger_path_matches_oracle() {
|
||||
for (idx, case) in truth()["ledger_path"].as_array().expect("ledger_path array").iter().enumerate() {
|
||||
for (idx, case) in truth()["ledger_path"]
|
||||
.as_array()
|
||||
.expect("ledger_path array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = ledger_path_for(case["input"]["index_file"].as_str().unwrap_or(""));
|
||||
assert_eq!(got, case["expected"].as_str().unwrap_or(""), "ledger_path case {}", idx);
|
||||
assert_eq!(
|
||||
got,
|
||||
case["expected"].as_str().unwrap_or(""),
|
||||
"ledger_path case {}",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_entry_matches_oracle() {
|
||||
for (idx, case) in truth()["k_snapshot"].as_array().expect("k_snapshot array").iter().enumerate() {
|
||||
let got = snapshot_entry(&case["input"]["blocks"], case["input"]["when"].as_str().unwrap_or(""));
|
||||
for (idx, case) in truth()["k_snapshot"]
|
||||
.as_array()
|
||||
.expect("k_snapshot array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = snapshot_entry(
|
||||
&case["input"]["blocks"],
|
||||
case["input"]["when"].as_str().unwrap_or(""),
|
||||
);
|
||||
assert_eq!(got, case["expected"], "k_snapshot case {}", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchor_record_matches_oracle() {
|
||||
for (idx, case) in truth()["k_anchor"].as_array().expect("k_anchor array").iter().enumerate() {
|
||||
let got = anchor_record(case["input"]["content"].as_str().unwrap_or(""),
|
||||
case["input"]["reason"].as_str().unwrap_or(""),
|
||||
case["input"]["when"].as_str().unwrap_or(""));
|
||||
for (idx, case) in truth()["k_anchor"]
|
||||
.as_array()
|
||||
.expect("k_anchor array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = anchor_record(
|
||||
case["input"]["content"].as_str().unwrap_or(""),
|
||||
case["input"]["reason"].as_str().unwrap_or(""),
|
||||
case["input"]["when"].as_str().unwrap_or(""),
|
||||
);
|
||||
assert_eq!(got, case["expected"], "k_anchor case {}", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_alerts_matches_oracle() {
|
||||
for (idx, case) in truth()["k_alerts"].as_array().expect("k_alerts array").iter().enumerate() {
|
||||
for (idx, case) in truth()["k_alerts"]
|
||||
.as_array()
|
||||
.expect("k_alerts array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = check_alerts(&case["input"]["ledger"], &case["input"]["blocks"]);
|
||||
assert_eq!(got, case["expected"], "k_alerts case {}", idx);
|
||||
}
|
||||
@@ -2110,20 +2164,37 @@ fn check_alerts_matches_oracle() {
|
||||
|
||||
#[test]
|
||||
fn auto_fix_decision_matches_oracle() {
|
||||
for (idx, case) in truth()["k_auto_fix"].as_array().expect("k_auto_fix array").iter().enumerate() {
|
||||
let got = _auto_fix_decision(case["input"]["label"].as_str().unwrap_or(""),
|
||||
matches!(case["input"]["anchored"], serde_json::Value::Bool(true)),
|
||||
matches!(case["input"]["has_last_good"], serde_json::Value::Bool(true)));
|
||||
for (idx, case) in truth()["k_auto_fix"]
|
||||
.as_array()
|
||||
.expect("k_auto_fix array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = _auto_fix_decision(
|
||||
case["input"]["label"].as_str().unwrap_or(""),
|
||||
matches!(case["input"]["anchored"], serde_json::Value::Bool(true)),
|
||||
matches!(
|
||||
case["input"]["has_last_good"],
|
||||
serde_json::Value::Bool(true)
|
||||
),
|
||||
);
|
||||
assert_eq!(got, case["expected"], "k_auto_fix case {}", idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fix_decision_matches_oracle() {
|
||||
for (idx, case) in truth()["k_fix"].as_array().expect("k_fix array").iter().enumerate() {
|
||||
let got = fix_decision(&case["input"]["ledger"],
|
||||
case["input"]["label"].as_str().unwrap_or(""),
|
||||
case["input"].get("cur_sha").and_then(|v| v.as_str()));
|
||||
for (idx, case) in truth()["k_fix"]
|
||||
.as_array()
|
||||
.expect("k_fix array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = fix_decision(
|
||||
&case["input"]["ledger"],
|
||||
case["input"]["label"].as_str().unwrap_or(""),
|
||||
case["input"].get("cur_sha").and_then(|v| v.as_str()),
|
||||
);
|
||||
assert_eq!(got, case["expected"], "k_fix case {}", idx);
|
||||
}
|
||||
}
|
||||
@@ -96,13 +96,21 @@ pub fn diff_blocks(reference: &str, current: &str) -> String {
|
||||
if !added.is_empty() {
|
||||
parts.push(format!(
|
||||
"ADDED: {}",
|
||||
added.iter().map(|l| l.chars().take(60).collect::<String>()).collect::<Vec<_>>().join(" | ")
|
||||
added
|
||||
.iter()
|
||||
.map(|l| l.chars().take(60).collect::<String>())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ")
|
||||
));
|
||||
}
|
||||
if !removed.is_empty() {
|
||||
parts.push(format!(
|
||||
"REMOVED: {}",
|
||||
removed.iter().map(|l| l.chars().take(60).collect::<String>()).collect::<Vec<_>>().join(" | ")
|
||||
removed
|
||||
.iter()
|
||||
.map(|l| l.chars().take(60).collect::<String>())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ")
|
||||
));
|
||||
}
|
||||
if parts.is_empty() {
|
||||
@@ -128,7 +136,11 @@ pub fn check_block(
|
||||
};
|
||||
let pct = (delta.abs() as f64) / (base.max(1) as f64);
|
||||
let is_constitution = matches!(BlockLabel::of_name(label), Some(BlockLabel::Constitution));
|
||||
let limit = if is_constitution { CONSTITUTION_LIMIT } else { DRIFT_LIMIT };
|
||||
let limit = if is_constitution {
|
||||
CONSTITUTION_LIMIT
|
||||
} else {
|
||||
DRIFT_LIMIT
|
||||
};
|
||||
let content_changed = is_constitution
|
||||
&& anchor_size.is_some()
|
||||
&& base_sha.is_some()
|
||||
@@ -185,7 +197,10 @@ pub fn ledger_path_for(index_file: &str) -> String {
|
||||
pub fn snapshot_entry(blocks: &Value, when: &str) -> Value {
|
||||
let mut b = serde_json::Map::new();
|
||||
for label in BlockLabel::ALL {
|
||||
let raw = blocks.get(label.as_str()).and_then(|v| v.as_str()).unwrap_or("");
|
||||
let raw = blocks
|
||||
.get(label.as_str())
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
b.insert(
|
||||
label.as_str().to_string(),
|
||||
json!({
|
||||
@@ -227,7 +242,10 @@ pub fn check_alerts(ledger: &Value, blocks: &Value) -> Value {
|
||||
let alerts: Vec<Value> = BlockLabel::ALL
|
||||
.iter()
|
||||
.filter_map(|label| {
|
||||
let cur = blocks.get(label.as_str()).and_then(|v| v.as_str()).unwrap_or("");
|
||||
let cur = blocks
|
||||
.get(label.as_str())
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let cur_size = cur.chars().count() as i64;
|
||||
let anchor_meta = anchors.get(label.as_str());
|
||||
let (base, base_sha, delta, ref_content) = match anchor_meta {
|
||||
@@ -305,7 +323,9 @@ fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -329,18 +349,14 @@ fn s(v: Option<&Value>) -> &str {
|
||||
/// dl_sha({"text"}) -> hex
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_sha(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
Value::String(sha(s(a.get("text"))))
|
||||
});
|
||||
let r = read_arg(arg).map(|a| Value::String(sha(s(a.get("text")))));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// dl_diff({"ref", "cur"}) -> summary string
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_diff(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
Value::String(diff_blocks(s(a.get("ref")), s(a.get("cur"))))
|
||||
});
|
||||
let r = read_arg(arg).map(|a| Value::String(diff_blocks(s(a.get("ref")), s(a.get("cur")))));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
@@ -391,9 +407,8 @@ pub extern "C" fn dl_snapshot_entry(arg: *const c_char) -> *mut c_char {
|
||||
/// dl_anchor_record({"content", "reason", "when"}) -> anchor record
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dl_anchor_record(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
anchor_record(s(a.get("content")), s(a.get("reason")), s(a.get("when")))
|
||||
});
|
||||
let r = read_arg(arg)
|
||||
.map(|a| anchor_record(s(a.get("content")), s(a.get("reason")), s(a.get("when"))));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
//! pinned (poisoned-truth law). Embedded VERBATIM — never transcribed.
|
||||
|
||||
use crate::{
|
||||
add_alert, acknowledge_alert, check_alerts, get_unacknowledged_alerts,
|
||||
run_retrospective,
|
||||
acknowledge_alert, add_alert, check_alerts, get_unacknowledged_alerts, run_retrospective,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@@ -17,10 +16,7 @@ const TRUTH: &str = include_str!("frozen_do_truth.json");
|
||||
|
||||
fn cases(group: &str) -> Vec<Value> {
|
||||
let truth: Value = serde_json::from_str(TRUTH).unwrap();
|
||||
truth["cases"][group]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone()
|
||||
truth["cases"][group].as_array().unwrap().clone()
|
||||
}
|
||||
|
||||
fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
@@ -83,9 +79,10 @@ fn run_retrospective_parity_battery() {
|
||||
|
||||
#[test]
|
||||
fn get_unacknowledged_alerts_parity_battery() {
|
||||
replay("get_unacknowledged_alerts", |i| {
|
||||
json!({"unacked": get_unacknowledged_alerts(i["alerts"].as_array().unwrap())})
|
||||
});
|
||||
replay(
|
||||
"get_unacknowledged_alerts",
|
||||
|i| json!({"unacked": get_unacknowledged_alerts(i["alerts"].as_array().unwrap())}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -147,10 +144,16 @@ fn main_parity_battery() {
|
||||
for case in cases("main") {
|
||||
let i = &case["input"];
|
||||
let st = DoMainState {
|
||||
alerts_text: i.get("alerts_text").and_then(Value::as_str).map(str::to_string),
|
||||
alerts_text: i
|
||||
.get("alerts_text")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
velocity: i["velocity"].clone(),
|
||||
direction: i["direction"].clone(),
|
||||
last_washed: i.get("last_washed").and_then(Value::as_str).map(str::to_string),
|
||||
last_washed: i
|
||||
.get("last_washed")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
counts: i["counts"].clone(),
|
||||
now: i["now"].as_str().unwrap_or("").to_string(),
|
||||
now_local: i["now_local"].as_str().unwrap_or("").to_string(),
|
||||
@@ -167,9 +170,21 @@ fn main_parity_battery() {
|
||||
let got = main(&argv, &st);
|
||||
let want = &case["expected"];
|
||||
let name = case["name"].as_str().unwrap();
|
||||
assert_eq!(got.exit as i64, want["exit"].as_i64().unwrap(), "[main] {name} exit");
|
||||
assert_eq!(got.stdout, want["stdout"].as_str().unwrap(), "[main] {name} stdout");
|
||||
assert_eq!(got.stderr, want["stderr"].as_str().unwrap(), "[main] {name} stderr");
|
||||
assert_eq!(
|
||||
got.exit as i64,
|
||||
want["exit"].as_i64().unwrap(),
|
||||
"[main] {name} exit"
|
||||
);
|
||||
assert_eq!(
|
||||
got.stdout,
|
||||
want["stdout"].as_str().unwrap(),
|
||||
"[main] {name} stdout"
|
||||
);
|
||||
assert_eq!(
|
||||
got.stderr,
|
||||
want["stderr"].as_str().unwrap(),
|
||||
"[main] {name} stderr"
|
||||
);
|
||||
if let Some(want_text) = want.get("alerts_file").and_then(Value::as_str) {
|
||||
let rendered = crate::_save_alerts(&got.alerts_after);
|
||||
assert_eq!(rendered, want_text, "[main] {name} alerts file bytes");
|
||||
@@ -182,7 +197,11 @@ fn main_parity_battery() {
|
||||
r.get("filename"),
|
||||
"[main] {name} retro filename"
|
||||
);
|
||||
assert_eq!(g.get("report"), r.get("report"), "[main] {name} retro report");
|
||||
assert_eq!(
|
||||
g.get("report"),
|
||||
r.get("report"),
|
||||
"[main] {name} retro report"
|
||||
);
|
||||
} else {
|
||||
assert!(got.retro.is_none(), "[main] {name} unexpected retro");
|
||||
}
|
||||
|
||||
@@ -221,7 +221,11 @@ pub fn check_alerts(
|
||||
let mut raised: Vec<NewAlert> = Vec::new();
|
||||
|
||||
// Velocity
|
||||
if velocity.get("alert").and_then(Value::as_bool).unwrap_or(false) {
|
||||
if velocity
|
||||
.get("alert")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
raised.push(NewAlert {
|
||||
kind: AlertKind::HighVelocity,
|
||||
message: format!(
|
||||
@@ -356,9 +360,7 @@ fn parse_iso(s: &str) -> Option<IsoInstant> {
|
||||
if bytes.len() < 19 {
|
||||
return None;
|
||||
}
|
||||
let num = |from: usize, to: usize| -> Option<i64> {
|
||||
s.get(from..to)?.parse::<i64>().ok()
|
||||
};
|
||||
let num = |from: usize, to: usize| -> Option<i64> { s.get(from..to)?.parse::<i64>().ok() };
|
||||
let (y, mo, d) = (num(0, 4)?, num(5, 7)?, num(8, 10)?);
|
||||
let sep = bytes[10];
|
||||
if sep != b'T' && sep != b't' && sep != b' ' {
|
||||
@@ -368,7 +370,10 @@ fn parse_iso(s: &str) -> Option<IsoInstant> {
|
||||
let mut rest = &s[19..];
|
||||
let mut micros = 0i64;
|
||||
if let Some(stripped) = rest.strip_prefix('.') {
|
||||
let digits: String = stripped.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
let digits: String = stripped
|
||||
.chars()
|
||||
.take_while(|c| c.is_ascii_digit())
|
||||
.collect();
|
||||
if digits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -391,7 +396,10 @@ fn parse_iso(s: &str) -> Option<IsoInstant> {
|
||||
let body = &rest[1..];
|
||||
let body = body.trim_end_matches(|c: char| c.is_whitespace());
|
||||
let (oh, om) = if body.len() == 5 && body.as_bytes()[2] == b':' {
|
||||
(body[0..2].parse::<i64>().ok()?, body[3..5].parse::<i64>().ok()?)
|
||||
(
|
||||
body[0..2].parse::<i64>().ok()?,
|
||||
body[3..5].parse::<i64>().ok()?,
|
||||
)
|
||||
} else if body.len() == 2 {
|
||||
(body[0..2].parse::<i64>().ok()?, 0)
|
||||
} else {
|
||||
@@ -401,7 +409,10 @@ fn parse_iso(s: &str) -> Option<IsoInstant> {
|
||||
}
|
||||
};
|
||||
let day_secs = days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + sec - offset_secs;
|
||||
Some(IsoInstant { micros: day_secs * 1_000_000 + micros, aware })
|
||||
Some(IsoInstant {
|
||||
micros: day_secs * 1_000_000 + micros,
|
||||
aware,
|
||||
})
|
||||
}
|
||||
|
||||
/// Howard Hinnant's days_from_civil — days since 1970-01-01.
|
||||
@@ -570,9 +581,7 @@ pub extern "C" fn dob_run_retrospective(arg: *const c_char) -> *mut c_char {
|
||||
/// dob_get_unacknowledged_alerts({alerts}) → {unacked}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dob_get_unacknowledged_alerts(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!({"unacked": get_unacknowledged_alerts(alerts_of(&a))})
|
||||
}))
|
||||
envelope(read_arg(arg).map(|a| json!({"unacked": get_unacknowledged_alerts(alerts_of(&a))})))
|
||||
}
|
||||
|
||||
/// dob_acknowledge_alert({alerts, index}) → {alerts, changed}
|
||||
@@ -790,7 +799,13 @@ pub fn main(argv: &[String], st: &DoMainState) -> DoMainOutcome {
|
||||
retro: None,
|
||||
};
|
||||
};
|
||||
let valid = ["check", "retrospective", "alerts", "acknowledge", "dashboard"];
|
||||
let valid = [
|
||||
"check",
|
||||
"retrospective",
|
||||
"alerts",
|
||||
"acknowledge",
|
||||
"dashboard",
|
||||
];
|
||||
if !valid.contains(&cmd.as_str()) {
|
||||
return top_usage_err(&format!(
|
||||
"argument cmd: invalid choice: '{cmd}' (choose from 'check', \
|
||||
@@ -819,9 +834,7 @@ fn subparser_err(sub: &str, usage: &str, detail: &str) -> DoMainOutcome {
|
||||
DoMainOutcome {
|
||||
exit: 2,
|
||||
stdout: String::new(),
|
||||
stderr: format!(
|
||||
"{usage}\ndrift_observability.py {sub}: error: {detail}\n"
|
||||
),
|
||||
stderr: format!("{usage}\ndrift_observability.py {sub}: error: {detail}\n"),
|
||||
alerts_after: Vec::new(),
|
||||
retro: None,
|
||||
}
|
||||
@@ -849,7 +862,13 @@ fn main_check(alerts: &[Value], st: &DoMainState) -> DoMainOutcome {
|
||||
));
|
||||
}
|
||||
}
|
||||
DoMainOutcome { exit: 0, stdout: out, stderr: String::new(), alerts_after: all, retro: None }
|
||||
DoMainOutcome {
|
||||
exit: 0,
|
||||
stdout: out,
|
||||
stderr: String::new(),
|
||||
alerts_after: all,
|
||||
retro: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn main_retrospective(days: i64, st: &DoMainState) -> DoMainOutcome {
|
||||
@@ -902,14 +921,16 @@ fn main_alerts(alerts: &[Value]) -> DoMainOutcome {
|
||||
));
|
||||
}
|
||||
}
|
||||
DoMainOutcome { exit: 0, stdout: out, stderr: String::new(), alerts_after: alerts.to_vec(), retro: None }
|
||||
DoMainOutcome {
|
||||
exit: 0,
|
||||
stdout: out,
|
||||
stderr: String::new(),
|
||||
alerts_after: alerts.to_vec(),
|
||||
retro: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn main_acknowledge(
|
||||
index_arg: Option<&str>,
|
||||
alerts: &[Value],
|
||||
_st: &DoMainState,
|
||||
) -> DoMainOutcome {
|
||||
fn main_acknowledge(index_arg: Option<&str>, alerts: &[Value], _st: &DoMainState) -> DoMainOutcome {
|
||||
let Some(raw) = index_arg else {
|
||||
return subparser_err(
|
||||
"acknowledge",
|
||||
@@ -927,7 +948,13 @@ fn main_acknowledge(
|
||||
};
|
||||
let (all, _) = acknowledge_alert(alerts, index);
|
||||
let out = format!("Alert {index} acknowledged.\n");
|
||||
DoMainOutcome { exit: 0, stdout: out, stderr: String::new(), alerts_after: all, retro: None }
|
||||
DoMainOutcome {
|
||||
exit: 0,
|
||||
stdout: out,
|
||||
stderr: String::new(),
|
||||
alerts_after: all,
|
||||
retro: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn main_dashboard(alerts: &[Value], st: &DoMainState) -> DoMainOutcome {
|
||||
@@ -945,12 +972,24 @@ fn main_dashboard(alerts: &[Value], st: &DoMainState) -> DoMainOutcome {
|
||||
out.push_str(&format!("Total entries: {total}\n"));
|
||||
out.push_str(&format!("Validated (proven, trusted): {established}\n"));
|
||||
out.push_str(&format!("Locked (immutable, identity anchors): {rock}\n"));
|
||||
out.push_str(&format!("Provisional (still being tested): {}\n", total - established - rock));
|
||||
out.push_str(&format!(
|
||||
"Provisional (still being tested): {}\n",
|
||||
total - established - rock
|
||||
));
|
||||
out.push_str(&format!("Trust ratio: {:.2}%\n", ratio * 100.0));
|
||||
out.push_str(&format!("Evolution direction: {}\n", py_scalar_str(&d("direction"))));
|
||||
out.push_str(&format!(
|
||||
"Evolution direction: {}\n",
|
||||
py_scalar_str(&d("direction"))
|
||||
));
|
||||
out.push_str("\n--- State Evolution ---\n");
|
||||
out.push_str(&format!("Changes/day: {}\n", py_scalar_str(&v("changes_per_day"))));
|
||||
out.push_str(&format!("Promotions/day: {}\n", py_scalar_str(&v("promotions_per_day"))));
|
||||
out.push_str(&format!(
|
||||
"Changes/day: {}\n",
|
||||
py_scalar_str(&v("changes_per_day"))
|
||||
));
|
||||
out.push_str(&format!(
|
||||
"Promotions/day: {}\n",
|
||||
py_scalar_str(&v("promotions_per_day"))
|
||||
));
|
||||
out.push_str(&format!("Alert: {}\n", py_scalar_str(&v("alert"))));
|
||||
out.push_str("\n--- Alerts ---\n");
|
||||
out.push_str(&format!("Unacknowledged: {}\n", unacked.len()));
|
||||
@@ -962,7 +1001,13 @@ fn main_dashboard(alerts: &[Value], st: &DoMainState) -> DoMainOutcome {
|
||||
a.get("message").and_then(Value::as_str).unwrap_or(""),
|
||||
));
|
||||
}
|
||||
DoMainOutcome { exit: 0, stdout: out, stderr: String::new(), alerts_after: alerts.to_vec(), retro: None }
|
||||
DoMainOutcome {
|
||||
exit: 0,
|
||||
stdout: out,
|
||||
stderr: String::new(),
|
||||
alerts_after: alerts.to_vec(),
|
||||
retro: None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Python-JSON writer (task_persist/task_gate law) ───────────────────────
|
||||
|
||||
@@ -37,17 +37,17 @@ fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
|
||||
#[test]
|
||||
fn classify_drift_parity_battery() {
|
||||
replay("classify_drift", |i| {
|
||||
json!({"record": classify_drift(&i["claim"], &i["observation"])})
|
||||
});
|
||||
replay(
|
||||
"classify_drift",
|
||||
|i| json!({"record": classify_drift(&i["claim"], &i["observation"])}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_parity_battery() {
|
||||
replay("reconcile", |i| {
|
||||
let claims = i["claims"].as_array().cloned().unwrap_or_default();
|
||||
let observations =
|
||||
i["observations"].as_array().cloned().unwrap_or_default();
|
||||
let observations = i["observations"].as_array().cloned().unwrap_or_default();
|
||||
reconcile(&claims, &observations)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,7 +156,11 @@ pub fn classify_drift(claim: &Value, observation: &Value) -> Value {
|
||||
Verdict::Matches => {
|
||||
let stale_ok = claim.get("stale_ok").map(py_truthy).unwrap_or(false);
|
||||
(
|
||||
if stale_ok { DriftClass::Expected } else { DriftClass::Confirmed },
|
||||
if stale_ok {
|
||||
DriftClass::Expected
|
||||
} else {
|
||||
DriftClass::Confirmed
|
||||
},
|
||||
get_or_dflt(observation, "checked_by", ""),
|
||||
)
|
||||
}
|
||||
@@ -168,8 +172,7 @@ pub fn classify_drift(claim: &Value, observation: &Value) -> Value {
|
||||
),
|
||||
),
|
||||
Verdict::Contradicted => {
|
||||
let user_locked =
|
||||
claim.get("user_locked").map(py_truthy).unwrap_or(false);
|
||||
let user_locked = claim.get("user_locked").map(py_truthy).unwrap_or(false);
|
||||
if user_locked {
|
||||
(
|
||||
DriftClass::Conflicting,
|
||||
@@ -263,7 +266,9 @@ impl MovedTwin {
|
||||
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(),
|
||||
MovedTwin::Absent => {
|
||||
"entity requires re-derivation or user-locked preservation".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,14 +279,8 @@ impl MovedTwin {
|
||||
/// files that no longer exist. REALITY WINS unless the claim is
|
||||
/// user-locked (identities.user_locked is True — the oracle's `is True`
|
||||
/// identity check, so a truthy non-True value is NOT locked).
|
||||
pub fn detect_structural_drift(
|
||||
entities: &[Value],
|
||||
files: &[Value],
|
||||
dirs: &[Value],
|
||||
) -> Vec<Value> {
|
||||
let in_set = |set: &[Value], src: &str| {
|
||||
set.iter().any(|p| p.as_str() == Some(src))
|
||||
};
|
||||
pub fn detect_structural_drift(entities: &[Value], files: &[Value], dirs: &[Value]) -> Vec<Value> {
|
||||
let in_set = |set: &[Value], src: &str| set.iter().any(|p| p.as_str() == Some(src));
|
||||
let mut drifts = Vec::new();
|
||||
for e in entities {
|
||||
if e.get("kind").and_then(Value::as_str) != Some("component") {
|
||||
@@ -290,23 +289,22 @@ pub fn detect_structural_drift(
|
||||
let Some(src) = source_of(e) else {
|
||||
continue;
|
||||
};
|
||||
let locked = obj_or_empty(e, "identities").get("user_locked")
|
||||
== Some(&Value::Bool(true));
|
||||
let locked = obj_or_empty(e, "identities").get("user_locked") == Some(&Value::Bool(true));
|
||||
let exists = in_set(files, src) || in_set(dirs, src);
|
||||
if exists {
|
||||
continue;
|
||||
}
|
||||
// reality: file gone/moved — search the facade listing for a
|
||||
// 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 expected_name = std::path::Path::new(src)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str());
|
||||
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())
|
||||
(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);
|
||||
@@ -365,9 +363,7 @@ fn envelope(r: Result<Value, FfiError>) -> *mut std::os::raw::c_char {
|
||||
|
||||
/// dr_classify_drift({claim, observation}) → {record}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dr_classify_drift(
|
||||
arg: *const std::os::raw::c_char,
|
||||
) -> *mut std::os::raw::c_char {
|
||||
pub extern "C" fn dr_classify_drift(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
json!({
|
||||
"record": classify_drift(
|
||||
@@ -380,9 +376,7 @@ pub extern "C" fn dr_classify_drift(
|
||||
|
||||
/// dr_reconcile({claims, observations}) → {resume, drifts, blocking}
|
||||
#[no_mangle]
|
||||
pub extern "C" fn dr_reconcile(
|
||||
arg: *const std::os::raw::c_char,
|
||||
) -> *mut std::os::raw::c_char {
|
||||
pub extern "C" fn dr_reconcile(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| {
|
||||
let claims = a.get("claims").and_then(Value::as_array);
|
||||
let observations = a.get("observations").and_then(Value::as_array);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! branch_battery — replays frozen_ec_truth.json VERBATIM.
|
||||
|
||||
use super::{pyjson, pack, size, unpack, Json};
|
||||
use super::{pack, pyjson, size, unpack, Json};
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_ec_truth.json");
|
||||
|
||||
|
||||
@@ -115,10 +115,7 @@ pub fn unpack(capsule: &Json) -> Json {
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Ok(x) => Json::Obj(vec![("ok".into(), Json::Bool(true)), ("value".into(), x)]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
|
||||
@@ -23,10 +23,7 @@ fn at<'a>(v: &'a Json, path: &[&str]) -> Option<&'a Json> {
|
||||
let mut cur = v;
|
||||
for k in path {
|
||||
cur = match cur {
|
||||
Json::Obj(pairs) => pairs
|
||||
.iter()
|
||||
.find(|(k2, _)| k2 == k)
|
||||
.map(|(_, x)| x)?,
|
||||
Json::Obj(pairs) => pairs.iter().find(|(k2, _)| k2 == k).map(|(_, x)| x)?,
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
@@ -126,9 +123,7 @@ fn replay_trace_task() {
|
||||
let lines = at(&c, &["args", "lines"]);
|
||||
let outcomes = match lines {
|
||||
None | Some(Json::Null) => None,
|
||||
Some(Json::Arr(items)) => {
|
||||
Some(read_outcome_events(items).expect("oracle-safe corpus"))
|
||||
}
|
||||
Some(Json::Arr(items)) => Some(read_outcome_events(items).expect("oracle-safe corpus")),
|
||||
_ => None,
|
||||
};
|
||||
let task_record = match at(&c, &["args", "task_record"]) {
|
||||
|
||||
@@ -74,8 +74,7 @@ pub fn read_outcome_events(lines: &[Json]) -> Result<Json, (String, String)> {
|
||||
// find-then-scan for duplicate task_ids — one pass over a journal of
|
||||
// N lines is O(N) instead of O(N²), with insertion order preserved
|
||||
// (the dict-assignment law needs first-seen ordering, not sorted).
|
||||
let mut index: std::collections::HashMap<String, usize> =
|
||||
std::collections::HashMap::new();
|
||||
let mut index: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
for line in lines {
|
||||
let Json::Str(raw) = line else { continue }; // TypeError branch
|
||||
let rec = match pyjson::parse(raw) {
|
||||
@@ -155,10 +154,19 @@ pub fn build_receipt(task: &Json, payload: Option<&Json>) -> Json {
|
||||
("description", field(Some(task), "description", s(""))),
|
||||
("status", status),
|
||||
("model", model),
|
||||
("memories_used", field(Some(task), "memories_used", Json::Arr(Vec::new()))),
|
||||
("rules_used", field(Some(task), "rules_used", Json::Arr(Vec::new()))),
|
||||
(
|
||||
"memories_used",
|
||||
field(Some(task), "memories_used", Json::Arr(Vec::new())),
|
||||
),
|
||||
(
|
||||
"rules_used",
|
||||
field(Some(task), "rules_used", Json::Arr(Vec::new())),
|
||||
),
|
||||
("capability", field(Some(task), "capability", s(""))),
|
||||
("actions", field(Some(task), "actions", Json::Arr(Vec::new()))),
|
||||
(
|
||||
"actions",
|
||||
field(Some(task), "actions", Json::Arr(Vec::new())),
|
||||
),
|
||||
("outcome", outcome),
|
||||
("completed_at", completed),
|
||||
])
|
||||
@@ -206,11 +214,9 @@ fn envelope(result: Result<Json, (String, String)>) -> *mut c_char {
|
||||
let out = pyjson::dumps(&body);
|
||||
match CString::new(out) {
|
||||
Ok(c) => c.into_raw(),
|
||||
Err(_) => CString::new(
|
||||
r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#,
|
||||
)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
Err(_) => CString::new(r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,51 +245,68 @@ macro_rules! ffi {
|
||||
($name:ident, $body:expr) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn $name(arg: *const c_char) -> *mut c_char {
|
||||
let out: Result<Json, (String, String)> =
|
||||
read_arg(arg).and_then(|raw| $body(&raw));
|
||||
let out: Result<Json, (String, String)> = read_arg(arg).and_then(|raw| $body(&raw));
|
||||
envelope(out)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ffi!(et_read_outcome_events, |raw: &str| -> Result<Json, (String, String)> {
|
||||
ffi!(et_read_outcome_events, |raw: &str| -> Result<
|
||||
Json,
|
||||
(String, String),
|
||||
> {
|
||||
let doc = parse_doc(raw)?;
|
||||
match doc.get("lines") {
|
||||
Some(Json::Arr(items)) => read_outcome_events(items),
|
||||
_ => Err(("lines must be a list of journal lines".into(), "TypeError".into())),
|
||||
_ => Err((
|
||||
"lines must be a list of journal lines".into(),
|
||||
"TypeError".into(),
|
||||
)),
|
||||
}
|
||||
});
|
||||
|
||||
ffi!(et_build_receipt, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let task = want_obj(&doc, "task")?
|
||||
.ok_or(("task must be an object".into(), "TypeError".into()))?;
|
||||
if !matches!(task, Json::Obj(_)) {
|
||||
return Err(("task must be an object".into(), "TypeError".into()));
|
||||
ffi!(
|
||||
et_build_receipt,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let task =
|
||||
want_obj(&doc, "task")?.ok_or(("task must be an object".into(), "TypeError".into()))?;
|
||||
if !matches!(task, Json::Obj(_)) {
|
||||
return Err(("task must be an object".into(), "TypeError".into()));
|
||||
}
|
||||
Ok(build_receipt(task, want_obj(&doc, "outcome_payload")?))
|
||||
}
|
||||
Ok(build_receipt(task, want_obj(&doc, "outcome_payload")?))
|
||||
});
|
||||
);
|
||||
|
||||
ffi!(et_trace_task, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let task_id = match doc.get("task_id") {
|
||||
Some(Json::Str(t)) => t.clone(),
|
||||
_ => return Err(("task_id must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let task_record = want_obj(&doc, "task_record")?;
|
||||
if let Some(rec) = task_record {
|
||||
if !matches!(rec, Json::Obj(_)) {
|
||||
return Err(("task_record must be an object or null".into(), "TypeError".into()));
|
||||
ffi!(
|
||||
et_trace_task,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let task_id = match doc.get("task_id") {
|
||||
Some(Json::Str(t)) => t.clone(),
|
||||
_ => return Err(("task_id must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let task_record = want_obj(&doc, "task_record")?;
|
||||
if let Some(rec) = task_record {
|
||||
if !matches!(rec, Json::Obj(_)) {
|
||||
return Err((
|
||||
"task_record must be an object or null".into(),
|
||||
"TypeError".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let outcomes = want_obj(&doc, "outcomes")?;
|
||||
if let Some(o) = outcomes {
|
||||
if !matches!(o, Json::Obj(_)) {
|
||||
return Err(("outcomes must be an object or null".into(), "TypeError".into()));
|
||||
let outcomes = want_obj(&doc, "outcomes")?;
|
||||
if let Some(o) = outcomes {
|
||||
if !matches!(o, Json::Obj(_)) {
|
||||
return Err((
|
||||
"outcomes must be an object or null".into(),
|
||||
"TypeError".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(trace_task(&task_id, task_record, outcomes))
|
||||
}
|
||||
Ok(trace_task(&task_id, task_record, outcomes))
|
||||
});
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
@@ -23,11 +23,18 @@ fn s<'a>(v: &'a Value, key: &str) -> &'a str {
|
||||
#[test]
|
||||
fn battery_replays_frozen_truth_verbatim() {
|
||||
let rows = rows();
|
||||
assert!(rows.len() >= 300, "truth must be substantial, got {}", rows.len());
|
||||
assert!(
|
||||
rows.len() >= 300,
|
||||
"truth must be substantial, got {}",
|
||||
rows.len()
|
||||
);
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
let fname = s(row, "fn");
|
||||
let expected = s(row, "expected");
|
||||
let args = row.get("args").and_then(|a| a.as_array()).expect("args array");
|
||||
let args = row
|
||||
.get("args")
|
||||
.and_then(|a| a.as_array())
|
||||
.expect("args array");
|
||||
let outcome = match fname {
|
||||
"affirm_rule" => affirm_rule(args[0].as_str().expect("fact arg")).into_owned(),
|
||||
"grade_fact" => {
|
||||
@@ -40,9 +47,13 @@ fn battery_replays_frozen_truth_verbatim() {
|
||||
other => panic!("unknown fn in truth row {}: {}", i, other),
|
||||
};
|
||||
assert_eq!(
|
||||
outcome, expected,
|
||||
outcome,
|
||||
expected,
|
||||
"row {} diverged: fn={} args={:?} user_name={:?}",
|
||||
i, fname, args, row.get("user_name")
|
||||
i,
|
||||
fname,
|
||||
args,
|
||||
row.get("user_name")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,14 +77,20 @@ fn truth_pin_name_token_branch() {
|
||||
.zip(named.iter())
|
||||
.filter(|(a, b)| a["args"] == b["args"] && s(a, "expected") != s(b, "expected"))
|
||||
.count();
|
||||
assert_eq!(divergent, 2, "name-token branch must flip exactly the 2 name rows");
|
||||
assert_eq!(
|
||||
divergent, 2,
|
||||
"name-token branch must flip exactly the 2 name rows"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn affirm_dict_order_is_first_match_wins() {
|
||||
// dict insertion order: "never hedge" precedes "don't stall",
|
||||
// "do not overclaim" precedes "don't stall".
|
||||
assert_eq!(affirm_rule("never hedge and don't stall"), "I answer directly, without hedging");
|
||||
assert_eq!(
|
||||
affirm_rule("never hedge and don't stall"),
|
||||
"I answer directly, without hedging"
|
||||
);
|
||||
assert_eq!(
|
||||
affirm_rule("don't stall and do not overclaim"),
|
||||
"I name evidence as evidence and confirmed causes as confirmed"
|
||||
@@ -131,5 +148,8 @@ fn affirm_pass_through_is_zero_copy() {
|
||||
std::borrow::Cow::Owned(s) => panic!("pass-through allocated: {s}"),
|
||||
}
|
||||
// A known prohibition rewrites -> owned.
|
||||
assert!(matches!(affirm_rule("never hedge"), std::borrow::Cow::Owned(_)));
|
||||
assert!(matches!(
|
||||
affirm_rule("never hedge"),
|
||||
std::borrow::Cow::Owned(_)
|
||||
));
|
||||
}
|
||||
+124
-27
@@ -29,47 +29,123 @@
|
||||
|
||||
/// Imperative / rule signals: a sentence instructing HOW to operate.
|
||||
pub const RULE_WEAK: &[&str] = &[
|
||||
"should", "prefer", "instead of", "lead with", "first", "before ", "avoid",
|
||||
"favour", "keep ", "stop ", "start ", "use ",
|
||||
"should",
|
||||
"prefer",
|
||||
"instead of",
|
||||
"lead with",
|
||||
"first",
|
||||
"before ",
|
||||
"avoid",
|
||||
"favour",
|
||||
"keep ",
|
||||
"stop ",
|
||||
"start ",
|
||||
"use ",
|
||||
];
|
||||
pub const RULE_STRONG: &[&str] = &[
|
||||
"always", "never ", "must", "do not", "don't", "i always", "i must",
|
||||
"i should",
|
||||
"always", "never ", "must", "do not", "don't", "i always", "i must", "i should",
|
||||
];
|
||||
/// Positive-affirmation hint words — behavioural, not factual.
|
||||
pub const AFFIRM_SIGNALS: &[&str] = &[
|
||||
"i protect", "i am", "i reveal", "i factor", "i dissolve", "i respect",
|
||||
"i teach", "i make", "i write", "i use the tiers", "i check", "i stop",
|
||||
"i protect",
|
||||
"i am",
|
||||
"i reveal",
|
||||
"i factor",
|
||||
"i dissolve",
|
||||
"i respect",
|
||||
"i teach",
|
||||
"i make",
|
||||
"i write",
|
||||
"i use the tiers",
|
||||
"i check",
|
||||
"i stop",
|
||||
];
|
||||
/// Subject signals: who/what the fact is about (portable — no hardcoded
|
||||
/// identity; the user's name arrives via the caller).
|
||||
pub const USER_FACT_SIGNALS: &[&str] = &[
|
||||
"name", "email", "role", "lives in", "located in", "daw", "os:", "shell",
|
||||
"has a dog", "has a cat", "prefers", "works in", "works at", "teaches",
|
||||
"studies", "interested in", "financial", "income", "dietary", "location",
|
||||
"my home", "my machine",
|
||||
"name",
|
||||
"email",
|
||||
"role",
|
||||
"lives in",
|
||||
"located in",
|
||||
"daw",
|
||||
"os:",
|
||||
"shell",
|
||||
"has a dog",
|
||||
"has a cat",
|
||||
"prefers",
|
||||
"works in",
|
||||
"works at",
|
||||
"teaches",
|
||||
"studies",
|
||||
"interested in",
|
||||
"financial",
|
||||
"income",
|
||||
"dietary",
|
||||
"location",
|
||||
"my home",
|
||||
"my machine",
|
||||
];
|
||||
/// Decisive personal-attribute signals: alone, they mark a fact as about
|
||||
/// the user — no second signal needed.
|
||||
pub const USER_ATTR_DECISIVE: &[&str] = &[
|
||||
"prefers", "lives in", "located in", "teaches", "studies", "works at",
|
||||
"works in", "interested in", "dietary", "income", "financial",
|
||||
"prefers",
|
||||
"lives in",
|
||||
"located in",
|
||||
"teaches",
|
||||
"studies",
|
||||
"works at",
|
||||
"works in",
|
||||
"interested in",
|
||||
"dietary",
|
||||
"income",
|
||||
"financial",
|
||||
];
|
||||
pub const PROJECT_SIGNALS: &[&str] = &[
|
||||
"project", "current work", "task", "todo", "goal", "milestone",
|
||||
"deadline", "in progress", "blocked", "decision", "spec", "wip",
|
||||
"building ", "developing ", "course", "channel", "migration", "feature",
|
||||
"fixing", "working on", "active projects", "current state",
|
||||
"project",
|
||||
"current work",
|
||||
"task",
|
||||
"todo",
|
||||
"goal",
|
||||
"milestone",
|
||||
"deadline",
|
||||
"in progress",
|
||||
"blocked",
|
||||
"decision",
|
||||
"spec",
|
||||
"wip",
|
||||
"building ",
|
||||
"developing ",
|
||||
"course",
|
||||
"channel",
|
||||
"migration",
|
||||
"feature",
|
||||
"fixing",
|
||||
"working on",
|
||||
"active projects",
|
||||
"current state",
|
||||
];
|
||||
/// Directives that grow the constitution.
|
||||
pub const CONSTITUTION_SIGNALS: &[&str] = &[
|
||||
"add this to the constitution", "constitution", "always do",
|
||||
"i will always", "must always", "inviolable",
|
||||
"add this to the constitution",
|
||||
"constitution",
|
||||
"always do",
|
||||
"i will always",
|
||||
"must always",
|
||||
"inviolable",
|
||||
];
|
||||
/// Blocks the grader would never route straight to a hot block.
|
||||
pub const DOC_SIGNALS: &[&str] = &[
|
||||
"book", "chapter", "document", "transcript", "pdf", "epub", "verbatim",
|
||||
"diary entry", "palace drawer", "gutenberg",
|
||||
"book",
|
||||
"chapter",
|
||||
"document",
|
||||
"transcript",
|
||||
"pdf",
|
||||
"epub",
|
||||
"verbatim",
|
||||
"diary entry",
|
||||
"palace drawer",
|
||||
"gutenberg",
|
||||
];
|
||||
|
||||
/// Curated prohibition -> true positive affirmation, in the oracle's
|
||||
@@ -97,8 +173,14 @@ pub const AFFIRM_DICT: &[(&str, &str)] = &[
|
||||
),
|
||||
("never hedge", "I answer directly, without hedging"),
|
||||
("don't hedge", "I answer directly, without hedging"),
|
||||
("do not overresearch", "I act on the quick answer when one will do"),
|
||||
("don't overresearch", "I act on the quick answer when one will do"),
|
||||
(
|
||||
"do not overresearch",
|
||||
"I act on the quick answer when one will do",
|
||||
),
|
||||
(
|
||||
"don't overresearch",
|
||||
"I act on the quick answer when one will do",
|
||||
),
|
||||
(
|
||||
"do not overclaim",
|
||||
"I name evidence as evidence and confirmed causes as confirmed",
|
||||
@@ -210,7 +292,8 @@ fn contains_space_bounded(low: &str, name: &str) -> bool {
|
||||
while let Some(pos) = low[from..].find(name) {
|
||||
let i = from + pos;
|
||||
let after = i + name.len();
|
||||
let bounded = (i == 0 || bytes[i - 1] == b' ') && (after == low.len() || bytes[after] == b' ');
|
||||
let bounded =
|
||||
(i == 0 || bytes[i - 1] == b' ') && (after == low.len() || bytes[after] == b' ');
|
||||
if bounded {
|
||||
return true;
|
||||
}
|
||||
@@ -226,7 +309,11 @@ fn contains_space_bounded(low: &str, name: &str) -> bool {
|
||||
/// the pass-through path (Cow borrows `fact` after trim).
|
||||
pub fn affirm_rule(fact: &str) -> std::borrow::Cow<'_, str> {
|
||||
let f = fact.trim().trim_start_matches(['-', '*', ' ']).trim();
|
||||
let low = f.to_lowercase().split_whitespace().collect::<Vec<&str>>().join(" ");
|
||||
let low = f
|
||||
.to_lowercase()
|
||||
.split_whitespace()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
// The oracle's second needle (low.strip(".").replace(" ", " ")) is
|
||||
// key-independent and can only differ from `low` when edge dots or
|
||||
// double spaces exist — normalize ONCE under that guard, lazily,
|
||||
@@ -371,7 +458,10 @@ fn str_field(v: &serde_json::Value, key: &str) -> String {
|
||||
/// gr_affirm_rule({fact}) -> the (possibly rewritten) rule string.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gr_affirm_rule(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| serde_json::Value::String(affirm_rule(&str_field(&a, "fact")).into_owned())))
|
||||
envelope(
|
||||
read_arg(arg)
|
||||
.map(|a| serde_json::Value::String(affirm_rule(&str_field(&a, "fact")).into_owned())),
|
||||
)
|
||||
}
|
||||
|
||||
/// gr_grade_fact({fact, target, user_name}) -> the tier name.
|
||||
@@ -383,14 +473,21 @@ pub extern "C" fn gr_grade_fact(arg: *const std::os::raw::c_char) -> *mut std::o
|
||||
_ => None,
|
||||
};
|
||||
let user_name = str_field(&a, "user_name");
|
||||
serde_json::Value::String(grade_fact(&str_field(&a, "fact"), target, &user_name).as_str().to_string())
|
||||
serde_json::Value::String(
|
||||
grade_fact(&str_field(&a, "fact"), target, &user_name)
|
||||
.as_str()
|
||||
.to_string(),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
/// gr_tier_of({grade}) -> the journal label.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gr_tier_of(arg: *const std::os::raw::c_char) -> *mut std::os::raw::c_char {
|
||||
envelope(read_arg(arg).map(|a| serde_json::Value::String(tier_of(&str_field(&a, "grade")).to_string())))
|
||||
envelope(
|
||||
read_arg(arg)
|
||||
.map(|a| serde_json::Value::String(tier_of(&str_field(&a, "grade")).to_string())),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -124,11 +124,19 @@ impl Store {
|
||||
}
|
||||
}
|
||||
fn ename(&self, id: i64) -> &str {
|
||||
self.entities.iter().find(|e| e.id == id).map(|e| e.name.as_str()).unwrap_or("")
|
||||
self.entities
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.map(|e| e.name.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
fn etype(&self, id: i64) -> &str {
|
||||
self.entities.iter().find(|e| e.id == id).map(|e| e.etype.as_str()).unwrap_or("")
|
||||
self.entities
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.map(|e| e.etype.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -214,14 +222,21 @@ impl Store {
|
||||
if term.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Some(e) = self.entities.iter().find(|e| e.name.to_lowercase() == term.to_lowercase()) {
|
||||
if let Some(e) = self
|
||||
.entities
|
||||
.iter()
|
||||
.find(|e| e.name.to_lowercase() == term.to_lowercase())
|
||||
{
|
||||
return Some(e.id);
|
||||
}
|
||||
let lower = term.to_lowercase();
|
||||
if let Some((eid, _)) = self.aliases.iter().find(|(_, a)| a.to_lowercase() == lower) {
|
||||
return Some(*eid);
|
||||
}
|
||||
self.entities.iter().find(|e| e.name.contains(term)).map(|e| e.id)
|
||||
self.entities
|
||||
.iter()
|
||||
.find(|e| e.name.contains(term))
|
||||
.map(|e| e.id)
|
||||
}
|
||||
|
||||
fn query(&self, term: &str, hops: i64) -> Value {
|
||||
@@ -237,7 +252,11 @@ impl Store {
|
||||
if depth > hops {
|
||||
break;
|
||||
}
|
||||
for e in self.edges.iter().filter(|e| e.subj == cur && e.status == "active") {
|
||||
for e in self
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|e| e.subj == cur && e.status == "active")
|
||||
{
|
||||
facts.push(query_fact(
|
||||
self.ename(e.subj),
|
||||
&e.pred,
|
||||
@@ -256,7 +275,11 @@ impl Store {
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in self.edges.iter().filter(|e| e.obj == eid && e.status == "active") {
|
||||
for e in self
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|e| e.obj == eid && e.status == "active")
|
||||
{
|
||||
facts.push(query_fact(
|
||||
self.ename(e.subj),
|
||||
&e.pred,
|
||||
@@ -331,7 +354,10 @@ fn gap_truth_replays_through_kernel() {
|
||||
}
|
||||
"gm_resolve" => {
|
||||
let name = s(args.first().unwrap_or(&Value::Null));
|
||||
let nt = args.get(1).map(|v| s(&v.clone())).unwrap_or_else(|| "concept".to_string());
|
||||
let nt = args
|
||||
.get(1)
|
||||
.map(|v| s(&v.clone()))
|
||||
.unwrap_or_else(|| "concept".to_string());
|
||||
match store.resolve(&name, &nt) {
|
||||
None => Value::Null,
|
||||
Some(id) => json!({"id": id, "name": store.ename(id), "type": store.etype(id)}),
|
||||
@@ -379,7 +405,13 @@ fn gap_truth_replays_through_kernel() {
|
||||
mut o => {
|
||||
if let Some(facts) = o.get("facts").cloned() {
|
||||
o["facts"] = Value::Array(
|
||||
facts.as_array().cloned().unwrap_or_default().into_iter().map(norm_fact).collect(),
|
||||
facts
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(norm_fact)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
o
|
||||
@@ -394,7 +426,13 @@ fn gap_truth_replays_through_kernel() {
|
||||
if let Some(o) = want.as_object_mut() {
|
||||
if let Some(facts) = o.get("facts").cloned() {
|
||||
want["facts"] = Value::Array(
|
||||
facts.as_array().cloned().unwrap_or_default().into_iter().map(norm_fact).collect(),
|
||||
facts
|
||||
.as_array()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(norm_fact)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -402,7 +440,11 @@ fn gap_truth_replays_through_kernel() {
|
||||
// now_iso carries the frozen clock's +00:00 suffix; the kernel
|
||||
// asserts the truncation SHAPE, not the envelope's clock.
|
||||
if fn_name == "now_iso" {
|
||||
assert_eq!(got, json!("2026-10-13T12:00:00"), "now_iso truncation shape");
|
||||
assert_eq!(
|
||||
got,
|
||||
json!("2026-10-13T12:00:00"),
|
||||
"now_iso truncation shape"
|
||||
);
|
||||
n += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -429,7 +471,10 @@ fn gap_truth_replays_through_kernel() {
|
||||
#[test]
|
||||
fn kernel_unit_pins() {
|
||||
// now_iso truncation shapes.
|
||||
assert_eq!(now_iso("2026-10-13T12:00:00.123456+00:00"), "2026-10-13T12:00:00+00:00");
|
||||
assert_eq!(
|
||||
now_iso("2026-10-13T12:00:00.123456+00:00"),
|
||||
"2026-10-13T12:00:00+00:00"
|
||||
);
|
||||
assert_eq!(now_iso("2026-10-13T12:00:00"), "2026-10-13T12:00:00");
|
||||
// gating boundaries.
|
||||
assert_eq!(strength_to_conf(Some(&json!(2.9999))), None);
|
||||
|
||||
@@ -306,19 +306,23 @@ pub extern "C" fn gm_now_iso(arg: *const c_char) -> *mut c_char {
|
||||
/// gm_strength_to_conf({strength}) -> {conf, ok}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_strength_to_conf(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| match strength_to_conf(a.get("strength")) {
|
||||
Some(c) => json!({"conf": c, "ok": true}),
|
||||
None => json!({"conf": 0.0, "ok": false}),
|
||||
}))
|
||||
envelope(
|
||||
read_arg(arg).map(|a| match strength_to_conf(a.get("strength")) {
|
||||
Some(c) => json!({"conf": c, "ok": true}),
|
||||
None => json!({"conf": 0.0, "ok": false}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// gm_normalize_pred({pred}) -> {pred, ok}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gm_normalize_pred(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).map(|a| match normalize_pred(&py_str(a.get("pred"))) {
|
||||
Some(p) => json!({"pred": p, "ok": true}),
|
||||
None => json!({"pred": "", "ok": false}),
|
||||
}))
|
||||
envelope(
|
||||
read_arg(arg).map(|a| match normalize_pred(&py_str(a.get("pred"))) {
|
||||
Some(p) => json!({"pred": p, "ok": true}),
|
||||
None => json!({"pred": "", "ok": false}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// gm_node_type({node_type}) -> effective type.
|
||||
@@ -373,7 +377,12 @@ pub extern "C" fn gm_stats(arg: *const c_char) -> *mut c_char {
|
||||
let by_pred: Vec<(String, i64)> = match a.get("by_pred") {
|
||||
Some(Value::Array(rows)) => rows
|
||||
.iter()
|
||||
.map(|r| (py_str(r.get("pred")), r.get("n").and_then(|v| v.as_i64()).unwrap_or(0)))
|
||||
.map(|r| {
|
||||
(
|
||||
py_str(r.get("pred")),
|
||||
r.get("n").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
@@ -200,7 +200,10 @@ mod phaseb_units {
|
||||
fn contradiction_wins_from_every_state() {
|
||||
for s in ["candidate", "supported", "trial", "promoted", "zzz"] {
|
||||
let d = json!({"state": s});
|
||||
let out = advance(&d, &json!({"contradictions": 1, "sightings": 9, "trial_outcomes": 9}));
|
||||
let out = advance(
|
||||
&d,
|
||||
&json!({"contradictions": 1, "sightings": 9, "trial_outcomes": 9}),
|
||||
);
|
||||
assert_eq!(out["state"], "rejected", "state {s}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ pub fn build_preview(
|
||||
|
||||
let env_runtime_implications: Vec<serde_json::Value> = {
|
||||
let target = closure.get("target").and_then(|t| t.as_str()).unwrap_or("");
|
||||
model.get(target)
|
||||
model
|
||||
.get(target)
|
||||
.and_then(|e| e.get("deployment_surfaces"))
|
||||
.and_then(|d| d.as_array())
|
||||
.cloned()
|
||||
@@ -131,7 +132,6 @@ pub fn validate_preview(preview: &serde_json::Value) -> Vec<String> {
|
||||
errs
|
||||
}
|
||||
|
||||
|
||||
// ── FFI surface (facade burn-down, 2026-10-10) ───────────────────────────────
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
@@ -151,17 +151,30 @@ fn read_arg(ptr: *const c_char) -> Result<String, String> {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ip_preview(arg: *const c_char) -> *mut c_char {
|
||||
let result = read_arg(arg)
|
||||
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).map_err(|e| format!("argument JSON parse failed: {}", e)))
|
||||
.and_then(|t| {
|
||||
serde_json::from_str::<serde_json::Value>(&t)
|
||||
.map_err(|e| format!("argument JSON parse failed: {}", e))
|
||||
})
|
||||
.and_then(|a| {
|
||||
let model: std::collections::HashMap<String, HashMap<String, serde_json::Value>> =
|
||||
serde_json::from_value(a.get("model").cloned().unwrap_or(serde_json::json!({})))
|
||||
.map_err(|e| format!("arg.model parse: {}", e))?;
|
||||
let closure = a.get("closure").cloned().ok_or("arg.closure required")?;
|
||||
let basis = a.get("basis").cloned();
|
||||
let scope = a.get("contract_scope").and_then(|c| c.as_array()).map(|arr| {
|
||||
arr.iter().filter_map(|v| v.as_str().map(String::from)).collect::<Vec<String>>()
|
||||
});
|
||||
Ok(build_preview(&model, &closure, basis.as_ref(), scope.as_deref()))
|
||||
let scope = a
|
||||
.get("contract_scope")
|
||||
.and_then(|c| c.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
Ok(build_preview(
|
||||
&model,
|
||||
&closure,
|
||||
basis.as_ref(),
|
||||
scope.as_deref(),
|
||||
))
|
||||
});
|
||||
let body = match result {
|
||||
Ok(v) => serde_json::json!({"ok": true, "value": v}),
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
//! so key order is insensitive but every value must match exactly.
|
||||
|
||||
use crate::{
|
||||
_now, context_packet, descriptive_impact, invalidate_behaviours,
|
||||
post_change_obligations, predictive_impact, record_delta_line,
|
||||
_now, context_packet, descriptive_impact, invalidate_behaviours, post_change_obligations,
|
||||
predictive_impact, record_delta_line,
|
||||
};
|
||||
use evidence_provenance::pyjson::{self, Json};
|
||||
|
||||
@@ -110,7 +110,9 @@ fn replay_context_packet() {
|
||||
atv(&c, &["args", "ledger"]).expect("ledger"),
|
||||
&jstr(atv(&c, &["args", "target"])),
|
||||
&jstr(atv(&c, &["args", "intent"])),
|
||||
atv(&c, &["args", "basis_summary"]).cloned().unwrap_or(Json::Null),
|
||||
atv(&c, &["args", "basis_summary"])
|
||||
.cloned()
|
||||
.unwrap_or(Json::Null),
|
||||
jnum(atv(&c, &["args", "max_local_files"]), 12.0) as usize,
|
||||
&jstr(atv(&c, &["args", "now"])),
|
||||
);
|
||||
|
||||
+102
-85
@@ -149,9 +149,7 @@ fn children(doc: &Json, eid: &str) -> Vec<String> {
|
||||
relationships(doc)
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
RelType::of(r) == Some(RelType::BelongsTo)
|
||||
&& s(r, "to") == eid
|
||||
&& s(r, "from") != eid
|
||||
RelType::of(r) == Some(RelType::BelongsTo) && s(r, "to") == eid && s(r, "from") != eid
|
||||
})
|
||||
.map(|r| s(r, "from"))
|
||||
.collect()
|
||||
@@ -184,10 +182,7 @@ fn owning_domain(doc: &Json, eid: &str) -> Json {
|
||||
}
|
||||
let parents: Vec<String> = relationships(doc)
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
RelType::of(r) == Some(RelType::BelongsTo)
|
||||
&& s(r, "from") == cur
|
||||
})
|
||||
.filter(|r| RelType::of(r) == Some(RelType::BelongsTo) && s(r, "from") == cur)
|
||||
.map(|r| s(r, "to"))
|
||||
.collect();
|
||||
match parents.into_iter().next() {
|
||||
@@ -202,11 +197,7 @@ fn owning_domain(doc: &Json, eid: &str) -> Json {
|
||||
fn dependencies(doc: &Json, eid: &str) -> Vec<String> {
|
||||
let mut v: Vec<String> = relationships(doc)
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
RelType::of(r) == Some(RelType::DependsOn)
|
||||
&& s(r, "from") == eid
|
||||
&& !stale(r)
|
||||
})
|
||||
.filter(|r| RelType::of(r) == Some(RelType::DependsOn) && s(r, "from") == eid && !stale(r))
|
||||
.map(|r| s(r, "to"))
|
||||
.collect();
|
||||
v.sort();
|
||||
@@ -312,10 +303,7 @@ fn impact_closure(doc: &Json, eid: &str) -> Json {
|
||||
"observation_scope",
|
||||
obj(vec![
|
||||
("direct_dependencies", arr(direct_deps)),
|
||||
(
|
||||
"interfaces_provided",
|
||||
arr(interfaces_for(doc, eid)),
|
||||
),
|
||||
("interfaces_provided", arr(interfaces_for(doc, eid))),
|
||||
("interfaces_consumed", arr(consumes_of(doc, eid))),
|
||||
]),
|
||||
),
|
||||
@@ -334,9 +322,7 @@ fn impact_closure(doc: &Json, eid: &str) -> Json {
|
||||
(
|
||||
"invariants",
|
||||
arr(vec![
|
||||
Json::Str(format!(
|
||||
"belongs_to hierarchy of {eid} remains stable"
|
||||
)),
|
||||
Json::Str(format!("belongs_to hierarchy of {eid} remains stable")),
|
||||
Json::Str("canonical-state ownership unchanged".into()),
|
||||
]),
|
||||
),
|
||||
@@ -387,10 +373,21 @@ fn ledger_context_packet(ledger: &Json, capability_id: &str) -> Json {
|
||||
}
|
||||
};
|
||||
let fields = [
|
||||
"capability_id", "name", "purpose", "component",
|
||||
"source_locations", "contract", "state_touched", "dependencies",
|
||||
"consumers", "invariants", "behaviour_specs", "maturity",
|
||||
"constraints", "generation", "limitations",
|
||||
"capability_id",
|
||||
"name",
|
||||
"purpose",
|
||||
"component",
|
||||
"source_locations",
|
||||
"contract",
|
||||
"state_touched",
|
||||
"dependencies",
|
||||
"consumers",
|
||||
"invariants",
|
||||
"behaviour_specs",
|
||||
"maturity",
|
||||
"constraints",
|
||||
"generation",
|
||||
"limitations",
|
||||
];
|
||||
let mut pairs: Vec<(String, Json)> =
|
||||
vec![("schema_version".into(), Json::Num(LEDGER_SCHEMA_VERSION))];
|
||||
@@ -443,23 +440,15 @@ pub fn context_packet(
|
||||
})
|
||||
.count();
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
for eid in std::iter::once(target.to_string())
|
||||
.chain(children(model, target))
|
||||
{
|
||||
for eid in std::iter::once(target.to_string()).chain(children(model, target)) {
|
||||
let e = get_entity(model, &eid);
|
||||
let mut src = String::new();
|
||||
if let Some(e) = e {
|
||||
if let Some(Json::Str(x)) = e
|
||||
.get("ext")
|
||||
.and_then(|x| x.get("source"))
|
||||
{
|
||||
if let Some(Json::Str(x)) = e.get("ext").and_then(|x| x.get("source")) {
|
||||
src = x.clone();
|
||||
}
|
||||
if src.is_empty() {
|
||||
if let Some(Json::Str(x)) = e
|
||||
.get("identities")
|
||||
.and_then(|x| x.get("source"))
|
||||
{
|
||||
if let Some(Json::Str(x)) = e.get("identities").and_then(|x| x.get("source")) {
|
||||
src = x.clone();
|
||||
}
|
||||
}
|
||||
@@ -479,8 +468,7 @@ pub fn context_packet(
|
||||
}
|
||||
let mut file_set: BTreeSet<String> = BTreeSet::new();
|
||||
file_set.extend(files);
|
||||
let local_files: Vec<String> =
|
||||
file_set.into_iter().take(max_local_files).collect();
|
||||
let local_files: Vec<String> = file_set.into_iter().take(max_local_files).collect();
|
||||
let closure = impact_closure(model, target);
|
||||
let mut iface_consumers: BTreeSet<String> = BTreeSet::new();
|
||||
for iface in interfaces_for(model, target) {
|
||||
@@ -493,7 +481,10 @@ pub fn context_packet(
|
||||
let mut connected_consumers: BTreeSet<String> = BTreeSet::new();
|
||||
connected_consumers.extend(consumers(model, target));
|
||||
connected_consumers.extend(iface_consumers.iter().cloned());
|
||||
let uncertainty = closure.get("uncertainty").cloned().unwrap_or(Json::Arr(vec![]));
|
||||
let uncertainty = closure
|
||||
.get("uncertainty")
|
||||
.cloned()
|
||||
.unwrap_or(Json::Arr(vec![]));
|
||||
let connected = obj(vec![
|
||||
("dependencies", arr(dependencies(model, target))),
|
||||
(
|
||||
@@ -510,7 +501,10 @@ pub fn context_packet(
|
||||
"verification",
|
||||
arr(edges_from(model, target, RelType::VerifiedBy)),
|
||||
),
|
||||
("deployment", arr(edges_from(model, target, RelType::DeployedBy))),
|
||||
(
|
||||
"deployment",
|
||||
arr(edges_from(model, target, RelType::DeployedBy)),
|
||||
),
|
||||
("uncertainty", uncertainty),
|
||||
]);
|
||||
let target_kind = match entity.get("kind") {
|
||||
@@ -557,16 +551,13 @@ pub fn descriptive_impact(
|
||||
_ => HashSet::new(),
|
||||
};
|
||||
let observed: HashSet<String> = observed_changed.iter().cloned().collect();
|
||||
let mut predicted_sorted: Vec<String> =
|
||||
predicted_scope.iter().cloned().collect();
|
||||
let mut predicted_sorted: Vec<String> = predicted_scope.iter().cloned().collect();
|
||||
predicted_sorted.sort();
|
||||
let mut observed_sorted: Vec<String> = observed.iter().cloned().collect();
|
||||
observed_sorted.sort();
|
||||
let mut unexpected: Vec<String> =
|
||||
observed.difference(&predicted_scope).cloned().collect();
|
||||
let mut unexpected: Vec<String> = observed.difference(&predicted_scope).cloned().collect();
|
||||
unexpected.sort();
|
||||
let mut unaffected: Vec<String> =
|
||||
predicted_scope.difference(&observed).cloned().collect();
|
||||
let mut unaffected: Vec<String> = predicted_scope.difference(&observed).cloned().collect();
|
||||
unaffected.sort();
|
||||
obj(vec![
|
||||
("target", Json::Str(target.to_string())),
|
||||
@@ -635,8 +626,11 @@ pub fn post_change_obligations(
|
||||
};
|
||||
let mut obligations: Vec<Json> = Vec::new();
|
||||
let mut seen: HashSet<(String, String)> = HashSet::new();
|
||||
let add = |kind: &str, target_e: &str, why: String,
|
||||
obligations: &mut Vec<Json>, seen: &mut HashSet<(String, String)>| {
|
||||
let add = |kind: &str,
|
||||
target_e: &str,
|
||||
why: String,
|
||||
obligations: &mut Vec<Json>,
|
||||
seen: &mut HashSet<(String, String)>| {
|
||||
if seen.insert((kind.to_string(), target_e.to_string())) {
|
||||
obligations.push(obj(vec![
|
||||
("kind", Json::Str(kind.to_string())),
|
||||
@@ -745,10 +739,7 @@ pub fn post_change_obligations(
|
||||
|
||||
pub(crate) fn envelope(result: Result<Json, (String, String)>) -> *mut c_char {
|
||||
let body = match result {
|
||||
Ok(value) => obj(vec![
|
||||
("ok", Json::Bool(true)),
|
||||
("value", value),
|
||||
]),
|
||||
Ok(value) => obj(vec![("ok", Json::Bool(true)), ("value", value)]),
|
||||
Err((err, ty)) => obj(vec![
|
||||
("ok", Json::Bool(false)),
|
||||
("err", Json::Str(err)),
|
||||
@@ -805,32 +796,43 @@ fn str_list(v: Option<&Json>) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
ffi!(itr_context_packet, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let model = doc.get("model").cloned().unwrap_or(Json::Null);
|
||||
let ledger = doc.get("ledger").cloned().unwrap_or(Json::Null);
|
||||
let target = s(&doc, "target");
|
||||
let intent = match doc.get("intent") {
|
||||
Some(Json::Str(x)) => x.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let basis = doc.get("basis_summary").cloned().unwrap_or(Json::Null);
|
||||
let max_local = match doc.get("max_local_files") {
|
||||
Some(Json::Num(n)) => *n as usize,
|
||||
_ => 12,
|
||||
};
|
||||
let now = s(&doc, "now");
|
||||
Ok(context_packet(&model, &ledger, &target, &intent, basis, max_local, &now))
|
||||
});
|
||||
ffi!(
|
||||
itr_context_packet,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let model = doc.get("model").cloned().unwrap_or(Json::Null);
|
||||
let ledger = doc.get("ledger").cloned().unwrap_or(Json::Null);
|
||||
let target = s(&doc, "target");
|
||||
let intent = match doc.get("intent") {
|
||||
Some(Json::Str(x)) => x.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let basis = doc.get("basis_summary").cloned().unwrap_or(Json::Null);
|
||||
let max_local = match doc.get("max_local_files") {
|
||||
Some(Json::Num(n)) => *n as usize,
|
||||
_ => 12,
|
||||
};
|
||||
let now = s(&doc, "now");
|
||||
Ok(context_packet(
|
||||
&model, &ledger, &target, &intent, basis, max_local, &now,
|
||||
))
|
||||
}
|
||||
);
|
||||
|
||||
ffi!(itr_predictive_impact, |raw: &str| -> Result<Json, (String, String)> {
|
||||
ffi!(itr_predictive_impact, |raw: &str| -> Result<
|
||||
Json,
|
||||
(String, String),
|
||||
> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let model = doc.get("model").cloned().unwrap_or(Json::Null);
|
||||
let target = s(&doc, "target");
|
||||
Ok(predictive_impact(&model, &target))
|
||||
});
|
||||
|
||||
ffi!(itr_descriptive_impact, |raw: &str| -> Result<Json, (String, String)> {
|
||||
ffi!(itr_descriptive_impact, |raw: &str| -> Result<
|
||||
Json,
|
||||
(String, String),
|
||||
> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let model = doc.get("model").cloned().unwrap_or(Json::Null);
|
||||
let target = s(&doc, "target");
|
||||
@@ -840,31 +842,46 @@ ffi!(itr_descriptive_impact, |raw: &str| -> Result<Json, (String, String)> {
|
||||
_ => String::new(),
|
||||
};
|
||||
let now = s(&doc, "now");
|
||||
Ok(descriptive_impact(&model, &target, &observed, &generation, &now))
|
||||
Ok(descriptive_impact(
|
||||
&model,
|
||||
&target,
|
||||
&observed,
|
||||
&generation,
|
||||
&now,
|
||||
))
|
||||
});
|
||||
|
||||
ffi!(itr_record_delta_line, |raw: &str| -> Result<Json, (String, String)> {
|
||||
ffi!(itr_record_delta_line, |raw: &str| -> Result<
|
||||
Json,
|
||||
(String, String),
|
||||
> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let delta = doc.get("delta").cloned().unwrap_or(Json::Null);
|
||||
Ok(Json::Str(record_delta_line(&delta)))
|
||||
});
|
||||
|
||||
ffi!(itr_invalidate, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let mut bh = doc.get("behaviours").cloned().unwrap_or(Json::Null);
|
||||
let changed = str_list(doc.get("changed"));
|
||||
let generation = match doc.get("generation") {
|
||||
Some(Json::Str(x)) => x.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let now = s(&doc, "now");
|
||||
match invalidate_behaviours(&mut bh, &changed, &generation, &now) {
|
||||
Ok(recs) => Ok(obj(vec![("doc", bh), ("recs", recs)])),
|
||||
Err(e) => Err((e, "RuntimeError".to_string())),
|
||||
ffi!(
|
||||
itr_invalidate,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let mut bh = doc.get("behaviours").cloned().unwrap_or(Json::Null);
|
||||
let changed = str_list(doc.get("changed"));
|
||||
let generation = match doc.get("generation") {
|
||||
Some(Json::Str(x)) => x.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
let now = s(&doc, "now");
|
||||
match invalidate_behaviours(&mut bh, &changed, &generation, &now) {
|
||||
Ok(recs) => Ok(obj(vec![("doc", bh), ("recs", recs)])),
|
||||
Err(e) => Err((e, "RuntimeError".to_string())),
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
ffi!(itr_post_change_obligations, |raw: &str| -> Result<Json, (String, String)> {
|
||||
ffi!(itr_post_change_obligations, |raw: &str| -> Result<
|
||||
Json,
|
||||
(String, String),
|
||||
> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let model = doc.get("model").cloned().unwrap_or(Json::Null);
|
||||
let target = s(&doc, "target");
|
||||
|
||||
@@ -264,17 +264,35 @@ fn contracts_fixture() -> Vec<serde_json::Value> {
|
||||
fn check_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
let contracts = contracts_fixture();
|
||||
for (idx, case) in truth["check"].as_array().expect("check array").iter().enumerate() {
|
||||
for (idx, case) in truth["check"]
|
||||
.as_array()
|
||||
.expect("check array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let got = check_layout(&contracts, &case["observed"]);
|
||||
assert_eq!(got["verdict"], case["out"]["verdict"], "check case {} verdict", idx);
|
||||
assert_eq!(got["checks"], case["out"]["checks"], "check case {} checks", idx);
|
||||
assert_eq!(
|
||||
got["verdict"], case["out"]["verdict"],
|
||||
"check case {} verdict",
|
||||
idx
|
||||
);
|
||||
assert_eq!(
|
||||
got["checks"], case["out"]["checks"],
|
||||
"check case {} checks",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drift_matches_oracle() {
|
||||
let truth: serde_json::Value = serde_json::from_str(TRUTH).expect("truth parses");
|
||||
for (idx, case) in truth["drift"].as_array().expect("drift array").iter().enumerate() {
|
||||
for (idx, case) in truth["drift"]
|
||||
.as_array()
|
||||
.expect("drift array")
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
// rebuild contracts with the recorded post-status (the DRIFTING case needs it)
|
||||
let contracts = contracts_fixture();
|
||||
if let Some(arr) = case.get("post_status").and_then(|v| v.as_array()) {
|
||||
@@ -285,6 +303,11 @@ fn drift_matches_oracle() {
|
||||
let got_checks = check_layout(&contracts, &case["observed"]);
|
||||
let checks = got_checks["checks"].as_array().cloned().unwrap_or_default();
|
||||
let got = drift_decisions(&contracts, &checks);
|
||||
assert_eq!(got["drifted_locked"], serde_json::json!(case["out"]["drifted_locked"]), "drift case {} staled", idx);
|
||||
assert_eq!(
|
||||
got["drifted_locked"],
|
||||
serde_json::json!(case["out"]["drifted_locked"]),
|
||||
"drift case {} staled",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,8 +106,17 @@ fn region(rid: &str, evidence: &[&str]) -> Value {
|
||||
fn branch_evidence_union_is_sorted_dedup() {
|
||||
let mut doc = doc_with(vec![region("sidebar", &["b", "a"])]);
|
||||
let e = kernels::define_region(
|
||||
&mut doc, "sidebar", "", None, "", "", "", &Default::default(),
|
||||
"scaffold", &[json!("a"), json!("c")], kernels::FROZEN_NOW,
|
||||
&mut doc,
|
||||
"sidebar",
|
||||
"",
|
||||
None,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
&Default::default(),
|
||||
"scaffold",
|
||||
&[json!("a"), json!("c")],
|
||||
kernels::FROZEN_NOW,
|
||||
)
|
||||
.expect("re-define ok");
|
||||
assert_eq!(
|
||||
@@ -123,11 +132,24 @@ fn branch_maturity_high_water_never_downgrades() {
|
||||
mature["maturity"] = json!("WIRED");
|
||||
let mut doc = doc_with(vec![mature]);
|
||||
let e = kernels::define_region(
|
||||
&mut doc, "sidebar", "", None, "", "", "", &Default::default(),
|
||||
"scaffold", &[], kernels::FROZEN_NOW,
|
||||
&mut doc,
|
||||
"sidebar",
|
||||
"",
|
||||
None,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
&Default::default(),
|
||||
"scaffold",
|
||||
&[],
|
||||
kernels::FROZEN_NOW,
|
||||
)
|
||||
.expect("re-define ok");
|
||||
assert_eq!(e["maturity"], json!("WIRED"), "DECLARED never downgrades WIRED");
|
||||
assert_eq!(
|
||||
e["maturity"],
|
||||
json!("WIRED"),
|
||||
"DECLARED never downgrades WIRED"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -138,8 +160,20 @@ fn branch_region_resolution_follows_aliases() {
|
||||
r["aliases"] = json!(["region:sidebar-old"]);
|
||||
let mut doc = doc_with(vec![r]);
|
||||
let e = kernels::bind_component(
|
||||
&mut doc, "version-info", "sidebar-old", false, "scaffold", None, "",
|
||||
"", "", "", "", "", &Default::default(), kernels::FROZEN_NOW,
|
||||
&mut doc,
|
||||
"version-info",
|
||||
"sidebar-old",
|
||||
false,
|
||||
"scaffold",
|
||||
None,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
&Default::default(),
|
||||
kernels::FROZEN_NOW,
|
||||
)
|
||||
.expect("alias-following resolution binds");
|
||||
assert_eq!(e["identities"]["region"], json!("sidebar-old"));
|
||||
@@ -163,7 +197,14 @@ fn branch_reason_truncation_is_200_codepoints() {
|
||||
]);
|
||||
let reason = "é".repeat(250); // multibyte — truncation must count codepoints
|
||||
let e = kernels::amend_layout_contract(
|
||||
&mut doc, "x", "", Some(9), false, "builder", &reason, "",
|
||||
&mut doc,
|
||||
"x",
|
||||
"",
|
||||
Some(9),
|
||||
false,
|
||||
"builder",
|
||||
&reason,
|
||||
"",
|
||||
kernels::FROZEN_NOW,
|
||||
)
|
||||
.expect("amend ok");
|
||||
@@ -196,11 +237,27 @@ fn branch_rebind_resets_amendments_ledger() {
|
||||
}),
|
||||
]);
|
||||
let e = kernels::bind_component(
|
||||
&mut doc, "x", "sidebar", false, "scaffold", None, "", "", "", "",
|
||||
"", "", &Default::default(), kernels::FROZEN_NOW,
|
||||
&mut doc,
|
||||
"x",
|
||||
"sidebar",
|
||||
false,
|
||||
"scaffold",
|
||||
None,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
&Default::default(),
|
||||
kernels::FROZEN_NOW,
|
||||
)
|
||||
.expect("re-bind ok");
|
||||
assert_eq!(e["ext"]["amendments"], json!([]), "re-bind clears the ledger");
|
||||
assert_eq!(
|
||||
e["ext"]["amendments"],
|
||||
json!([]),
|
||||
"re-bind clears the ledger"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -223,14 +280,22 @@ fn branch_amend_unlock_rejection_leaves_prior_mutations() {
|
||||
}),
|
||||
]);
|
||||
let err = kernels::amend_layout_contract(
|
||||
&mut doc, "x", "other", None, true, "builder", "r", "",
|
||||
&mut doc,
|
||||
"x",
|
||||
"other",
|
||||
None,
|
||||
true,
|
||||
"builder",
|
||||
"r",
|
||||
"",
|
||||
kernels::FROZEN_NOW,
|
||||
)
|
||||
.expect_err("unlock without user must fail");
|
||||
assert_eq!(err, "unlock requires user authorization");
|
||||
let stored = &doc["entities"][2];
|
||||
assert_eq!(
|
||||
stored["identities"]["region"], json!("other"),
|
||||
stored["identities"]["region"],
|
||||
json!("other"),
|
||||
"region mutation precedes the unlock rejection (oracle order)"
|
||||
);
|
||||
}
|
||||
@@ -19,7 +19,12 @@ use serde_json::{json, Map, Value};
|
||||
pub const FROZEN_NOW: &str = "2026-10-14T00:00:00Z";
|
||||
|
||||
const MATURITY_ORDER: [&str; 6] = [
|
||||
"DECLARED", "PRESENT", "WIRED", "REACHABLE", "EXERCISED", "LIVE_PROVEN",
|
||||
"DECLARED",
|
||||
"PRESENT",
|
||||
"WIRED",
|
||||
"REACHABLE",
|
||||
"EXERCISED",
|
||||
"LIVE_PROVEN",
|
||||
];
|
||||
|
||||
fn maturity_rank(state: &str) -> i64 {
|
||||
@@ -96,10 +101,7 @@ fn add_entity(
|
||||
.get_mut("entities")
|
||||
.and_then(|v| v.as_array_mut())
|
||||
.ok_or_else(|| "KeyError: 'entities'".to_string())?;
|
||||
if let Some(idx) = ents
|
||||
.iter()
|
||||
.position(|e| as_str(e.get("id")) == id)
|
||||
{
|
||||
if let Some(idx) = ents.iter().position(|e| as_str(e.get("id")) == id) {
|
||||
let e = &mut ents[idx];
|
||||
let current = as_str(e.get("maturity"));
|
||||
if !maturity.is_empty() && maturity_rank(maturity) > maturity_rank(current) {
|
||||
@@ -219,9 +221,7 @@ pub fn bind_component(
|
||||
format!("ui:{component}")
|
||||
};
|
||||
if entity_index(doc, &format!("region:{region_id}")).is_none() {
|
||||
return Err(format!(
|
||||
"unknown region: {region_id} (define_region first)"
|
||||
));
|
||||
return Err(format!("unknown region: {region_id} (define_region first)"));
|
||||
}
|
||||
if lock && provenance != "user" {
|
||||
return Err("lock=True requires user provenance — a scaffold/builder/model can never self-issue a lock".into());
|
||||
@@ -235,7 +235,10 @@ pub fn bind_component(
|
||||
identities.insert("sizing".into(), json!(sizing));
|
||||
identities.insert("visibility".into(), json!(visibility));
|
||||
identities.insert("implementation".into(), json!(implementation));
|
||||
identities.insert("framework_attrs".into(), Value::Object(framework_attrs.clone()));
|
||||
identities.insert(
|
||||
"framework_attrs".into(),
|
||||
Value::Object(framework_attrs.clone()),
|
||||
);
|
||||
if let Some(o) = order {
|
||||
identities.insert("order".into(), json!(o));
|
||||
}
|
||||
@@ -310,9 +313,7 @@ pub fn amend_layout_contract(
|
||||
}
|
||||
}
|
||||
}
|
||||
if !new_region.is_empty()
|
||||
&& entity_index(doc, &format!("region:{new_region}")).is_none()
|
||||
{
|
||||
if !new_region.is_empty() && entity_index(doc, &format!("region:{new_region}")).is_none() {
|
||||
return Err(format!("unknown region: {new_region}"));
|
||||
}
|
||||
let mut changes: Map<String, Value> = Map::new();
|
||||
@@ -403,12 +404,7 @@ pub fn dispatch(case: &Value) -> Value {
|
||||
.to_string()
|
||||
};
|
||||
let korder = |k: &str| kwargs.get(k).and_then(|v| v.as_i64());
|
||||
let kbool = |k: &str| {
|
||||
kwargs
|
||||
.get(k)
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
let kbool = |k: &str| kwargs.get(k).and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let kmap = |k: &str| {
|
||||
kwargs
|
||||
.get(k)
|
||||
@@ -435,8 +431,14 @@ pub fn dispatch(case: &Value) -> Value {
|
||||
.filter(|(k, _)| {
|
||||
!matches!(
|
||||
k.as_str(),
|
||||
"lock" | "provenance" | "order" | "parent_of" | "anchor"
|
||||
| "sizing" | "visibility" | "evidence_ref"
|
||||
"lock"
|
||||
| "provenance"
|
||||
| "order"
|
||||
| "parent_of"
|
||||
| "anchor"
|
||||
| "sizing"
|
||||
| "visibility"
|
||||
| "evidence_ref"
|
||||
| "implementation"
|
||||
)
|
||||
})
|
||||
|
||||
@@ -49,8 +49,14 @@ pub fn check_layout(contracts: &[Value], observed: &Value) -> Value {
|
||||
Some(m) => m,
|
||||
None => continue,
|
||||
};
|
||||
let cid = ids.get("component").map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let required_region = ids.get("region").map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let cid = ids
|
||||
.get("component")
|
||||
.map(|v| s(Some(v)).to_string())
|
||||
.unwrap_or_default();
|
||||
let required_region = ids
|
||||
.get("region")
|
||||
.map(|v| s(Some(v)).to_string())
|
||||
.unwrap_or_default();
|
||||
let locked = bool_of(ids.get("lock"));
|
||||
let required_order = ids.get("order").and_then(|v| v.as_i64());
|
||||
|
||||
@@ -73,7 +79,10 @@ pub fn check_layout(contracts: &[Value], observed: &Value) -> Value {
|
||||
let mut problems: Vec<String> = vec![];
|
||||
let got_region = s(o.get("region"));
|
||||
if got_region != required_region {
|
||||
problems.push(format!("region '{}' != required '{}'", got_region, required_region));
|
||||
problems.push(format!(
|
||||
"region '{}' != required '{}'",
|
||||
got_region, required_region
|
||||
));
|
||||
}
|
||||
if let Some(req_order) = required_order {
|
||||
if let Some(got_order) = o.get("order").and_then(|v| v.as_i64()) {
|
||||
@@ -115,7 +124,9 @@ pub fn drift_decisions(contracts: &[Value], checks: &[Value]) -> Value {
|
||||
let comp = s(d.get("component")).to_string();
|
||||
let is_locked = bool_of(d.get("locked"));
|
||||
let has_entity = contracts.iter().any(|e| {
|
||||
idents(e).map(|m| m.get("component").map(|v| s(Some(v))) == Some(&comp)).unwrap_or(false)
|
||||
idents(e)
|
||||
.map(|m| m.get("component").map(|v| s(Some(v))) == Some(&comp))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
if !has_entity {
|
||||
continue;
|
||||
@@ -129,7 +140,10 @@ pub fn drift_decisions(contracts: &[Value], checks: &[Value]) -> Value {
|
||||
if s(e.get("kind")) != "layout_contract" || s(e.get("status")) != "DRIFTING" {
|
||||
continue;
|
||||
}
|
||||
let comp = idents(e).and_then(|m| m.get("component")).map(|v| s(Some(v)).to_string()).unwrap_or_default();
|
||||
let comp = idents(e)
|
||||
.and_then(|m| m.get("component"))
|
||||
.map(|v| s(Some(v)).to_string())
|
||||
.unwrap_or_default();
|
||||
let mine = checks.iter().find(|c| s(c.get("component")) == comp);
|
||||
if let Some(m) = mine {
|
||||
if s(m.get("verdict")) == "PASS" {
|
||||
@@ -144,7 +158,9 @@ fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
let x = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_str()
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -170,7 +186,10 @@ fn arr(v: Option<&Value>) -> Vec<Value> {
|
||||
pub extern "C" fn lc_check(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
let contracts = arr(a.get("contracts"));
|
||||
let observed = a.get("observed").cloned().unwrap_or(Value::Object(Default::default()));
|
||||
let observed = a
|
||||
.get("observed")
|
||||
.cloned()
|
||||
.unwrap_or(Value::Object(Default::default()));
|
||||
check_layout(&contracts, &observed)
|
||||
});
|
||||
envelope(r)
|
||||
@@ -192,10 +211,7 @@ pub extern "C" fn lc_drift(arg: *const c_char) -> *mut c_char {
|
||||
// {"doc": updated_doc, "entity"|"observations": ...}.
|
||||
|
||||
fn a_str(a: &Value, k: &str, d: &str) -> String {
|
||||
a.get(k)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(d)
|
||||
.to_string()
|
||||
a.get(k).and_then(|v| v.as_str()).unwrap_or(d).to_string()
|
||||
}
|
||||
|
||||
fn a_order(a: &Value, k: &str) -> Option<i64> {
|
||||
@@ -225,7 +241,11 @@ fn a_refs(a: &Value, k: &str) -> Vec<Value> {
|
||||
pub extern "C" fn lc_define_region(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let mut doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let region_id = a.get("region_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let region_id = a
|
||||
.get("region_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let now = a_str(&a, "now", kernels::FROZEN_NOW);
|
||||
let e = kernels::define_region(
|
||||
&mut doc,
|
||||
@@ -250,8 +270,16 @@ pub extern "C" fn lc_define_region(arg: *const c_char) -> *mut c_char {
|
||||
pub extern "C" fn lc_bind_component(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let mut doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let component = a.get("component").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let region_id = a.get("region_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let component = a
|
||||
.get("component")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let region_id = a
|
||||
.get("region_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let now = a_str(&a, "now", kernels::FROZEN_NOW);
|
||||
let e = kernels::bind_component(
|
||||
&mut doc,
|
||||
@@ -279,7 +307,11 @@ pub extern "C" fn lc_bind_component(arg: *const c_char) -> *mut c_char {
|
||||
pub extern "C" fn lc_amend_layout_contract(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let mut doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let component = a.get("component").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let component = a
|
||||
.get("component")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let now = a_str(&a, "now", kernels::FROZEN_NOW);
|
||||
let e = kernels::amend_layout_contract(
|
||||
&mut doc,
|
||||
@@ -302,14 +334,14 @@ pub extern "C" fn lc_amend_layout_contract(arg: *const c_char) -> *mut c_char {
|
||||
pub extern "C" fn lc_record_observation(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let mut doc = a.get("doc").cloned().ok_or("arg.doc required")?;
|
||||
let adapter = a.get("adapter").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let adapter = a
|
||||
.get("adapter")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let now = a_str(&a, "now", kernels::FROZEN_NOW);
|
||||
let out = kernels::record_layout_observation(
|
||||
&mut doc,
|
||||
&adapter,
|
||||
&a_map(&a, "observed"),
|
||||
&now,
|
||||
)?;
|
||||
let out =
|
||||
kernels::record_layout_observation(&mut doc, &adapter, &a_map(&a, "observed"), &now)?;
|
||||
Ok(json!({"doc": doc, "observations": out}))
|
||||
});
|
||||
envelope(r)
|
||||
|
||||
@@ -45,9 +45,10 @@ fn replay(group: &str, f: impl Fn(&Value) -> Value) {
|
||||
|
||||
#[test]
|
||||
fn content_words_parity_battery() {
|
||||
replay("content_words", |i| {
|
||||
json!({"words": content_words(i["text"].as_str())})
|
||||
});
|
||||
replay(
|
||||
"content_words",
|
||||
|i| json!({"words": content_words(i["text"].as_str())}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -56,7 +57,11 @@ fn distinctive_terms_parity_battery() {
|
||||
let fact = i["fact"].as_str().unwrap_or("");
|
||||
let corpus: Vec<String> = i["corpus_texts"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let terms: Vec<String> = distinctive_terms(fact, &corpus).into_iter().collect();
|
||||
json!({"terms": terms})
|
||||
@@ -69,13 +74,18 @@ fn shared_distinctive_parity_battery() {
|
||||
let fact = i["fact"].as_str().unwrap_or("");
|
||||
let strs = |k: &str| -> Vec<String> {
|
||||
i[k].as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let existing = strs("existing_values");
|
||||
let corpus = strs("corpus_texts");
|
||||
let shared: Vec<String> =
|
||||
shared_distinctive(fact, &existing, &corpus).into_iter().collect();
|
||||
let shared: Vec<String> = shared_distinctive(fact, &existing, &corpus)
|
||||
.into_iter()
|
||||
.collect();
|
||||
json!({"shared": shared})
|
||||
});
|
||||
}
|
||||
@@ -85,7 +95,11 @@ fn coherence_parity_battery() {
|
||||
replay("coherence", |i| {
|
||||
let strs = |k: &str| -> Vec<String> {
|
||||
i[k].as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let verdict = coherence(
|
||||
@@ -102,7 +116,11 @@ fn grounded_parity_battery() {
|
||||
replay("grounded", |i| {
|
||||
let sources: Vec<String> = i["source_texts"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let min = i.get("min_distinct").and_then(Value::as_f64).unwrap_or(1.0);
|
||||
json!({"grounded": grounded(i["fact"].as_str().unwrap_or(""), &sources, min)})
|
||||
@@ -111,9 +129,10 @@ fn grounded_parity_battery() {
|
||||
|
||||
#[test]
|
||||
fn stem_parity_battery() {
|
||||
replay("stem", |i| {
|
||||
json!({"stem": stem(i["word"].as_str().unwrap_or(""))})
|
||||
});
|
||||
replay(
|
||||
"stem",
|
||||
|i| json!({"stem": stem(i["word"].as_str().unwrap_or(""))}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -121,11 +140,19 @@ fn promote_eligible_parity_battery() {
|
||||
replay("promote_eligible", |i| {
|
||||
let existing: Vec<String> = i["existing"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let corpus: Vec<String> = i["corpus_texts"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or("").to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let strength = i.get("evidence_strength").cloned().unwrap_or(json!(0));
|
||||
let min = i.get("min_strength").cloned().unwrap_or(json!(3));
|
||||
|
||||
@@ -82,30 +82,64 @@ use std::sync::OnceLock;
|
||||
// SAME stopword set as memory_store._STOPWORDS (single source of truth
|
||||
// would be ideal; keep in sync — see memory_store.py).
|
||||
pub const STOP: &[&str] = &[
|
||||
"the", "and", "for", "with", "that", "this", "from", "into", "when",
|
||||
"then", "were", "have", "been", "will", "was", "are", "but", "not",
|
||||
"you", "your", "also", "its", "his", "her", "him", "over", "under",
|
||||
"them", "they", "there", "about", "after", "what", "which", "who",
|
||||
"how", "why", "where", "while", "just", "more", "each", "than", "then",
|
||||
"very", "such", "some", "only", "prefers", "needs", "wants", "likes",
|
||||
"the", "and", "for", "with", "that", "this", "from", "into", "when", "then", "were", "have",
|
||||
"been", "will", "was", "are", "but", "not", "you", "your", "also", "its", "his", "her", "him",
|
||||
"over", "under", "them", "they", "there", "about", "after", "what", "which", "who", "how",
|
||||
"why", "where", "while", "just", "more", "each", "than", "then", "very", "such", "some",
|
||||
"only", "prefers", "needs", "wants", "likes",
|
||||
];
|
||||
|
||||
// Hard stop for evolution: these generic framing words never carry a
|
||||
// relation ("work", "always", "way") — a persona built on them would be a
|
||||
// coincidence.
|
||||
pub const FRAMING: &[&str] = &[
|
||||
"work", "always", "way", "thing", "value", "identity", "persona",
|
||||
"someone", "something", "never", "always", "before", "after", "then",
|
||||
"when", "while", "must", "should", "will", "would", "could", "can",
|
||||
"have", "has", "had", "been", "being", "used", "use", "uses", "make",
|
||||
"makes", "made", "keep", "keeps", "take", "takes", "took", "get",
|
||||
"gets", "got", "going",
|
||||
"work",
|
||||
"always",
|
||||
"way",
|
||||
"thing",
|
||||
"value",
|
||||
"identity",
|
||||
"persona",
|
||||
"someone",
|
||||
"something",
|
||||
"never",
|
||||
"always",
|
||||
"before",
|
||||
"after",
|
||||
"then",
|
||||
"when",
|
||||
"while",
|
||||
"must",
|
||||
"should",
|
||||
"will",
|
||||
"would",
|
||||
"could",
|
||||
"can",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"been",
|
||||
"being",
|
||||
"used",
|
||||
"use",
|
||||
"uses",
|
||||
"make",
|
||||
"makes",
|
||||
"made",
|
||||
"keep",
|
||||
"keeps",
|
||||
"take",
|
||||
"takes",
|
||||
"took",
|
||||
"get",
|
||||
"gets",
|
||||
"got",
|
||||
"going",
|
||||
];
|
||||
|
||||
pub const NEGATION: &[&str] = &[
|
||||
"not", "never", "don't", "doesn't", "cannot", "can't", "no", "nor",
|
||||
"neither", "without", "avoid", "reject", "against", "refuses",
|
||||
"refuse", "opposed",
|
||||
"not", "never", "don't", "doesn't", "cannot", "can't", "no", "nor", "neither", "without",
|
||||
"avoid", "reject", "against", "refuses", "refuse", "opposed",
|
||||
];
|
||||
|
||||
// same rare-word threshold as recall/associations
|
||||
@@ -218,16 +252,14 @@ pub fn stem(w: &str) -> String {
|
||||
/// The df table for the corpus: stem → number of documents containing it
|
||||
/// (stem-aware: migrate/migration count as one term).
|
||||
fn doc_frequencies(corpus_texts: &[String]) -> HashMap<String, u64> {
|
||||
corpus_texts
|
||||
.iter()
|
||||
.fold(HashMap::new(), |mut acc, t| {
|
||||
let low = t.to_lowercase();
|
||||
let stems: HashSet<String> = scan_words(&low).map(stem).collect();
|
||||
for s in stems {
|
||||
*acc.entry(s).or_insert(0) += 1;
|
||||
}
|
||||
acc
|
||||
})
|
||||
corpus_texts.iter().fold(HashMap::new(), |mut acc, t| {
|
||||
let low = t.to_lowercase();
|
||||
let stems: HashSet<String> = scan_words(&low).map(stem).collect();
|
||||
for s in stems {
|
||||
*acc.entry(s).or_insert(0) += 1;
|
||||
}
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
/// `distinctive_terms` — the terms in `fact` that are RARE across the
|
||||
@@ -367,7 +399,11 @@ pub fn grounded(fact: &str, source_texts: &[impl AsRef<str>], min_distinct: f64)
|
||||
if srcs.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let joined = srcs.iter().map(|s| s.to_lowercase()).collect::<Vec<_>>().join(" ");
|
||||
let joined = srcs
|
||||
.iter()
|
||||
.map(|s| s.to_lowercase())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let stems: HashSet<String> = scan_words(&joined).map(stem).collect();
|
||||
let low = fact.to_lowercase();
|
||||
let fact_terms: BTreeSet<&str> = content_word_refs(&low).collect();
|
||||
@@ -447,11 +483,8 @@ pub fn promote_eligible(
|
||||
let shared = shared_distinctive_of(proposal, &existing, &corpus);
|
||||
if !shared.is_empty() {
|
||||
let top: Vec<String> = shared.iter().take(3).map(|s| format!("'{}'", s)).collect();
|
||||
return Promotion::Eligible(format!(
|
||||
"shares distinctive term(s): [{}]",
|
||||
top.join(", ")
|
||||
))
|
||||
.into_pair();
|
||||
return Promotion::Eligible(format!("shares distinctive term(s): [{}]", top.join(", ")))
|
||||
.into_pair();
|
||||
}
|
||||
// No distinctive shared term: a NOVEL axis is eligible only if it is
|
||||
// GROUNDED in the absorbed corpus via a distinctive term (a real
|
||||
@@ -555,8 +588,9 @@ pub extern "C" fn le_shared_distinctive(arg: *const c_char) -> *mut c_char {
|
||||
let fact = as_text(a.get("fact")).unwrap_or("");
|
||||
let existing = str_list(&a, "existing_values");
|
||||
let corpus = str_list(&a, "corpus_texts");
|
||||
let shared: Vec<String> =
|
||||
shared_distinctive(fact, &existing, &corpus).into_iter().collect();
|
||||
let shared: Vec<String> = shared_distinctive(fact, &existing, &corpus)
|
||||
.into_iter()
|
||||
.collect();
|
||||
json!({"shared": shared})
|
||||
}))
|
||||
}
|
||||
@@ -602,8 +636,7 @@ pub extern "C" fn le_promote_eligible(arg: *const c_char) -> *mut c_char {
|
||||
let corpus = str_list(&a, "corpus_texts");
|
||||
let strength = a.get("evidence_strength").cloned().unwrap_or(json!(0));
|
||||
let min = a.get("min_strength").cloned().unwrap_or(json!(3));
|
||||
let (eligible, reason) =
|
||||
promote_eligible(proposal, &existing, &strength, &min, &corpus);
|
||||
let (eligible, reason) = promote_eligible(proposal, &existing, &strength, &min, &corpus);
|
||||
json!({"eligible": eligible, "reason": reason})
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@ fn at<'a>(v: &'a Json, path: &[&str]) -> Option<&'a Json> {
|
||||
let mut cur = v;
|
||||
for k in path {
|
||||
cur = match cur {
|
||||
Json::Obj(pairs) => pairs
|
||||
.iter()
|
||||
.find(|(k2, _)| k2 == k)
|
||||
.map(|(_, x)| x)?,
|
||||
Json::Obj(pairs) => pairs.iter().find(|(k2, _)| k2 == k).map(|(_, x)| x)?,
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -49,14 +49,54 @@ pub struct Trigger {
|
||||
|
||||
/// The eight trigger classes, TABLE ORDER (the fire_due listing law).
|
||||
pub const TRIGGERS: [Trigger; 8] = [
|
||||
Trigger { name: "unresolved_commitments", condition_key: "has_open_commitments", rate_limit_s: 3600.0, stop_key: "commitments_resolved" },
|
||||
Trigger { name: "approaching_deadlines", condition_key: "deadline_within_24h", rate_limit_s: 1800.0, stop_key: "deadline_passed_or_met" },
|
||||
Trigger { name: "repeated_failure", condition_key: "same_failure_3x", rate_limit_s: 900.0, stop_key: "failure_pattern_broken" },
|
||||
Trigger { name: "contradictions", condition_key: "contradiction_detected", rate_limit_s: 1800.0, stop_key: "contradiction_resolved" },
|
||||
Trigger { name: "curiosity_candidates", condition_key: "novel_material_present", rate_limit_s: 7200.0, stop_key: "material_absorbed" },
|
||||
Trigger { name: "prediction_mismatches", condition_key: "prediction_wrong", rate_limit_s: 1800.0, stop_key: "model_updated" },
|
||||
Trigger { name: "task_blockers", condition_key: "task_blocked", rate_limit_s: 600.0, stop_key: "task_unblocked" },
|
||||
Trigger { name: "owner_corrections", condition_key: "correction_received", rate_limit_s: 300.0, stop_key: "correction_absorbed" },
|
||||
Trigger {
|
||||
name: "unresolved_commitments",
|
||||
condition_key: "has_open_commitments",
|
||||
rate_limit_s: 3600.0,
|
||||
stop_key: "commitments_resolved",
|
||||
},
|
||||
Trigger {
|
||||
name: "approaching_deadlines",
|
||||
condition_key: "deadline_within_24h",
|
||||
rate_limit_s: 1800.0,
|
||||
stop_key: "deadline_passed_or_met",
|
||||
},
|
||||
Trigger {
|
||||
name: "repeated_failure",
|
||||
condition_key: "same_failure_3x",
|
||||
rate_limit_s: 900.0,
|
||||
stop_key: "failure_pattern_broken",
|
||||
},
|
||||
Trigger {
|
||||
name: "contradictions",
|
||||
condition_key: "contradiction_detected",
|
||||
rate_limit_s: 1800.0,
|
||||
stop_key: "contradiction_resolved",
|
||||
},
|
||||
Trigger {
|
||||
name: "curiosity_candidates",
|
||||
condition_key: "novel_material_present",
|
||||
rate_limit_s: 7200.0,
|
||||
stop_key: "material_absorbed",
|
||||
},
|
||||
Trigger {
|
||||
name: "prediction_mismatches",
|
||||
condition_key: "prediction_wrong",
|
||||
rate_limit_s: 1800.0,
|
||||
stop_key: "model_updated",
|
||||
},
|
||||
Trigger {
|
||||
name: "task_blockers",
|
||||
condition_key: "task_blocked",
|
||||
rate_limit_s: 600.0,
|
||||
stop_key: "task_unblocked",
|
||||
},
|
||||
Trigger {
|
||||
name: "owner_corrections",
|
||||
condition_key: "correction_received",
|
||||
rate_limit_s: 300.0,
|
||||
stop_key: "correction_absorbed",
|
||||
},
|
||||
];
|
||||
|
||||
fn find(trigger: &str) -> Option<&'static Trigger> {
|
||||
@@ -186,11 +226,9 @@ fn envelope(result: Result<Json, (String, String)>) -> *mut c_char {
|
||||
let out = pyjson::dumps(&body);
|
||||
match CString::new(out) {
|
||||
Ok(c) => c.into_raw(),
|
||||
Err(_) => CString::new(
|
||||
r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#,
|
||||
)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
Err(_) => CString::new(r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,23 +266,25 @@ macro_rules! ffi {
|
||||
($name:ident, $body:expr) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn $name(arg: *const c_char) -> *mut c_char {
|
||||
let out: Result<Json, (String, String)> =
|
||||
read_arg(arg).and_then(|raw| $body(&raw));
|
||||
let out: Result<Json, (String, String)> = read_arg(arg).and_then(|raw| $body(&raw));
|
||||
envelope(out)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ffi!(me_should_fire, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let trigger = match doc.get("trigger") {
|
||||
Some(Json::Str(t)) => t.clone(),
|
||||
_ => return Err(("trigger must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let state = state_of(&doc)?.clone();
|
||||
let now = now_of(&doc)?;
|
||||
Ok(should_fire(&trigger, &state, now))
|
||||
});
|
||||
ffi!(
|
||||
me_should_fire,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let trigger = match doc.get("trigger") {
|
||||
Some(Json::Str(t)) => t.clone(),
|
||||
_ => return Err(("trigger must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let state = state_of(&doc)?.clone();
|
||||
let now = now_of(&doc)?;
|
||||
Ok(should_fire(&trigger, &state, now))
|
||||
}
|
||||
);
|
||||
|
||||
ffi!(me_fire_due, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
@@ -253,16 +293,19 @@ ffi!(me_fire_due, |raw: &str| -> Result<Json, (String, String)> {
|
||||
Ok(fire_due(&state, now))
|
||||
});
|
||||
|
||||
ffi!(me_mark_fired, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let state = state_of(&doc)?.clone();
|
||||
let trigger = match doc.get("trigger") {
|
||||
Some(Json::Str(t)) => t.clone(),
|
||||
_ => return Err(("trigger must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let now = now_of(&doc)?;
|
||||
Ok(mark_fired(&state, &trigger, now))
|
||||
});
|
||||
ffi!(
|
||||
me_mark_fired,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let state = state_of(&doc)?.clone();
|
||||
let trigger = match doc.get("trigger") {
|
||||
Some(Json::Str(t)) => t.clone(),
|
||||
_ => return Err(("trigger must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let now = now_of(&doc)?;
|
||||
Ok(mark_fired(&state, &trigger, now))
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
@@ -83,7 +83,9 @@ fn main() {
|
||||
eprintln!("ms_ipc: ms_{op} returned null");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let s = unsafe { CStr::from_ptr(ret) }.to_string_lossy().into_owned();
|
||||
let s = unsafe { CStr::from_ptr(ret) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
memory_scoring::ms_free_string(ret);
|
||||
|
||||
// Wrap the exact FFI envelope string as a JSON string: the Go side
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! the per-source decision math at its input boundary.
|
||||
|
||||
use crate::{
|
||||
abstraction_decision, add_source, consolidated_decision, graphs_decision,
|
||||
neurons_decision, persona_wanted, select_core, store_project,
|
||||
abstraction_decision, add_source, consolidated_decision, graphs_decision, neurons_decision,
|
||||
persona_wanted, select_core, store_project,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -1802,8 +1802,7 @@ mod gap_battery {
|
||||
|
||||
#[test]
|
||||
fn add_source_rows_match_frozen_oracle() {
|
||||
let rows: Vec<Value> =
|
||||
serde_json::from_str(GAP_TRUTH).expect("gap truth parses");
|
||||
let rows: Vec<Value> = serde_json::from_str(GAP_TRUTH).expect("gap truth parses");
|
||||
let mut replayed = 0;
|
||||
for row in &rows {
|
||||
if row["fn"].as_str() != Some("ms_add_source") {
|
||||
@@ -1830,8 +1829,7 @@ mod gap_battery {
|
||||
// contract row exists and says exactly that, so a facade that
|
||||
// ignored the pin would fail the live-parity gate, not silently
|
||||
// pass.
|
||||
let rows: Vec<Value> =
|
||||
serde_json::from_str(GAP_TRUTH).expect("gap truth parses");
|
||||
let rows: Vec<Value> = serde_json::from_str(GAP_TRUTH).expect("gap truth parses");
|
||||
let row = rows
|
||||
.iter()
|
||||
.find(|r| r["fn"].as_str() == Some("_mse_core"))
|
||||
|
||||
@@ -213,7 +213,11 @@ pub fn neurons_decision(neurons: &[Value], query: &str, budget: i64, min_score:
|
||||
let text = py_str(neuron.get("text"));
|
||||
let score = lex_overlap_score(&text, query);
|
||||
let slug = py_str(neuron.get("slug"));
|
||||
let slug = if slug.is_empty() { "warm".to_string() } else { slug };
|
||||
let slug = if slug.is_empty() {
|
||||
"warm".to_string()
|
||||
} else {
|
||||
slug
|
||||
};
|
||||
Some((format!("[{slug}] {text}"), score))
|
||||
});
|
||||
Value::Array(floored_rows(
|
||||
@@ -242,7 +246,13 @@ pub fn graphs_decision(
|
||||
}
|
||||
(text, score)
|
||||
});
|
||||
Value::Array(floored_rows(pairs, "graph", "graph_memory.query", budget, min_score))
|
||||
Value::Array(floored_rows(
|
||||
pairs,
|
||||
"graph",
|
||||
"graph_memory.query",
|
||||
budget,
|
||||
min_score,
|
||||
))
|
||||
}
|
||||
|
||||
/// _abstraction_recall's post-IO decision math: LIKE matches are only
|
||||
@@ -253,7 +263,13 @@ pub fn abstraction_decision(whats: &[Value], query: &str, budget: i64, min_score
|
||||
let score = lex_overlap_score(&text, query);
|
||||
(text, score)
|
||||
});
|
||||
Value::Array(floored_rows(pairs, "abstraction", "abstraction facts", budget, min_score))
|
||||
Value::Array(floored_rows(
|
||||
pairs,
|
||||
"abstraction",
|
||||
"abstraction facts",
|
||||
budget,
|
||||
min_score,
|
||||
))
|
||||
}
|
||||
|
||||
/// _consolidated_recall's post-IO decision math: knowledge competes under the
|
||||
|
||||
@@ -28,11 +28,7 @@ fn frozen_truth_replays_verbatim() {
|
||||
let want_keys: Vec<&String> = want.keys().collect();
|
||||
assert_eq!(got_keys, want_keys, "case {name}: key order");
|
||||
for (k, v) in want {
|
||||
assert_eq!(
|
||||
got[k].as_i64(),
|
||||
v.as_i64(),
|
||||
"case {name}: share {k}"
|
||||
);
|
||||
assert_eq!(got[k].as_i64(), v.as_i64(), "case {name}: share {k}");
|
||||
}
|
||||
}
|
||||
"type_for_request" => {
|
||||
|
||||
@@ -59,7 +59,10 @@ pub fn allocate_budgets(total_tokens: i64) -> Map<String, Value> {
|
||||
used += share;
|
||||
}
|
||||
// remainder absorbs rounding
|
||||
out.insert(TYPES[TYPES.len() - 1].to_string(), json!(total_tokens - used));
|
||||
out.insert(
|
||||
TYPES[TYPES.len() - 1].to_string(),
|
||||
json!(total_tokens - used),
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
@@ -71,7 +74,16 @@ const ROUTE_TABLE: [(&[&str], &str); 5] = [
|
||||
EPISODIC,
|
||||
),
|
||||
(
|
||||
&["how to", "how do", "skill", "procedure", "steps", "deploy", "build", "run"],
|
||||
&[
|
||||
"how to",
|
||||
"how do",
|
||||
"skill",
|
||||
"procedure",
|
||||
"steps",
|
||||
"deploy",
|
||||
"build",
|
||||
"run",
|
||||
],
|
||||
PROCEDURAL,
|
||||
),
|
||||
(&["who am i", "identity", "values", "persona"], SELF),
|
||||
@@ -159,10 +171,7 @@ mod phaseb_units {
|
||||
|
||||
#[test]
|
||||
fn shares_sum_to_one() {
|
||||
let total: f64 = budget_shares()
|
||||
.values()
|
||||
.filter_map(Value::as_f64)
|
||||
.sum();
|
||||
let total: f64 = budget_shares().values().filter_map(Value::as_f64).sum();
|
||||
assert!((total - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,12 +47,7 @@ fn check_err(case_name: &str, want: &Value, got: &crate::MgErr) -> Result<(), St
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn expect_field(
|
||||
case_name: &str,
|
||||
field: &str,
|
||||
got: &Value,
|
||||
want: &Value,
|
||||
) -> Result<(), String> {
|
||||
fn expect_field(case_name: &str, field: &str, got: &Value, want: &Value) -> Result<(), String> {
|
||||
if got != want {
|
||||
return Err(format!(
|
||||
"[{case_name}] {field} mismatch:\n got: {got}\n want: {want}"
|
||||
@@ -159,26 +154,43 @@ fn replay(group: &str, c: &Value) -> Result<(), String> {
|
||||
}
|
||||
"lineage" => {
|
||||
let g = graph_from_wire(input).map_err(|e| format!("[{case_name}] {e:?}"))?;
|
||||
let got = Value::Array(g.lineage(input["node_id"].as_str()).into_iter().cloned().collect());
|
||||
let got = Value::Array(
|
||||
g.lineage(input["node_id"].as_str())
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
);
|
||||
expect_field(&case_name, "result", &got, &expected["result"])
|
||||
}
|
||||
"history" => {
|
||||
let g = graph_from_wire(input).map_err(|e| format!("[{case_name}] {e:?}"))?;
|
||||
let got = Value::Array(
|
||||
g.history(input["node_id"].as_str().unwrap_or_default()).into_iter().cloned().collect(),
|
||||
g.history(input["node_id"].as_str().unwrap_or_default())
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
);
|
||||
expect_field(&case_name, "result", &got, &expected["result"])
|
||||
}
|
||||
"migrate_flat" => {
|
||||
let text = input.get("text").and_then(Value::as_str);
|
||||
let transcript: Vec<Value> = input["transcript"].as_array().cloned().unwrap_or_default();
|
||||
let transcript: Vec<Value> =
|
||||
input["transcript"].as_array().cloned().unwrap_or_default();
|
||||
let ids: Vec<String> = input["ids"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or_default().to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or_default().to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let tss: Vec<String> = input["tss"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().map(|v| v.as_str().unwrap_or_default().to_string()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.map(|v| v.as_str().unwrap_or_default().to_string())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
match MessageGraph::migrate_flat(text, &transcript, &ids, &tss) {
|
||||
Err(e) => check_err(&case_name, c, &e),
|
||||
@@ -284,8 +296,17 @@ fn battery_probe() {
|
||||
#[test]
|
||||
fn battery_total_is_68() {
|
||||
let total: usize = [
|
||||
"load", "save", "add", "tool_call", "tool_result", "revise", "branch",
|
||||
"lineage", "history", "migrate_flat", "cue",
|
||||
"load",
|
||||
"save",
|
||||
"add",
|
||||
"tool_call",
|
||||
"tool_result",
|
||||
"revise",
|
||||
"branch",
|
||||
"lineage",
|
||||
"history",
|
||||
"migrate_flat",
|
||||
"cue",
|
||||
]
|
||||
.iter()
|
||||
.map(|g| run_group(g))
|
||||
|
||||
@@ -155,10 +155,16 @@ pub struct MgErr {
|
||||
|
||||
impl MgErr {
|
||||
fn value(msg: impl Into<String>) -> Self {
|
||||
MgErr { kind: ErrKind::ValueError, message: msg.into() }
|
||||
MgErr {
|
||||
kind: ErrKind::ValueError,
|
||||
message: msg.into(),
|
||||
}
|
||||
}
|
||||
fn key(msg: impl Into<String>) -> Self {
|
||||
MgErr { kind: ErrKind::KeyError, message: msg.into() }
|
||||
MgErr {
|
||||
kind: ErrKind::KeyError,
|
||||
message: msg.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,10 +198,7 @@ impl MessageGraph {
|
||||
let Some(obj) = doc.as_object() else {
|
||||
return Err(MgErr {
|
||||
kind: ErrKind::AttributeError,
|
||||
message: format!(
|
||||
"'{}' object has no attribute 'get'",
|
||||
py_type(&doc)
|
||||
),
|
||||
message: format!("'{}' object has no attribute 'get'", py_type(&doc)),
|
||||
});
|
||||
};
|
||||
if !schema_is_one(obj.get("schema").unwrap_or(&Value::Null)) {
|
||||
@@ -268,8 +271,8 @@ impl MessageGraph {
|
||||
) -> Result<Value, MgErr> {
|
||||
let kind =
|
||||
Kind::parse(kind).ok_or_else(|| MgErr::value(format!("unknown kind '{kind}'")))?;
|
||||
let edge = EdgeType::parse(edge)
|
||||
.ok_or_else(|| MgErr::value(format!("unknown edge '{edge}'")))?;
|
||||
let edge =
|
||||
EdgeType::parse(edge).ok_or_else(|| MgErr::value(format!("unknown edge '{edge}'")))?;
|
||||
let parent_value = match parent {
|
||||
Some(p) => json!(p),
|
||||
None => match self.order.last() {
|
||||
@@ -355,7 +358,9 @@ impl MessageGraph {
|
||||
Some(old) => match old.get("role") {
|
||||
Some(r) => (
|
||||
r.clone(),
|
||||
old.get("parent").and_then(Value::as_str).map(str::to_string),
|
||||
old.get("parent")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string),
|
||||
),
|
||||
None => return Err(MgErr::key("'role'")),
|
||||
},
|
||||
@@ -380,12 +385,7 @@ impl MessageGraph {
|
||||
}
|
||||
|
||||
/// Start a new conversation branch off any message.
|
||||
pub fn branch(
|
||||
&mut self,
|
||||
node_id: &str,
|
||||
head_id: &str,
|
||||
ts: &str,
|
||||
) -> Result<Value, MgErr> {
|
||||
pub fn branch(&mut self, node_id: &str, head_id: &str, ts: &str) -> Result<Value, MgErr> {
|
||||
if !self.nodes.contains_key(node_id) {
|
||||
return Err(MgErr::key(format!("no such message '{node_id}'")));
|
||||
}
|
||||
@@ -451,9 +451,7 @@ impl MessageGraph {
|
||||
.map(|edges| {
|
||||
edges
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
e.get("type").and_then(Value::as_str) == Some("supersedes")
|
||||
})
|
||||
.filter(|e| e.get("type").and_then(Value::as_str) == Some("supersedes"))
|
||||
.filter_map(|e| e.get("to").and_then(Value::as_str))
|
||||
.collect()
|
||||
})
|
||||
@@ -586,14 +584,14 @@ fn py_type(v: &Value) -> &'static str {
|
||||
/// The oracle's `doc.get("schema") != SCHEMA` gate: Python equality —
|
||||
/// 1.0 == 1 and True == 1 both pass.
|
||||
fn schema_is_one(v: &Value) -> bool {
|
||||
v.as_i64() == Some(SCHEMA)
|
||||
|| v.as_f64() == Some(SCHEMA as f64)
|
||||
|| v.as_bool() == Some(true)
|
||||
v.as_i64() == Some(SCHEMA) || v.as_f64() == Some(SCHEMA as f64) || v.as_bool() == Some(true)
|
||||
}
|
||||
|
||||
/// Graph state from the wire: {"nodes": {id: node}, "order": [ids]}.
|
||||
fn graph_from_wire(arg: &Value) -> Result<MessageGraph, MgErr> {
|
||||
let g = arg.get("graph").ok_or_else(|| arg_err("arg.graph required"))?;
|
||||
let g = arg
|
||||
.get("graph")
|
||||
.ok_or_else(|| arg_err("arg.graph required"))?;
|
||||
let nodes = g
|
||||
.get("nodes")
|
||||
.and_then(Value::as_object)
|
||||
@@ -659,19 +657,23 @@ fn envelope(r: Result<Value, MgErr>) -> *mut c_char {
|
||||
};
|
||||
match CString::new(serde_json::to_string(&v).unwrap_or_default()) {
|
||||
Ok(s) => s.into_raw(),
|
||||
Err(_) => CString::new("{\"ok\":false,\"err\":\"envelope nul\"}").unwrap().into_raw(),
|
||||
Err(_) => CString::new("{\"ok\":false,\"err\":\"envelope nul\"}")
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_err(e: impl Into<String>) -> MgErr {
|
||||
MgErr { kind: ErrKind::ValueError, message: e.into() }
|
||||
MgErr {
|
||||
kind: ErrKind::ValueError,
|
||||
message: e.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mg_load(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
MessageGraph::load(opt_str(&a, "text")).map(|g| g.to_wire())
|
||||
});
|
||||
let r =
|
||||
read_arg(arg).and_then(|a| MessageGraph::load(opt_str(&a, "text")).map(|g| g.to_wire()));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
@@ -765,7 +767,10 @@ pub extern "C" fn mg_lineage(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let g = graph_from_wire(&a)?;
|
||||
Ok(Value::Array(
|
||||
g.lineage(opt_str(&a, "node_id")).into_iter().cloned().collect(),
|
||||
g.lineage(opt_str(&a, "node_id"))
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
))
|
||||
});
|
||||
envelope(r)
|
||||
@@ -776,7 +781,10 @@ pub extern "C" fn mg_history(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let g = graph_from_wire(&a)?;
|
||||
Ok(Value::Array(
|
||||
g.history(require_str(&a, "node_id")?).into_iter().cloned().collect(),
|
||||
g.history(require_str(&a, "node_id")?)
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect(),
|
||||
))
|
||||
});
|
||||
envelope(r)
|
||||
@@ -812,11 +820,8 @@ pub extern "C" fn mg_migrate_flat(arg: *const c_char) -> *mut c_char {
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn mg_cue(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).map(|a| {
|
||||
Value::String(MessageGraph::cue(
|
||||
a.get("node").unwrap_or(&Value::Null),
|
||||
))
|
||||
});
|
||||
let r = read_arg(arg)
|
||||
.map(|a| Value::String(MessageGraph::cue(a.get("node").unwrap_or(&Value::Null))));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ fn replays_frozen_oracle_truth_verbatim() {
|
||||
let got = step(path_str, op);
|
||||
let want = scenario["obs"][i]["result"].clone();
|
||||
assert_eq!(
|
||||
got, want,
|
||||
got,
|
||||
want,
|
||||
"{name} op#{i} ({}) diverged from the frozen oracle",
|
||||
op["op"].as_str().unwrap_or("?")
|
||||
);
|
||||
@@ -70,5 +71,8 @@ fn unknown_op_is_an_err_envelope_not_a_panic() {
|
||||
let out = unsafe { std::ffi::CStr::from_ptr(ptr) }.to_str().unwrap();
|
||||
let env: Value = serde_json::from_str(out).unwrap();
|
||||
assert_eq!(env["ok"], Value::Bool(false));
|
||||
assert!(env["err"].as_str().unwrap().contains("arg.op must be one of"));
|
||||
assert!(env["err"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("arg.op must be one of"));
|
||||
}
|
||||
@@ -217,9 +217,12 @@ impl Where<'_> {
|
||||
/// OutcomeLedger._query_stats — identical aggregate SQL to the oracle.
|
||||
pub fn query_stats(db_path: &str, wc: Where) -> Result<Value, String> {
|
||||
let conn = Conn::open(db_path)?;
|
||||
let sql = format!("{STATS_SQL_HEAD}
|
||||
let sql = format!(
|
||||
"{STATS_SQL_HEAD}
|
||||
{}
|
||||
", wc.sql());
|
||||
",
|
||||
wc.sql()
|
||||
);
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
if let Some(p) = wc.param() {
|
||||
stmt.bind_text(1, p)?;
|
||||
@@ -339,7 +342,9 @@ impl<'a> Args<'a> {
|
||||
}
|
||||
|
||||
fn record(&self) -> Result<RecordView<'a>, String> {
|
||||
let v = self.record.ok_or_else(|| "arg.record required".to_string())?;
|
||||
let v = self
|
||||
.record
|
||||
.ok_or_else(|| "arg.record required".to_string())?;
|
||||
RecordView::from_value(v).ok_or_else(|| "arg.record invalid".to_string())
|
||||
}
|
||||
}
|
||||
@@ -381,7 +386,10 @@ pub extern "C" fn ol_run(arg: *const std::os::raw::c_char) -> *mut std::os::raw:
|
||||
.get("op")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(Op::parse)
|
||||
.ok_or_else(|| "arg.op must be one of init_db|record|get_stats|get_model_stats|to_dict".to_string())?;
|
||||
.ok_or_else(|| {
|
||||
"arg.op must be one of init_db|record|get_stats|get_model_stats|to_dict"
|
||||
.to_string()
|
||||
})?;
|
||||
let args = Args::from_value(&doc).ok_or_else(|| "arg.db_path required".to_string())?;
|
||||
dispatch(op, &args)
|
||||
});
|
||||
|
||||
@@ -65,7 +65,12 @@ impl Conn {
|
||||
let cpath = CString::new(path).map_err(|e| e.to_string())?;
|
||||
let mut db: *mut c_void = std::ptr::null_mut();
|
||||
let rc = unsafe {
|
||||
sqlite3_open_v2(cpath.as_ptr(), &mut db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, std::ptr::null())
|
||||
sqlite3_open_v2(
|
||||
cpath.as_ptr(),
|
||||
&mut db,
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if rc != SQLITE_OK {
|
||||
return Err(format!("sqlite3_open_v2 failed (rc={rc}) for {path}"));
|
||||
@@ -129,7 +134,13 @@ impl Stmt {
|
||||
pub fn bind_text(&mut self, idx: usize, val: &str) -> Result<(), String> {
|
||||
let cval = CString::new(val).map_err(|e| e.to_string())?;
|
||||
let rc = unsafe {
|
||||
sqlite3_bind_text(self.0, idx as c_int, cval.as_ptr(), val.len() as c_int, SQLITE_TRANSIENT)
|
||||
sqlite3_bind_text(
|
||||
self.0,
|
||||
idx as c_int,
|
||||
cval.as_ptr(),
|
||||
val.len() as c_int,
|
||||
SQLITE_TRANSIENT,
|
||||
)
|
||||
};
|
||||
if rc != SQLITE_OK {
|
||||
Err(format!("sqlite3_bind_text failed (rc={rc})"))
|
||||
@@ -198,7 +209,9 @@ impl Stmt {
|
||||
if ptr.is_null() {
|
||||
String::new()
|
||||
} else {
|
||||
CStr::from_ptr(ptr.cast::<c_char>()).to_string_lossy().into_owned()
|
||||
CStr::from_ptr(ptr.cast::<c_char>())
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ fn frozen_truth_replays_verbatim() {
|
||||
let expect = &c["expect"];
|
||||
match c["fn"].as_str().expect("fn tag") {
|
||||
"apply_outcome" => {
|
||||
let got = crate::apply_outcome(f(args, "utility"), args["outcome"].as_str().unwrap());
|
||||
let got =
|
||||
crate::apply_outcome(f(args, "utility"), args["outcome"].as_str().unwrap());
|
||||
assert_eq!(Some(got), expect.as_f64(), "case {name}");
|
||||
}
|
||||
"apply_stale" => {
|
||||
|
||||
@@ -92,7 +92,10 @@ pub fn record_verified_outcome(
|
||||
store.insert(key.clone(), json!({"utility": 0.0, "confidence": 0.5}));
|
||||
store.get_mut(&key).unwrap()
|
||||
};
|
||||
let util = current.get("utility").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let util = current
|
||||
.get("utility")
|
||||
.and_then(Value::as_f64)
|
||||
.unwrap_or(0.0);
|
||||
let new_util = apply_outcome(util, outcome);
|
||||
current["utility"] = json!(new_util);
|
||||
let confidence = store[&key]
|
||||
|
||||
@@ -23,10 +23,7 @@ fn at<'a>(v: &'a Json, path: &[&str]) -> Option<&'a Json> {
|
||||
let mut cur = v;
|
||||
for k in path {
|
||||
cur = match cur {
|
||||
Json::Obj(pairs) => pairs
|
||||
.iter()
|
||||
.find(|(k2, _)| k2 == k)
|
||||
.map(|(_, x)| x)?,
|
||||
Json::Obj(pairs) => pairs.iter().find(|(k2, _)| k2 == k).map(|(_, x)| x)?,
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,9 +88,10 @@ pub fn classify_utterance(text: Option<&str>) -> Json {
|
||||
UtteranceKind::Amendment => {
|
||||
obj(vec![("kind", s("amendment")), ("priority", Json::Num(2.0))])
|
||||
}
|
||||
UtteranceKind::Discussion => {
|
||||
obj(vec![("kind", s("discussion")), ("priority", Json::Num(1.0))])
|
||||
}
|
||||
UtteranceKind::Discussion => obj(vec![
|
||||
("kind", s("discussion")),
|
||||
("priority", Json::Num(1.0)),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,11 +192,9 @@ fn envelope(result: Result<Json, (String, String)>) -> *mut c_char {
|
||||
let out = pyjson::dumps(&body);
|
||||
match CString::new(out) {
|
||||
Ok(c) => c.into_raw(),
|
||||
Err(_) => CString::new(
|
||||
r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#,
|
||||
)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
Err(_) => CString::new(r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +217,10 @@ fn opt_text(doc: &Json, key: &str) -> Result<Option<String>, (String, String)> {
|
||||
None => Err((format!("{key} is required"), "KeyError".into())),
|
||||
Some(Json::Null) => Ok(None),
|
||||
Some(Json::Str(t)) => Ok(Some(t.clone())),
|
||||
Some(_) => Err((format!("{key} must be a string or null"), "TypeError".into())),
|
||||
Some(_) => Err((
|
||||
format!("{key} must be a string or null"),
|
||||
"TypeError".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,32 +228,37 @@ macro_rules! ffi {
|
||||
($name:ident, $body:expr) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn $name(arg: *const c_char) -> *mut c_char {
|
||||
let out: Result<Json, (String, String)> =
|
||||
read_arg(arg).and_then(|raw| $body(&raw));
|
||||
let out: Result<Json, (String, String)> = read_arg(arg).and_then(|raw| $body(&raw));
|
||||
envelope(out)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ffi!(pa_classify_utterance, |raw: &str| -> Result<Json, (String, String)> {
|
||||
ffi!(pa_classify_utterance, |raw: &str| -> Result<
|
||||
Json,
|
||||
(String, String),
|
||||
> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let text = opt_text(&doc, "text")?;
|
||||
Ok(classify_utterance(text.as_deref()))
|
||||
});
|
||||
|
||||
ffi!(pa_apply_to_task, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let pane = match doc.get("pane") {
|
||||
Some(Json::Str(p)) => p.clone(),
|
||||
_ => return Err(("pane must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let task = match doc.get("task") {
|
||||
Some(t @ Json::Obj(_)) => t.clone(),
|
||||
_ => return Err(("task must be an object".into(), "TypeError".into())),
|
||||
};
|
||||
let utterance = opt_text(&doc, "utterance")?;
|
||||
Ok(apply_to_task(&pane, &task, utterance.as_deref()))
|
||||
});
|
||||
ffi!(
|
||||
pa_apply_to_task,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let pane = match doc.get("pane") {
|
||||
Some(Json::Str(p)) => p.clone(),
|
||||
_ => return Err(("pane must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let task = match doc.get("task") {
|
||||
Some(t @ Json::Obj(_)) => t.clone(),
|
||||
_ => return Err(("task must be an object".into(), "TypeError".into())),
|
||||
};
|
||||
let utterance = opt_text(&doc, "utterance")?;
|
||||
Ok(apply_to_task(&pane, &task, utterance.as_deref()))
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
@@ -61,9 +61,7 @@ fn replay_result(group: &str, f: impl Fn(&Value) -> Result<Value, crate::Registr
|
||||
|
||||
#[test]
|
||||
fn default_doc_parity_battery() {
|
||||
replay("default_doc", |i| {
|
||||
json!({"doc": default_doc(s(i, "now"))})
|
||||
});
|
||||
replay("default_doc", |i| json!({"doc": default_doc(s(i, "now"))}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -88,24 +86,31 @@ fn register_parity_battery() {
|
||||
|
||||
#[test]
|
||||
fn get_parity_battery() {
|
||||
replay("get", |i| {
|
||||
json!({"entry": get(&doc_of(i), s(i, "project_id")).unwrap()})
|
||||
});
|
||||
replay(
|
||||
"get",
|
||||
|i| json!({"entry": get(&doc_of(i), s(i, "project_id")).unwrap()}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_projects_parity_battery() {
|
||||
replay("all_projects", |i| {
|
||||
json!({"projects": all_projects(&doc_of(i)).unwrap()})
|
||||
});
|
||||
replay(
|
||||
"all_projects",
|
||||
|i| json!({"projects": all_projects(&doc_of(i)).unwrap()}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_health_parity_battery() {
|
||||
replay_result("set_health", |i| {
|
||||
set_health(&doc_of(i), s(i, "project_id"), s(i, "health"), s(i, "note"),
|
||||
s(i, "now"))
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
set_health(
|
||||
&doc_of(i),
|
||||
s(i, "project_id"),
|
||||
s(i, "health"),
|
||||
s(i, "note"),
|
||||
s(i, "now"),
|
||||
)
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,25 +125,29 @@ fn set_outstanding_parity_battery() {
|
||||
#[test]
|
||||
fn mark_generation_parity_battery() {
|
||||
replay_result("mark_generation", |i| {
|
||||
mark_generation(&doc_of(i), s(i, "project_id"), s(i, "current"),
|
||||
s(i, "last_known_good"))
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
mark_generation(
|
||||
&doc_of(i),
|
||||
s(i, "project_id"),
|
||||
s(i, "current"),
|
||||
s(i, "last_known_good"),
|
||||
)
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_cwd_parity_battery() {
|
||||
replay_result("resolve_cwd", |i| {
|
||||
resolve_cwd(&doc_of(i), s(i, "project_id"))
|
||||
.map(|cwd| json!({"cwd": cwd}))
|
||||
resolve_cwd(&doc_of(i), s(i, "project_id")).map(|cwd| json!({"cwd": cwd}))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn needs_migration_parity_battery() {
|
||||
replay("needs_migration", |i| {
|
||||
json!({"ids": needs_migration(&doc_of(i), i["proposed"].as_i64().unwrap()).unwrap()})
|
||||
});
|
||||
replay(
|
||||
"needs_migration",
|
||||
|i| json!({"ids": needs_migration(&doc_of(i), i["proposed"].as_i64().unwrap()).unwrap()}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -302,8 +302,7 @@ pub fn set_health(
|
||||
.and_then(Value::as_object_mut)
|
||||
.ok_or_else(|| RegistryError::Key("scaffold_state".to_string()))?;
|
||||
state.insert("health".to_string(), json!(lattice.as_str()));
|
||||
state
|
||||
.insert("last_material_update".to_string(), json!(now));
|
||||
state.insert("last_material_update".to_string(), json!(now));
|
||||
if !note.is_empty() {
|
||||
let notes = state
|
||||
.entry("notes".to_string())
|
||||
@@ -379,10 +378,7 @@ impl CwdResolution {
|
||||
/// unresolved/refuse. Registry stores repo_identity as "git:<path>" or a
|
||||
/// bare path. Scaffold storage is NEVER used as a repo-cwd proxy. Raises
|
||||
/// for unknown projects (refuse mutation rather than guessing).
|
||||
pub fn resolve_cwd(
|
||||
doc: &Map<String, Value>,
|
||||
project_id: &str,
|
||||
) -> Result<String, RegistryError> {
|
||||
pub fn resolve_cwd(doc: &Map<String, Value>, project_id: &str) -> Result<String, RegistryError> {
|
||||
let entry = projects_of(doc)?
|
||||
.get(project_id)
|
||||
.ok_or_else(|| unknown_project(project_id))?;
|
||||
@@ -414,7 +410,10 @@ pub fn resolve_cwd(
|
||||
/// `needs_migration` — projects whose structural schema is older than a
|
||||
/// proposed version (feeds the 18I consent sweep — never silent
|
||||
/// upgrades). Entries default to schema version 1.
|
||||
pub fn needs_migration(doc: &Map<String, Value>, proposed: i64) -> Result<Vec<String>, RegistryError> {
|
||||
pub fn needs_migration(
|
||||
doc: &Map<String, Value>,
|
||||
proposed: i64,
|
||||
) -> Result<Vec<String>, RegistryError> {
|
||||
let mut ids: Vec<String> = projects_of(doc)?
|
||||
.iter()
|
||||
.filter(|(_, p)| SchemaVersion::of(p).older_than(proposed))
|
||||
@@ -494,8 +493,7 @@ pub extern "C" fn pr_register(arg: *const c_char) -> *mut c_char {
|
||||
s("repo_identity"),
|
||||
s("forge_repo"),
|
||||
s("default_branch"),
|
||||
a.get("structural_schema_version")
|
||||
.unwrap_or(&Value::Null),
|
||||
a.get("structural_schema_version").unwrap_or(&Value::Null),
|
||||
s("runtime_generation"),
|
||||
s("last_known_good"),
|
||||
s("now"),
|
||||
@@ -531,9 +529,15 @@ pub extern "C" fn pr_all_projects(arg: *const c_char) -> *mut c_char {
|
||||
pub extern "C" fn pr_set_health(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
set_health(&doc_of(&a), s("project_id"), s("health"), s("note"), s("now"))
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
.map_err(FfiError::Registry)
|
||||
set_health(
|
||||
&doc_of(&a),
|
||||
s("project_id"),
|
||||
s("health"),
|
||||
s("note"),
|
||||
s("now"),
|
||||
)
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -554,9 +558,14 @@ pub extern "C" fn pr_set_outstanding(arg: *const c_char) -> *mut c_char {
|
||||
pub extern "C" fn pr_mark_generation(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
let s = |k: &str| a.get(k).and_then(Value::as_str).unwrap_or("");
|
||||
mark_generation(&doc_of(&a), s("project_id"), s("current"), s("last_known_good"))
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
.map_err(FfiError::Registry)
|
||||
mark_generation(
|
||||
&doc_of(&a),
|
||||
s("project_id"),
|
||||
s("current"),
|
||||
s("last_known_good"),
|
||||
)
|
||||
.map(|doc| json!({"doc": doc}))
|
||||
.map_err(FfiError::Registry)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! store fixtures). Every case must reproduce byte-identically through the
|
||||
//! Rust kernel.
|
||||
|
||||
use super::{_fetch_corpus, pyjson, expand, Json};
|
||||
use super::{_fetch_corpus, expand, pyjson, Json};
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_ru_truth.json");
|
||||
|
||||
@@ -47,7 +47,10 @@ fn frozen_truth_replays_verbatim() {
|
||||
_ => None,
|
||||
};
|
||||
let got = Json::Arr(expand(query, corpus).into_iter().map(Json::Str).collect());
|
||||
assert_eq!(got, expected, "case {i} ({query:?}) diverged from the oracle");
|
||||
assert_eq!(
|
||||
got, expected,
|
||||
"case {i} ({query:?}) diverged from the oracle"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,32 +39,86 @@ pub const MAX_COOCCUR_WORDS: usize = 40;
|
||||
/// The expansion map, in oracle dict-literal order (iteration order is
|
||||
/// observable: terms are added map-order first).
|
||||
const EXPANSIONS: &[(&str, &[&str])] = &[
|
||||
("housing", &["housing", "rent", "rent burden", "eviction", "affordable housing", "homelessness", "landlord", "mortgage"]),
|
||||
("homeless", &["homelessness", "homeless", "housing", "shelter", "rent"]),
|
||||
("memory", &["memory store", "recall", "retrieval", "embedding", "sqlite-vec"]),
|
||||
("recall", &["recall", "retrieval", "memory store", "abstention", "floor"]),
|
||||
("router", &["routing", "adaptive router", "route authority", "RouteDecision"]),
|
||||
("route", &["routing", "adaptive router", "route authority", "escalation"]),
|
||||
("research", &["research pipeline", "epistemic stage", "OUTWARD", "SYNTHESIS"]),
|
||||
(
|
||||
"housing",
|
||||
&[
|
||||
"housing",
|
||||
"rent",
|
||||
"rent burden",
|
||||
"eviction",
|
||||
"affordable housing",
|
||||
"homelessness",
|
||||
"landlord",
|
||||
"mortgage",
|
||||
],
|
||||
),
|
||||
(
|
||||
"homeless",
|
||||
&["homelessness", "homeless", "housing", "shelter", "rent"],
|
||||
),
|
||||
(
|
||||
"memory",
|
||||
&[
|
||||
"memory store",
|
||||
"recall",
|
||||
"retrieval",
|
||||
"embedding",
|
||||
"sqlite-vec",
|
||||
],
|
||||
),
|
||||
(
|
||||
"recall",
|
||||
&["recall", "retrieval", "memory store", "abstention", "floor"],
|
||||
),
|
||||
(
|
||||
"router",
|
||||
&[
|
||||
"routing",
|
||||
"adaptive router",
|
||||
"route authority",
|
||||
"RouteDecision",
|
||||
],
|
||||
),
|
||||
(
|
||||
"route",
|
||||
&[
|
||||
"routing",
|
||||
"adaptive router",
|
||||
"route authority",
|
||||
"escalation",
|
||||
],
|
||||
),
|
||||
(
|
||||
"research",
|
||||
&[
|
||||
"research pipeline",
|
||||
"epistemic stage",
|
||||
"OUTWARD",
|
||||
"SYNTHESIS",
|
||||
],
|
||||
),
|
||||
("test", &["unit test", "pytest", "regression", "suite"]),
|
||||
("ci", &["CI", "gitea", "pipeline", "required checks"]),
|
||||
("model", &["model routing", "provider", "ollama", "local model"]),
|
||||
("migration", &["schema migration", "PLAN", "old-schema", "compat shim"]),
|
||||
(
|
||||
"model",
|
||||
&["model routing", "provider", "ollama", "local model"],
|
||||
),
|
||||
(
|
||||
"migration",
|
||||
&["schema migration", "PLAN", "old-schema", "compat shim"],
|
||||
),
|
||||
("gitea", &["gitea", "issue", "pull request", "CI"]),
|
||||
];
|
||||
|
||||
/// The oracle's stopword set (insertion is a set — membership only).
|
||||
const STOPWORDS: &[&str] = &[
|
||||
"the", "a", "an", "and", "or", "of", "to", "in", "on", "for", "with",
|
||||
"is", "are", "was", "were", "be", "been", "it", "its", "this", "that",
|
||||
"as", "at", "by", "from", "has", "have", "had", "not", "no", "but",
|
||||
"if", "then", "than", "so", "such", "can", "could", "should", "would",
|
||||
"will", "shall", "may", "might", "must", "do", "does", "did", "done",
|
||||
"you", "your", "i", "me", "my", "we", "our", "they", "their", "them",
|
||||
"he", "she", "his", "her", "what", "which", "who", "whom", "when",
|
||||
"where", "why", "how", "all", "any", "both", "each", "few", "more",
|
||||
"most", "other", "some", "only", "own", "same", "too", "very", "just",
|
||||
"also",
|
||||
"the", "a", "an", "and", "or", "of", "to", "in", "on", "for", "with", "is", "are", "was",
|
||||
"were", "be", "been", "it", "its", "this", "that", "as", "at", "by", "from", "has", "have",
|
||||
"had", "not", "no", "but", "if", "then", "than", "so", "such", "can", "could", "should",
|
||||
"would", "will", "shall", "may", "might", "must", "do", "does", "did", "done", "you", "your",
|
||||
"i", "me", "my", "we", "our", "they", "their", "them", "he", "she", "his", "her", "what",
|
||||
"which", "who", "whom", "when", "where", "why", "how", "all", "any", "both", "each", "few",
|
||||
"more", "most", "other", "some", "only", "own", "same", "too", "very", "just", "also",
|
||||
];
|
||||
|
||||
fn is_stopword(w: &str) -> bool {
|
||||
@@ -93,7 +147,13 @@ fn findall_runs(s: &str, min_len: usize) -> Vec<String> {
|
||||
/// ASCII fold (sqlite LIKE default folds ASCII only).
|
||||
fn ascii_lower(s: &str) -> String {
|
||||
s.chars()
|
||||
.map(|c| if c.is_ascii_uppercase() { (c as u8 + 32) as char } else { c })
|
||||
.map(|c| {
|
||||
if c.is_ascii_uppercase() {
|
||||
(c as u8 + 32) as char
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -218,10 +278,7 @@ pub fn expand(query: &str, corpus: Option<&[Json]>) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
let originals: Vec<String> = tokens
|
||||
.into_iter()
|
||||
.filter(|t| !seen.contains(t))
|
||||
.collect();
|
||||
let originals: Vec<String> = tokens.into_iter().filter(|t| !seen.contains(t)).collect();
|
||||
originals.into_iter().chain(added).collect()
|
||||
}
|
||||
|
||||
@@ -239,10 +296,7 @@ fn read_arg(ptr: *const c_char) -> Result<Json, String> {
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Ok(x) => Json::Obj(vec![("ok".into(), Json::Bool(true)), ("value".into(), x)]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! branch_battery — replays frozen_rb_truth.json VERBATIM.
|
||||
|
||||
use super::{pyjson, new_budget, spend, stop, Json};
|
||||
use super::{new_budget, pyjson, spend, stop, Json};
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_rb_truth.json");
|
||||
|
||||
|
||||
@@ -135,10 +135,7 @@ pub fn stop(budget: &Json, reason: &str) -> Json {
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Ok(x) => Json::Obj(vec![("ok".into(), Json::Bool(true)), ("value".into(), x)]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
|
||||
@@ -24,10 +24,7 @@ fn at<'a>(v: &'a Json, path: &[&str]) -> Option<&'a Json> {
|
||||
let mut cur = v;
|
||||
for k in path {
|
||||
cur = match cur {
|
||||
Json::Obj(pairs) => pairs
|
||||
.iter()
|
||||
.find(|(k2, _)| k2 == k)
|
||||
.map(|(_, x)| x)?,
|
||||
Json::Obj(pairs) => pairs.iter().find(|(k2, _)| k2 == k).map(|(_, x)| x)?,
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ pub enum StatsShape {
|
||||
}
|
||||
|
||||
/// profile_function / profile_model_call stats law (shared body).
|
||||
pub fn profile_stats(times: &[f64], iterations: usize, shape: StatsShape
|
||||
pub fn profile_stats(
|
||||
times: &[f64],
|
||||
iterations: usize,
|
||||
shape: StatsShape,
|
||||
) -> Result<Json, (String, String)> {
|
||||
if times.is_empty() {
|
||||
// the oracle raises min() on the empty list before any stats
|
||||
@@ -95,11 +98,9 @@ fn envelope(result: Result<Json, (String, String)>) -> *mut c_char {
|
||||
let out = pyjson::dumps(&body);
|
||||
match CString::new(out) {
|
||||
Ok(c) => c.into_raw(),
|
||||
Err(_) => CString::new(
|
||||
r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#,
|
||||
)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
Err(_) => CString::new(r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,39 +122,41 @@ macro_rules! ffi {
|
||||
($name:ident, $body:expr) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn $name(arg: *const c_char) -> *mut c_char {
|
||||
let out: Result<Json, (String, String)> =
|
||||
read_arg(arg).and_then(|raw| $body(&raw));
|
||||
let out: Result<Json, (String, String)> = read_arg(arg).and_then(|raw| $body(&raw));
|
||||
envelope(out)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ffi!(rp_profile_stats, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let times: Vec<f64> = match doc.get("times") {
|
||||
Some(Json::Arr(items)) => items
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
// elapsed-ms values are Python floats — json.dumps emits
|
||||
// them with a decimal point, which pyjson parses as
|
||||
// Json::Float; the int form (Num) rides for int inputs
|
||||
Json::Num(n) | Json::Float(n) => Some(*n),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let iterations = match doc.get("iterations") {
|
||||
Some(Json::Num(n)) => *n as usize,
|
||||
_ => times.len(),
|
||||
};
|
||||
let shape = if matches!(doc.get("model_call"), Some(Json::Bool(true))) {
|
||||
StatsShape::ModelCall
|
||||
} else {
|
||||
StatsShape::Function
|
||||
};
|
||||
profile_stats(×, iterations, shape)
|
||||
});
|
||||
ffi!(
|
||||
rp_profile_stats,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let times: Vec<f64> = match doc.get("times") {
|
||||
Some(Json::Arr(items)) => items
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
// elapsed-ms values are Python floats — json.dumps emits
|
||||
// them with a decimal point, which pyjson parses as
|
||||
// Json::Float; the int form (Num) rides for int inputs
|
||||
Json::Num(n) | Json::Float(n) => Some(*n),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let iterations = match doc.get("iterations") {
|
||||
Some(Json::Num(n)) => *n as usize,
|
||||
_ => times.len(),
|
||||
};
|
||||
let shape = if matches!(doc.get("model_call"), Some(Json::Bool(true))) {
|
||||
StatsShape::ModelCall
|
||||
} else {
|
||||
StatsShape::Function
|
||||
};
|
||||
profile_stats(×, iterations, shape)
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
@@ -32,13 +32,19 @@ fn get_str<'a>(v: &'a Json, key: &str) -> Option<&'a str> {
|
||||
fn main() {
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_to_string(&mut input).is_err() {
|
||||
emit(Err(("failed to read stdin".to_string(), "OSError".to_string())));
|
||||
emit(Err((
|
||||
"failed to read stdin".to_string(),
|
||||
"OSError".to_string(),
|
||||
)));
|
||||
return;
|
||||
}
|
||||
let parsed = match pyjson::parse(&input) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
emit(Err((format!("stdin JSON parse failed: {e}"), "ValueError".to_string())));
|
||||
emit(Err((
|
||||
format!("stdin JSON parse failed: {e}"),
|
||||
"ValueError".to_string(),
|
||||
)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,10 +60,7 @@ fn replay(case: &Json) {
|
||||
.unwrap_or("");
|
||||
assert_eq!(err, msg, "case {}/{} error text", fname, name);
|
||||
}
|
||||
(other, _) => panic!(
|
||||
"case {}/{}: unexpected outcome: {:?}",
|
||||
fname, name, other
|
||||
),
|
||||
(other, _) => panic!("case {}/{}: unexpected outcome: {:?}", fname, name, other),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,4 +234,4 @@ mod truth_battery {
|
||||
let case = parse(TRUTH_CASES[25]);
|
||||
replay(&case);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,9 +75,7 @@ fn push_indent(out: &mut String, depth: usize) {
|
||||
// ── sf_sorted({"v": ...}) -> envelope(normalized) ───────────────────────────
|
||||
|
||||
pub fn sorted_fn(arg: &Json) -> Result<Json, String> {
|
||||
let v = arg
|
||||
.get("v")
|
||||
.ok_or_else(|| "arg.v required".to_string())?;
|
||||
let v = arg.get("v").ok_or_else(|| "arg.v required".to_string())?;
|
||||
Ok(store::sorted_deep(v))
|
||||
}
|
||||
|
||||
@@ -151,16 +149,10 @@ pub fn declared_surfaces(arg: &Json) -> Result<Json, String> {
|
||||
]));
|
||||
}
|
||||
for surface in str_list(u.get("go_surfaces")) {
|
||||
go_surfaces.push(Json::Arr(vec![
|
||||
Json::Str(surface),
|
||||
Json::Str(name.clone()),
|
||||
]));
|
||||
go_surfaces.push(Json::Arr(vec![Json::Str(surface), Json::Str(name.clone())]));
|
||||
}
|
||||
for cfg in str_list(u.get("configs")) {
|
||||
configs.push(Json::Arr(vec![
|
||||
Json::Str(cfg),
|
||||
Json::Str(name.clone()),
|
||||
]));
|
||||
configs.push(Json::Arr(vec![Json::Str(cfg), Json::Str(name.clone())]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +46,12 @@ fn split_name(id: &str) -> &str {
|
||||
/// carries the oracle's EXACT ValueError text (parity battery pins it).
|
||||
pub fn ingest_umbrellas(doc: &mut Json, manifest: &Json, now: &str) -> Result<i64, String> {
|
||||
if obj_get(manifest, GENERATED_VIEW_MARKER).is_some() {
|
||||
return Err("the manifest is a Spine-generated compatibility view; refusing to \
|
||||
return Err(
|
||||
"the manifest is a Spine-generated compatibility view; refusing to \
|
||||
ingest it (Phase A reads the manual authority exactly once). \
|
||||
Structural membership now lives in the Spine store."
|
||||
.to_string());
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let known = [
|
||||
"description",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! deep-copied). Every case must reproduce byte-identically through the
|
||||
//! Rust kernel.
|
||||
|
||||
use super::{pyjson, compile_self_model, estimate_tokens, why_question, Json};
|
||||
use super::{compile_self_model, estimate_tokens, pyjson, why_question, Json};
|
||||
|
||||
const TRUTH: &str = include_str!("../frozen_sm_truth.json");
|
||||
|
||||
@@ -57,8 +57,7 @@ fn frozen_truth_replays_verbatim() {
|
||||
};
|
||||
let (gs, es) = (pyjson::dumps(&got), pyjson::dumps(&expected));
|
||||
assert_eq!(
|
||||
gs,
|
||||
es,
|
||||
gs, es,
|
||||
"case {i} ({fname}) diverged:\n got: {gs}\n want: {es}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,10 +149,7 @@ fn contains_ci(text: &str, topic: &str) -> bool {
|
||||
|
||||
fn envelope(r: Result<Json, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Ok(x) => Json::Obj(vec![("ok".into(), Json::Bool(true)), ("value".into(), x)]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e)),
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
//! an error (envelope law — the loader re-raises).
|
||||
|
||||
use crate::{
|
||||
compare_baseline, monitor, propose_change, promote_or_discard, rollback, run_gates,
|
||||
FfiError,
|
||||
compare_baseline, monitor, promote_or_discard, propose_change, rollback, run_gates, FfiError,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
@@ -47,10 +46,7 @@ fn cases(group: &str) -> &[Value] {
|
||||
// ── fixture + setup interpreter (same recipe as the generator) ───────────
|
||||
|
||||
fn battery_root(group: &str) -> PathBuf {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"smod-battery-{}-{group}",
|
||||
std::process::id()
|
||||
));
|
||||
let root = std::env::temp_dir().join(format!("smod-battery-{}-{group}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(&root).expect("battery root");
|
||||
root
|
||||
@@ -104,8 +100,17 @@ fn run_setup(repo: &Path, steps: &[Value]) -> Option<PathBuf> {
|
||||
match op {
|
||||
"worktree" => {
|
||||
let path = repo.parent().unwrap().join(format!("wt-{arg}"));
|
||||
let (ok, _, err) =
|
||||
git(repo, &["worktree", "add", &path.to_string_lossy(), "-b", arg, "master"]);
|
||||
let (ok, _, err) = git(
|
||||
repo,
|
||||
&[
|
||||
"worktree",
|
||||
"add",
|
||||
&path.to_string_lossy(),
|
||||
"-b",
|
||||
arg,
|
||||
"master",
|
||||
],
|
||||
);
|
||||
assert!(ok, "worktree add failed: {err}");
|
||||
wt = Some(path);
|
||||
}
|
||||
@@ -186,11 +191,9 @@ fn call_kernel(group: &str, input: &Value) -> Result<Value, FfiError> {
|
||||
&input["candidate_result"],
|
||||
&input["baseline_result"],
|
||||
)),
|
||||
"promote_or_discard" => promote_or_discard(
|
||||
s("repo"),
|
||||
&input["proposal"],
|
||||
&input["verdict"],
|
||||
),
|
||||
"promote_or_discard" => {
|
||||
promote_or_discard(s("repo"), &input["proposal"], &input["verdict"])
|
||||
}
|
||||
"rollback" => rollback(s("repo"), s("rollback_ref")),
|
||||
"monitor" => Ok(monitor(&input["proposal"])),
|
||||
other => panic!("unknown kernel group: {other}"),
|
||||
@@ -203,8 +206,11 @@ fn replay_case(group: &str, case: &Value) {
|
||||
let case_root = root.join(name);
|
||||
fs::create_dir_all(&case_root).expect("case root");
|
||||
let repo = make_fixture(&case_root);
|
||||
let wt = run_setup(&repo, case["setup"].as_array().map(Vec::as_slice).unwrap_or(&[]))
|
||||
.unwrap_or_else(|| case_root.join("wt-input"));
|
||||
let wt = run_setup(
|
||||
&repo,
|
||||
case["setup"].as_array().map(Vec::as_slice).unwrap_or(&[]),
|
||||
)
|
||||
.unwrap_or_else(|| case_root.join("wt-input"));
|
||||
let stub = if case["stub"].as_bool().unwrap_or(false) {
|
||||
make_stub_dir(&case_root)
|
||||
} else {
|
||||
@@ -232,12 +238,14 @@ fn replay_case(group: &str, case: &Value) {
|
||||
return;
|
||||
}
|
||||
let got = got.unwrap_or_else(|e| {
|
||||
panic!("[{group}] {name}: kernel raised {e}, expected {:?}", expected)
|
||||
panic!(
|
||||
"[{group}] {name}: kernel raised {e}, expected {:?}",
|
||||
expected
|
||||
)
|
||||
});
|
||||
let got = normalize(&got, &repo, &wt, &stub);
|
||||
assert_eq!(
|
||||
&got,
|
||||
expected,
|
||||
&got, expected,
|
||||
"[{group}] {name} diverged (frozen truth, kernel-level)"
|
||||
);
|
||||
}
|
||||
@@ -253,9 +261,8 @@ impl PathGuard {
|
||||
std::env::set_var(
|
||||
"PATH",
|
||||
std::env::join_paths(
|
||||
std::iter::once(stub.to_path_buf()).chain(
|
||||
std::env::split_paths(old.as_deref().unwrap_or_default()),
|
||||
),
|
||||
std::iter::once(stub.to_path_buf())
|
||||
.chain(std::env::split_paths(old.as_deref().unwrap_or_default())),
|
||||
)
|
||||
.expect("joined PATH"),
|
||||
);
|
||||
@@ -421,9 +428,5 @@ fn phaseb_chars_tail_is_char_boundary_safe() {
|
||||
assert!(tail.ends_with("αβγδε"));
|
||||
let short = "short";
|
||||
assert_eq!(chars_tail(short, 2000), short);
|
||||
assert_eq!(
|
||||
chars_tail("", 2000),
|
||||
"",
|
||||
"empty gate output stays empty"
|
||||
);
|
||||
assert_eq!(chars_tail("", 2000), "", "empty gate output stays empty");
|
||||
}
|
||||
@@ -121,18 +121,9 @@ impl BaselineVerdict {
|
||||
/// 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,
|
||||
},
|
||||
Promoted { branch: String, rollback_ref: Value },
|
||||
MergeFailed { error: String, rollback_ref: Value },
|
||||
Discarded { branch: String, rollback_ref: Value },
|
||||
}
|
||||
|
||||
impl ProposalOutcome {
|
||||
@@ -176,7 +167,10 @@ pub enum FfiError {
|
||||
/// pipe read / wait failed (the oracle's decode errors propagate too).
|
||||
Io(std::io::Error),
|
||||
/// subprocess.run(timeout=…) — TimeoutExpired propagates in the oracle.
|
||||
Timeout { cmd: String, secs: u64 },
|
||||
Timeout {
|
||||
cmd: String,
|
||||
secs: u64,
|
||||
},
|
||||
/// dict[missing-key] — the oracle's KeyError.
|
||||
KeyError(String),
|
||||
/// a non-string reached the argv seam — the oracle's TypeError.
|
||||
@@ -192,7 +186,11 @@ 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))
|
||||
write!(
|
||||
f,
|
||||
"[Errno {errno}] {e}",
|
||||
errno = e.raw_os_error().unwrap_or(0)
|
||||
)
|
||||
}
|
||||
FfiError::Io(e) => write!(f, "{e}"),
|
||||
FfiError::Timeout { cmd, secs } => {
|
||||
@@ -264,9 +262,7 @@ fn _run(cmd: &[&str], cwd: &str, timeout_secs: u64) -> Result<(i32, String, Stri
|
||||
Ok((status.code().unwrap_or(-1), uni(out), uni(err)))
|
||||
}
|
||||
|
||||
fn join_reader(
|
||||
handle: thread::JoinHandle<Result<String, FfiError>>,
|
||||
) -> Result<String, FfiError> {
|
||||
fn join_reader(handle: thread::JoinHandle<Result<String, FfiError>>) -> Result<String, FfiError> {
|
||||
match handle.join() {
|
||||
Ok(r) => r,
|
||||
Err(_) => Err(FfiError::Io(std::io::Error::other(
|
||||
@@ -317,10 +313,7 @@ fn mkdtemp_selfmod() -> Result<String, FfiError> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(
|
||||
&path,
|
||||
std::fs::Permissions::from_mode(0o700),
|
||||
);
|
||||
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700));
|
||||
}
|
||||
return Ok(path.to_string_lossy().into_owned());
|
||||
}
|
||||
@@ -359,12 +352,7 @@ fn random_byte() -> u8 {
|
||||
|
||||
/// `propose_change` — create the isolated worktree + branch. Returns the
|
||||
/// proposal record (with rollback_ref = the base HEAD — the recovery path).
|
||||
pub fn propose_change(
|
||||
repo: &str,
|
||||
branch: &str,
|
||||
base: &str,
|
||||
now: &str,
|
||||
) -> Result<Value, FfiError> {
|
||||
pub fn propose_change(repo: &str, branch: &str, base: &str, now: &str) -> Result<Value, FfiError> {
|
||||
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}")}));
|
||||
@@ -611,12 +599,10 @@ pub extern "C" fn smod_promote_or_discard(arg: *const c_char) -> *mut c_char {
|
||||
/// smod_rollback({repo, rollback_ref}) → result dict
|
||||
#[no_mangle]
|
||||
pub extern "C" fn smod_rollback(arg: *const c_char) -> *mut c_char {
|
||||
envelope(read_arg(arg).and_then(|a| {
|
||||
rollback(
|
||||
required_str(&a, "repo")?,
|
||||
required_str(&a, "rollback_ref")?,
|
||||
)
|
||||
}))
|
||||
envelope(
|
||||
read_arg(arg)
|
||||
.and_then(|a| rollback(required_str(&a, "repo")?, required_str(&a, "rollback_ref")?)),
|
||||
)
|
||||
}
|
||||
|
||||
/// smod_monitor({proposal}) → watch record
|
||||
|
||||
@@ -21,17 +21,23 @@ extern "C" fn mock_ops_cb(op_ptr: *const c_char) -> *const c_char {
|
||||
let op = pyjson::parse(op_json).unwrap();
|
||||
let next = OPS.with(|q| {
|
||||
let mut q = q.borrow_mut();
|
||||
assert!(!q.is_empty(), "kernel issued an op the oracle never did: {}", op_json);
|
||||
assert!(
|
||||
!q.is_empty(),
|
||||
"kernel issued an op the oracle never did: {}",
|
||||
op_json
|
||||
);
|
||||
q.remove(0)
|
||||
});
|
||||
assert_eq!(
|
||||
op.get("op"), next.get("op"),
|
||||
op.get("op"),
|
||||
next.get("op"),
|
||||
"op order divergence: kernel issued {}, truth expects {:?}",
|
||||
op_json,
|
||||
next.get("op")
|
||||
);
|
||||
assert_eq!(
|
||||
op.get("args"), next.get("args"),
|
||||
op.get("args"),
|
||||
next.get("args"),
|
||||
"op args divergence on op {:?}: kernel {}, truth {:?}",
|
||||
next.get("op"),
|
||||
op_json,
|
||||
@@ -85,22 +91,48 @@ fn setup_case(case: &Json, root: &std::path::Path) {
|
||||
fn run_kernel(case: &Json) -> Json {
|
||||
let inp = case.get("input").unwrap();
|
||||
let index = inp.get("index").cloned().unwrap_or(Json::Obj(Vec::new()));
|
||||
let fn_name = case.get("fn").and_then(|f| match f {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
}).unwrap();
|
||||
let fn_name = case
|
||||
.get("fn")
|
||||
.and_then(|f| match f {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
match fn_name {
|
||||
"_load_index" => load_index(
|
||||
inp.get("index_path").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
).unwrap(),
|
||||
inp.get("index_path")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
"__init__" => {
|
||||
let idx = init(
|
||||
inp.get("shard_dir").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
inp.get("index_path").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
).unwrap();
|
||||
inp.get("shard_dir")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
inp.get("index_path")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let dir_exists = std::path::Path::new(
|
||||
inp.get("shard_dir").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
).is_dir();
|
||||
inp.get("shard_dir")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.is_dir();
|
||||
Json::Obj(vec![
|
||||
("index".into(), idx),
|
||||
("dir_exists".into(), Json::Bool(dir_exists)),
|
||||
@@ -108,30 +140,67 @@ fn run_kernel(case: &Json) -> Json {
|
||||
}
|
||||
"create_shard" => {
|
||||
let (path, idx) = create_shard(
|
||||
inp.get("shard_dir").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
inp.get("index_path").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
inp.get("topic").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
inp.get("shard_dir")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
inp.get("index_path")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
inp.get("topic")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
&index,
|
||||
mock_ops_cb,
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
Json::Obj(vec![
|
||||
("shard_path".into(), Json::Str(path)),
|
||||
("index".into(), idx),
|
||||
])
|
||||
}
|
||||
"route_query" => Json::Str(route_query(
|
||||
inp.get("query").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
inp.get("query")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
&index,
|
||||
)),
|
||||
"merge_results" => merge_results(inp.get("results_list").unwrap()),
|
||||
"get_shard" => {
|
||||
let (path, idx) = get_shard(
|
||||
inp.get("shard_dir").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
inp.get("index_path").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
inp.get("topic").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
inp.get("shard_dir")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
inp.get("index_path")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
inp.get("topic")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
&index,
|
||||
mock_ops_cb,
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
Json::Obj(vec![
|
||||
("shard_path".into(), Json::Str(path)),
|
||||
("index".into(), idx),
|
||||
@@ -149,15 +218,19 @@ fn assert_case(case: &Json, result: &Json) {
|
||||
Some(Json::Str(s)) => s.as_str(),
|
||||
_ => "?",
|
||||
};
|
||||
let fn_name = case.get("fn").and_then(|f| match f {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
}).unwrap();
|
||||
let fn_name = case
|
||||
.get("fn")
|
||||
.and_then(|f| match f {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let expected = case.get("expected").unwrap();
|
||||
match fn_name {
|
||||
"create_shard" | "get_shard" => {
|
||||
assert_eq!(
|
||||
result.get("shard_path"), Some(expected),
|
||||
result.get("shard_path"),
|
||||
Some(expected),
|
||||
"case {}: shard_path mismatch",
|
||||
id
|
||||
);
|
||||
@@ -203,7 +276,12 @@ fn battery_replays_frozen_truth_verbatim() {
|
||||
assert!(
|
||||
remaining.is_empty(),
|
||||
"case {}: kernel issued fewer ops than the oracle ({} pending: {:?})",
|
||||
case.get("id").and_then(|v| match v { Json::Str(s) => Some(s.as_str()), _ => None }).unwrap(),
|
||||
case.get("id")
|
||||
.and_then(|v| match v {
|
||||
Json::Str(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap(),
|
||||
remaining.len(),
|
||||
remaining
|
||||
);
|
||||
|
||||
@@ -97,10 +97,7 @@ fn read_arg(ptr: *const c_char) -> Result<Json, SmError> {
|
||||
|
||||
fn envelope(r: Result<Json, SmError>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(true)),
|
||||
("value".into(), x),
|
||||
]),
|
||||
Ok(x) => Json::Obj(vec![("ok".into(), Json::Bool(true)), ("value".into(), x)]),
|
||||
Err(e) => Json::Obj(vec![
|
||||
("ok".into(), Json::Bool(false)),
|
||||
("err".into(), Json::Str(e.to_string())),
|
||||
@@ -206,9 +203,12 @@ pub fn create_shard(
|
||||
if std::path::Path::new(&shard_path).exists() {
|
||||
return Ok((shard_path, index.clone()));
|
||||
}
|
||||
try_op(cb, &OpKind::EnsureSchema {
|
||||
path: shard_path.clone(),
|
||||
})
|
||||
try_op(
|
||||
cb,
|
||||
&OpKind::EnsureSchema {
|
||||
path: shard_path.clone(),
|
||||
},
|
||||
)
|
||||
.ok_or(SmError::OpRejected(OP_ENSURE_SCHEMA))?;
|
||||
let mut idx = index.clone();
|
||||
index_set(&mut idx, topic, &shard_path);
|
||||
@@ -322,9 +322,12 @@ pub fn get_shard_stats(index: &Json, cb: OpsCb) -> Result<Json, SmError> {
|
||||
Json::Str(s) => s.as_str(),
|
||||
_ => "",
|
||||
};
|
||||
let count = try_op(cb, &OpKind::CountEntries {
|
||||
path: path.to_string(),
|
||||
})
|
||||
let count = try_op(
|
||||
cb,
|
||||
&OpKind::CountEntries {
|
||||
path: path.to_string(),
|
||||
},
|
||||
)
|
||||
.and_then(|v| match v {
|
||||
Json::Num(n) => Some(n as i64),
|
||||
_ => None,
|
||||
@@ -418,7 +421,10 @@ pub extern "C" fn sm_route_query(doc_ptr: *const c_char) -> *mut c_char {
|
||||
pub extern "C" fn sm_merge_results(doc_ptr: *const c_char) -> *mut c_char {
|
||||
let run = || -> Result<Json, SmError> {
|
||||
let doc = read_arg(doc_ptr)?;
|
||||
let results_list = doc.get("results_list").cloned().unwrap_or(Json::Arr(Vec::new()));
|
||||
let results_list = doc
|
||||
.get("results_list")
|
||||
.cloned()
|
||||
.unwrap_or(Json::Arr(Vec::new()));
|
||||
Ok(merge_results(&results_list))
|
||||
};
|
||||
envelope(run())
|
||||
|
||||
@@ -23,10 +23,7 @@ fn at<'a>(v: &'a Json, path: &[&str]) -> Option<&'a Json> {
|
||||
let mut cur = v;
|
||||
for k in path {
|
||||
cur = match cur {
|
||||
Json::Obj(pairs) => pairs
|
||||
.iter()
|
||||
.find(|(k2, _)| k2 == k)
|
||||
.map(|(_, x)| x)?,
|
||||
Json::Obj(pairs) => pairs.iter().find(|(k2, _)| k2 == k).map(|(_, x)| x)?,
|
||||
_ => return None,
|
||||
};
|
||||
}
|
||||
@@ -127,11 +124,7 @@ fn replay_default_step_check() {
|
||||
}
|
||||
let got = default_step_check(r.get("step").expect("step"));
|
||||
let want = crate::obj_ok_detail(&r);
|
||||
assert_case(
|
||||
&jstr(at(&c, &["name"])),
|
||||
&want,
|
||||
&got,
|
||||
);
|
||||
assert_case(&jstr(at(&c, &["name"])), &want, &got);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,14 @@ pub fn default_step_check(step: &Json) -> Json {
|
||||
/// must replay cleanly; vacuous-True when there are no results.
|
||||
pub fn sandbox_gate(skill: &Json, results: &[Json]) -> Json {
|
||||
let passed = results.iter().all(|r| truthy(r.get("ok")));
|
||||
Json::Obj(with_key(clone_pairs(skill), "sandbox", obj(vec![
|
||||
("passed", Json::Bool(passed)),
|
||||
("results", Json::Arr(results.to_vec())),
|
||||
])))
|
||||
Json::Obj(with_key(
|
||||
clone_pairs(skill),
|
||||
"sandbox",
|
||||
obj(vec![
|
||||
("passed", Json::Bool(passed)),
|
||||
("results", Json::Arr(results.to_vec())),
|
||||
]),
|
||||
))
|
||||
}
|
||||
|
||||
/// Phase B (language advantage): the promote gate is a typed verdict
|
||||
@@ -177,11 +181,9 @@ fn envelope(result: Result<Json, (String, String)>) -> *mut c_char {
|
||||
let out = pyjson::dumps(&body);
|
||||
match CString::new(out) {
|
||||
Ok(c) => c.into_raw(),
|
||||
Err(_) => CString::new(
|
||||
r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#,
|
||||
)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
Err(_) => CString::new(r#"{"ok":false,"err":"nul in output","type":"RuntimeError"}"#)
|
||||
.unwrap()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,8 +205,7 @@ macro_rules! ffi {
|
||||
($name:ident, $body:expr) => {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn $name(arg: *const c_char) -> *mut c_char {
|
||||
let out: Result<Json, (String, String)> =
|
||||
read_arg(arg).and_then(|raw| $body(&raw));
|
||||
let out: Result<Json, (String, String)> = read_arg(arg).and_then(|raw| $body(&raw));
|
||||
envelope(out)
|
||||
}
|
||||
};
|
||||
@@ -217,26 +218,35 @@ fn steps_of(doc: &Json, key: &str) -> Result<Vec<Json>, (String, String)> {
|
||||
}
|
||||
}
|
||||
|
||||
ffi!(sk_sequence_id, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
Ok(s(&sequence_id(&steps_of(&doc, "steps")?)))
|
||||
});
|
||||
ffi!(
|
||||
sk_sequence_id,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
Ok(s(&sequence_id(&steps_of(&doc, "steps")?)))
|
||||
}
|
||||
);
|
||||
|
||||
ffi!(sk_candidate_skill, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let steps = steps_of(&doc, "steps")?;
|
||||
let family = match doc.get("family") {
|
||||
Some(Json::Str(f)) => f.clone(),
|
||||
_ => return Err(("family must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let evidence = match doc.get("evidence") {
|
||||
None | Some(Json::Null) => None,
|
||||
Some(e) => Some(e),
|
||||
};
|
||||
Ok(candidate_skill(&steps, &family, evidence))
|
||||
});
|
||||
ffi!(
|
||||
sk_candidate_skill,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let steps = steps_of(&doc, "steps")?;
|
||||
let family = match doc.get("family") {
|
||||
Some(Json::Str(f)) => f.clone(),
|
||||
_ => return Err(("family must be a string".into(), "TypeError".into())),
|
||||
};
|
||||
let evidence = match doc.get("evidence") {
|
||||
None | Some(Json::Null) => None,
|
||||
Some(e) => Some(e),
|
||||
};
|
||||
Ok(candidate_skill(&steps, &family, evidence))
|
||||
}
|
||||
);
|
||||
|
||||
ffi!(sk_default_step_check, |raw: &str| -> Result<Json, (String, String)> {
|
||||
ffi!(sk_default_step_check, |raw: &str| -> Result<
|
||||
Json,
|
||||
(String, String),
|
||||
> {
|
||||
let doc = parse_doc(raw)?;
|
||||
match doc.get("step") {
|
||||
Some(step) => Ok(default_step_check(step)),
|
||||
@@ -244,15 +254,18 @@ ffi!(sk_default_step_check, |raw: &str| -> Result<Json, (String, String)> {
|
||||
}
|
||||
});
|
||||
|
||||
ffi!(sk_sandbox_gate, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let skill = match doc.get("skill") {
|
||||
Some(sk @ Json::Obj(_)) => sk.clone(),
|
||||
_ => return Err(("skill must be an object".into(), "TypeError".into())),
|
||||
};
|
||||
let results = steps_of(&doc, "results")?;
|
||||
Ok(sandbox_gate(&skill, &results))
|
||||
});
|
||||
ffi!(
|
||||
sk_sandbox_gate,
|
||||
|raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
let skill = match doc.get("skill") {
|
||||
Some(sk @ Json::Obj(_)) => sk.clone(),
|
||||
_ => return Err(("skill must be an object".into(), "TypeError".into())),
|
||||
};
|
||||
let results = steps_of(&doc, "results")?;
|
||||
Ok(sandbox_gate(&skill, &results))
|
||||
}
|
||||
);
|
||||
|
||||
ffi!(sk_promote, |raw: &str| -> Result<Json, (String, String)> {
|
||||
let doc = parse_doc(raw)?;
|
||||
|
||||
@@ -58,11 +58,7 @@ fn materialize(files: &Json, root: &Path) {
|
||||
}
|
||||
|
||||
fn sandbox_dir(idx: usize) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!(
|
||||
"spine-gap-battery-{}-{}",
|
||||
std::process::id(),
|
||||
idx
|
||||
));
|
||||
let d = std::env::temp_dir().join(format!("spine-gap-battery-{}-{}", std::process::id(), idx));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
fs::create_dir_all(&d).expect("sandbox mkdir");
|
||||
d
|
||||
@@ -109,12 +105,7 @@ mod truth_battery {
|
||||
let got = replay_harness(case)
|
||||
.unwrap_or_else(|e| panic!("{} dispatch failed: {}", fname, e));
|
||||
let expect = expect_field(case, "expected");
|
||||
assert_eq!(
|
||||
canon(&got),
|
||||
canon(expect),
|
||||
"case {} parity (canon)",
|
||||
fname
|
||||
);
|
||||
assert_eq!(canon(&got), canon(expect), "case {} parity (canon)", fname);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
@@ -166,23 +157,26 @@ mod truth_battery {
|
||||
None => Json::Null,
|
||||
})
|
||||
}
|
||||
"_py_project_meta_surfaces" => gap::_py_project_meta_surfaces(
|
||||
&args[0],
|
||||
&args[1],
|
||||
&stamp,
|
||||
)
|
||||
.map(|out| {
|
||||
// the frozen expected carries the oracle's return
|
||||
// (the count int); post_doc carries the mutated doc
|
||||
Json::Obj(vec![
|
||||
("result".into(), out.get("count").cloned().unwrap_or(Json::Null)),
|
||||
("doc".into(), out.get("doc").cloned().unwrap_or(Json::Null)),
|
||||
])
|
||||
}),
|
||||
"_py_project_meta_surfaces" => {
|
||||
gap::_py_project_meta_surfaces(&args[0], &args[1], &stamp).map(|out| {
|
||||
// the frozen expected carries the oracle's return
|
||||
// (the count int); post_doc carries the mutated doc
|
||||
Json::Obj(vec![
|
||||
(
|
||||
"result".into(),
|
||||
out.get("count").cloned().unwrap_or(Json::Null),
|
||||
),
|
||||
("doc".into(), out.get("doc").cloned().unwrap_or(Json::Null)),
|
||||
])
|
||||
})
|
||||
}
|
||||
"_py_ingest_umbrellas_data" => {
|
||||
gap::_py_ingest_umbrellas_data(&args[0], &args[1], &stamp).map(|out| {
|
||||
Json::Obj(vec![
|
||||
("result".into(), out.get("count").cloned().unwrap_or(Json::Null)),
|
||||
(
|
||||
"result".into(),
|
||||
out.get("count").cloned().unwrap_or(Json::Null),
|
||||
),
|
||||
("doc".into(), out.get("doc").cloned().unwrap_or(Json::Null)),
|
||||
])
|
||||
})
|
||||
@@ -200,19 +194,14 @@ mod truth_battery {
|
||||
// the Python-side gathers the generator patched in
|
||||
// are case fixtures (the registry/graph seam):
|
||||
let registry = vec!["canary_alpha".to_string(), "canary_beta".to_string()];
|
||||
let graph = parse(
|
||||
r#"{"alpha": ["json", "rune"], "beta": ["sys"]}"#,
|
||||
);
|
||||
let graph = parse(r#"{"alpha": ["json", "rune"], "beta": ["sys"]}"#);
|
||||
// the oracle's _test_backed_modules strips the
|
||||
// test_ prefix (p.stem[5:])
|
||||
let mut tested: Vec<String> = file_stems_under(
|
||||
&root.join("tests"),
|
||||
"test_",
|
||||
".py",
|
||||
)
|
||||
.into_iter()
|
||||
.map(|stem| stem["test_".len()..].to_string())
|
||||
.collect();
|
||||
let mut tested: Vec<String> =
|
||||
file_stems_under(&root.join("tests"), "test_", ".py")
|
||||
.into_iter()
|
||||
.map(|stem| stem["test_".len()..].to_string())
|
||||
.collect();
|
||||
tested.sort();
|
||||
let src_stems = readdir_stems(&root.join("src/rune"), ".py");
|
||||
gap::_gather_drift_inputs(
|
||||
@@ -224,10 +213,10 @@ mod truth_battery {
|
||||
&Json::Null,
|
||||
)
|
||||
}
|
||||
"_py_drift_from_inputs" => gap::_py_drift_from_inputs(&args[0], &args[1], false),
|
||||
"_py_export_umbrellas" => {
|
||||
gap::_py_export_umbrellas(&args[0], &stamp, false)
|
||||
"_py_drift_from_inputs" => {
|
||||
gap::_py_drift_from_inputs(&args[0], &args[1], false)
|
||||
}
|
||||
"_py_export_umbrellas" => gap::_py_export_umbrellas(&args[0], &stamp, false),
|
||||
"_py_query" => gap::_py_query(&args[0], store::as_str(&args[1]), false),
|
||||
"_fresh_doc" => gap::_fresh_doc(&root, None),
|
||||
other => panic!("unknown fn in clocked truth: {}", other),
|
||||
@@ -270,9 +259,7 @@ mod truth_battery {
|
||||
}
|
||||
}
|
||||
(Err(err), Some(err_doc)) => {
|
||||
let msg = store::as_str(
|
||||
err_doc.get("message").expect("error.message"),
|
||||
);
|
||||
let msg = store::as_str(err_doc.get("message").expect("error.message"));
|
||||
assert_eq!(
|
||||
err,
|
||||
strip_loader_prefix(msg),
|
||||
@@ -281,10 +268,9 @@ mod truth_battery {
|
||||
name
|
||||
);
|
||||
}
|
||||
(other, _) => panic!(
|
||||
"case {}/{}: unexpected outcome: {:?}",
|
||||
fname, name, other
|
||||
),
|
||||
(other, _) => {
|
||||
panic!("case {}/{}: unexpected outcome: {:?}", fname, name, other)
|
||||
}
|
||||
}
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
n += 1;
|
||||
|
||||
+33
-20
@@ -51,7 +51,8 @@ use rune_spine::facade;
|
||||
use rune_spine::pyjson::{self, Json};
|
||||
use rune_spine::store;
|
||||
|
||||
pub const KERNEL_NOT_BUILT: &str = "rune_spine kernel not built — run the rune_spine build; the Python oracle was retired";
|
||||
pub const KERNEL_NOT_BUILT: &str =
|
||||
"rune_spine kernel not built — run the rune_spine build; the Python oracle was retired";
|
||||
|
||||
fn s(text: &str) -> Json {
|
||||
Json::Str(text.to_string())
|
||||
@@ -300,17 +301,18 @@ pub fn _module_imports(module: &str, src_text: Option<&str>) -> Result<Vec<Strin
|
||||
i += 1;
|
||||
}
|
||||
let parsed: Result<(), String> = if i < chars.len() && is_ident_start(chars[i]) {
|
||||
parse_dotted(&chars, i).and_then(|(module_path, next)| {
|
||||
expect_keyword(&chars, next, "import").map(|_| module_path)
|
||||
})
|
||||
.map(|module_path| {
|
||||
// `from x.y import ...` → node.module = "x.y" → "x";
|
||||
// `from .x import ...` → node.module = "x" → "x"
|
||||
let top = module_path.split('.').next().unwrap_or("").to_string();
|
||||
if !names.contains(&top) {
|
||||
names.push(top);
|
||||
}
|
||||
})
|
||||
parse_dotted(&chars, i)
|
||||
.and_then(|(module_path, next)| {
|
||||
expect_keyword(&chars, next, "import").map(|_| module_path)
|
||||
})
|
||||
.map(|module_path| {
|
||||
// `from x.y import ...` → node.module = "x.y" → "x";
|
||||
// `from .x import ...` → node.module = "x" → "x"
|
||||
let top = module_path.split('.').next().unwrap_or("").to_string();
|
||||
if !names.contains(&top) {
|
||||
names.push(top);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// `from . import x` → node.module = None → skipped by the
|
||||
// oracle; still validate the import keyword + names
|
||||
@@ -337,7 +339,10 @@ fn read_manifest(root: &Path) -> Option<Json> {
|
||||
|
||||
fn dir_entry_names(dir: &Path) -> Vec<String> {
|
||||
let mut names: Vec<String> = match fs::read_dir(dir) {
|
||||
Ok(rd) => rd.filter_map(|e| e.ok()).map(|e| e.file_name().to_string_lossy().into_owned()).collect(),
|
||||
Ok(rd) => rd
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.collect(),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
names.sort();
|
||||
@@ -345,9 +350,7 @@ fn dir_entry_names(dir: &Path) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn glob_match(name: &str, prefix: &str, suffix: &str) -> bool {
|
||||
name.len() >= prefix.len() + suffix.len()
|
||||
&& name.starts_with(prefix)
|
||||
&& name.ends_with(suffix)
|
||||
name.len() >= prefix.len() + suffix.len() && name.starts_with(prefix) && name.ends_with(suffix)
|
||||
}
|
||||
|
||||
/// The spine._gather_meta_inputs core. `workflows_dir` defaults to
|
||||
@@ -460,7 +463,7 @@ pub fn _gather_meta_inputs(root: &Path, workflows_dir: Option<&Path>) -> Result<
|
||||
pub fn _deployed_source_files(root: &Path) -> Vec<String> {
|
||||
let mut out: Vec<String> = vec![];
|
||||
let bases = [
|
||||
(root.join("src").join("rune"), "*.py" , false),
|
||||
(root.join("src").join("rune"), "*.py", false),
|
||||
(root.join("src").join("rust"), "*/canary_*.py", true),
|
||||
];
|
||||
for (base, pattern, nested) in bases {
|
||||
@@ -508,7 +511,10 @@ fn readdir_names(dir: &Path) -> Vec<String> {
|
||||
}
|
||||
|
||||
fn rel_posix(root: &Path, p: &Path) -> String {
|
||||
p.strip_prefix(root).unwrap_or(p).to_string_lossy().replace('\\', "/")
|
||||
p.strip_prefix(root)
|
||||
.unwrap_or(p)
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
}
|
||||
|
||||
// ── _gather_drift_inputs — assembly + pyproject version extraction ────────
|
||||
@@ -529,7 +535,10 @@ pub fn _gather_drift_inputs(
|
||||
let mut sorted_graph: Vec<(String, Json)> = vec![];
|
||||
if let Some(Json::Obj(pairs)) = Some(graph) {
|
||||
for (m, deps) in pairs {
|
||||
let mut list: Vec<String> = store::as_arr(deps).iter().map(|x| store::as_str(x).to_string()).collect();
|
||||
let mut list: Vec<String> = store::as_arr(deps)
|
||||
.iter()
|
||||
.map(|x| store::as_str(x).to_string())
|
||||
.collect();
|
||||
list.sort();
|
||||
sorted_graph.push((m.clone(), str_arr(&list)));
|
||||
}
|
||||
@@ -682,7 +691,11 @@ pub fn _py_build_from_inputs(inputs: &Json, core_available: bool) -> Result<Json
|
||||
rune_spine::project::project(inputs)
|
||||
}
|
||||
|
||||
pub fn _py_drift_from_inputs(doc: &Json, inputs: &Json, core_available: bool) -> Result<Json, String> {
|
||||
pub fn _py_drift_from_inputs(
|
||||
doc: &Json,
|
||||
inputs: &Json,
|
||||
core_available: bool,
|
||||
) -> Result<Json, String> {
|
||||
if !core_available {
|
||||
return Err(KERNEL_NOT_BUILT.to_string());
|
||||
}
|
||||
|
||||
+12
-13
@@ -63,10 +63,13 @@ fn root_arg(v: &Json, key: &str) -> Result<std::path::PathBuf, String> {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn sp_module_imports(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let module = a.get("module").and_then(|x| match x {
|
||||
Json::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
}).ok_or("arg.module required")?;
|
||||
let module = a
|
||||
.get("module")
|
||||
.and_then(|x| match x {
|
||||
Json::Str(s) => Some(s.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or("arg.module required")?;
|
||||
let src_text = match a.get("src_text") {
|
||||
Some(Json::Str(t)) => Some(t.clone()),
|
||||
_ => None,
|
||||
@@ -97,7 +100,10 @@ pub extern "C" fn sp_deployed_source_files(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| {
|
||||
let root = root_arg(&a, "root")?;
|
||||
Ok(Json::Arr(
|
||||
gap::_deployed_source_files(&root).into_iter().map(Json::Str).collect(),
|
||||
gap::_deployed_source_files(&root)
|
||||
.into_iter()
|
||||
.map(Json::Str)
|
||||
.collect(),
|
||||
))
|
||||
});
|
||||
envelope(r)
|
||||
@@ -147,14 +153,7 @@ pub extern "C" fn sp_gather_drift_inputs(arg: *const c_char) -> *mut c_char {
|
||||
})
|
||||
.ok_or("arg.src_stems required")?;
|
||||
let deployed_obs = a.get("deployed_obs").cloned().unwrap_or(Json::Null);
|
||||
gap::_gather_drift_inputs(
|
||||
&root,
|
||||
®istry,
|
||||
&graph,
|
||||
&tested,
|
||||
&src_stems,
|
||||
&deployed_obs,
|
||||
)
|
||||
gap::_gather_drift_inputs(&root, ®istry, &graph, &tested, &src_stems, &deployed_obs)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,8 @@ fn frozen_truth_replays_verbatim() {
|
||||
Some(Json::Str(s)) => s.as_str().to_string(),
|
||||
_ => format!("case-{i}"),
|
||||
};
|
||||
let expected = spine::mask_observed_at(&case.get("expected").cloned().unwrap_or(Json::Null));
|
||||
let expected =
|
||||
spine::mask_observed_at(&case.get("expected").cloned().unwrap_or(Json::Null));
|
||||
match run_case(&fn_name, case) {
|
||||
Ok(actual) => {
|
||||
if !jeq(&expected, &actual) {
|
||||
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user