refactor(abstraction_ladder): Phase B language-advantage pass — dichotomy wave
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / rust-surface (push) Failing after 13s
CI Pipeline / go-surface (push) Failing after 16s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / lint (push) Canceled after 25s
CI Pipeline / canary (push) Canceled after 25s
CI Pipeline / unit-tests (push) Canceled after 25s
CI Pipeline / gitea-offline (push) Canceled after 11s
deploy / deploy (push) Canceled after 29s
Auto-version and Release / release (push) Canceled after 31s
CI Pipeline / type-check (push) Canceled after 15s

This commit is contained in:
Nick committed 2026-10-12 13:04:29 +01:00
1 parent 4d0883b39c
commit 00bf4d210c
1 file changed
+185 -99
+185 -99
View File
@@ -1,14 +1,21 @@
//! ladder.rs — the promotion ladder kernel, a verbatim port of
//! src/rune/abstraction_ladder.py (dichotomy wave #2479 tranche).
//! src/rune/abstraction_ladder.py (dichotomy wave #2479 tranche),
//! Phase B language-advantage pass.
//!
//! episode → fact → observation → concept → belief → rule → skill.
//! Each promotion needs: evidence threshold, provenance, contradiction
//! handling, prediction/behavioural consequence, outcome-based revision.
//! Schema alone does not count — the ladder moves items or it doesn't.
//!
//! Phase B shapes (vs the parity commit): Stage enum with exhaustive
//! matches everywhere a rung is decided, a typed Verdict, an
//! iterator-driven falsy/truthy gate chain, zero-copy borrows from the
//! parsed arg straight through to the envelope.
//!
//! PYTHON-MIRRORING LAWS:
//! * unknown / non-string stage → next_stage None → can_promote says
//! "already at the top (skill)" (the oracle's ValueError collapse).
//! * any stage value that is not a ladder rung (unknown string,
//! non-string, null) → STAGES.index ValueError → next_stage None →
//! can_promote says "already at the top (skill)".
//! * missing stage key defaults to "episode"; missing sightings to 0.
//! * provenance / contradictions / consequence / verified_outcome use
//! Python truthiness (empty string/list/object, 0, null, false all
@@ -19,30 +26,66 @@
use capability_scoring::pyjson::Json;
pub const STAGES: [&str; 7] = [
"episode", "fact", "observation", "concept", "belief", "rule", "skill",
];
/// Evidence threshold per promotion (sightings required). The 1.0
/// default mirrors THRESHOLDS.get(stage, 1).
fn threshold(stage: &str) -> f64 {
match stage {
"episode" => 1.0,
"fact" => 2.0,
"observation" => 3.0,
"concept" => 3.0,
"belief" => 4.0,
"rule" => 4.0,
"skill" => 5.0,
_ => 1.0,
}
/// One rung of the promotion ladder, in causal order.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
Episode,
Fact,
Observation,
Concept,
Belief,
Rule,
Skill,
}
/// The stage after this one (None at the top). A non-string stage
/// mirrors the oracle's STAGES.index ValueError → None.
pub fn next_stage(stage: Option<&str>) -> Option<&'static str> {
let i = STAGES.iter().position(|s| Some(*s) == stage)?;
STAGES.get(i + 1).copied()
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 fn name(self) -> &'static str {
match self {
Episode => "episode",
Fact => "fact",
Observation => "observation",
Concept => "concept",
Belief => "belief",
Rule => "rule",
Skill => "skill",
}
}
/// The oracle's STAGES.index — the only way into the ladder
/// (unknown names fall off, mirroring the ValueError → None law).
pub fn parse(name: &str) -> Option<Stage> {
Self::LADDER.iter().copied().find(|s| s.name() == name)
}
/// The stage after this one (None at the top).
pub fn next(self) -> Option<Stage> {
let idx = Self::LADDER.iter().position(|s| *s == self)?;
Self::LADDER.get(idx + 1).copied()
}
/// Evidence threshold per promotion (sightings required) — the
/// oracle's THRESHOLDS table, exhaustive over the enum.
pub fn threshold(self) -> f64 {
match self {
Episode => 1.0,
Fact => 2.0,
Observation | Concept => 3.0,
Belief | Rule => 4.0,
Skill => 5.0,
}
}
/// belief+ carries an outcome-revision obligation.
fn needs_verified_outcome(self) -> bool {
matches!(self, Belief | Rule)
}
}
/// Python truthiness for JSON values (bools are ints; 0/0.0, "", [],
@@ -69,42 +112,98 @@ fn fmt_num(x: f64) -> String {
}
}
fn stage_of(item: &Json) -> Result<&str, ()> {
/// The item's stage as the oracle sees it. A missing key defaults to
/// "episode"; anything that is not a ladder rung (unknown string,
/// non-string value) takes the STAGES.index ValueError path.
enum StageInput {
Rung(Stage),
Default,
OffLadder,
}
fn stage_input(item: &Json) -> StageInput {
match item.get("stage") {
// missing key → the oracle's item.get("stage", "episode") default
None => Ok("episode"),
Some(Json::Str(s)) => Ok(s.as_str()),
// present but non-string → STAGES.index raises → "top" quirk
Some(_) => Err(()),
None => StageInput::Default,
Some(Json::Str(s)) => Stage::parse(s).map_or(StageInput::OffLadder, StageInput::Rung),
Some(_) => StageInput::OffLadder,
}
}
fn verdict_top() -> Json {
Json::Obj(vec![(
"promote".into(),
Json::Bool(false),
), (
"reason".into(),
Json::Str("already at the top (skill)".into()),
)])
/// A promotion verdict — the oracle's verdict doc, typed.
struct Verdict {
promote: bool,
to: Option<Stage>,
reason: String,
}
fn verdict_refuse(reason: String) -> Json {
Json::Obj(vec![
("promote".into(), Json::Bool(false)),
("reason".into(), Json::Str(reason)),
])
impl Verdict {
fn refuse(reason: impl Into<String>) -> Verdict {
Verdict { promote: false, to: None, reason: reason.into() }
}
fn grant(to: Stage) -> Verdict {
Verdict {
promote: true,
to: Some(to),
reason: format!("threshold met, promoting to {}", to.name()),
}
}
fn to_pairs(&self) -> Vec<(String, Json)> {
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())));
}
pairs.push(("reason".into(), Json::Str(self.reason.clone())));
pairs
}
fn to_json(&self) -> Json {
Json::Obj(self.to_pairs())
}
}
fn verdict_top() -> Verdict {
Verdict::refuse("already at the top (skill)")
}
/// Which truthiness a promotion gate tests, mirroring the oracle's
/// `if not X` / `if X` asymmetry.
enum Gate {
Falsy,
Truthy,
}
impl Gate {
fn blocks(&self, present: bool) -> bool {
match self {
Gate::Falsy => !present,
Gate::Truthy => present,
}
}
}
/// 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)
}
/// Can this item promote? Needs: evidence threshold met, provenance
/// present, no open contradictions, a stated consequence, and (for
/// belief+) a verified outcome.
pub fn can_promote(item: &Json) -> Json {
let stage = match stage_of(item) {
Ok(s) => s,
Err(()) => return verdict_top(),
verdict(item).to_json()
}
fn verdict(item: &Json) -> Verdict {
let stage = match stage_input(item) {
StageInput::Rung(s) => s,
StageInput::Default => Episode,
StageInput::OffLadder => return verdict_top(),
};
let nxt = match next_stage(Some(stage)) {
let nxt = match stage.next() {
Some(n) => n,
None => return verdict_top(),
};
@@ -112,36 +211,31 @@ pub fn can_promote(item: &Json) -> Json {
Some(Json::Num(x)) => *x,
_ => 0.0,
};
let thr = threshold(stage);
let thr = stage.threshold();
if sightings < thr {
return verdict_refuse(format!(
return Verdict::refuse(format!(
"evidence {} < {}",
fmt_num(sightings),
fmt_num(thr)
));
}
if !item.get("provenance").map(truthy).unwrap_or(false) {
return verdict_refuse("no provenance".into());
let gates = [
("provenance", Gate::Falsy, "no provenance"),
("contradictions", Gate::Truthy, "open contradictions"),
("consequence", Gate::Falsy, "no stated consequence"),
];
if let Some(reason) = gates.iter().find_map(|(key, gate, reason)| {
let present = item.get(key).map(truthy).unwrap_or(false);
gate.blocks(present).then_some(*reason)
}) {
return Verdict::refuse(reason);
}
if item.get("contradictions").map(truthy).unwrap_or(false) {
return verdict_refuse("open contradictions".into());
}
if !item.get("consequence").map(truthy).unwrap_or(false) {
return verdict_refuse("no stated consequence".into());
}
if (stage == "belief" || stage == "rule")
if stage.needs_verified_outcome()
&& !item.get("verified_outcome").map(truthy).unwrap_or(false)
{
return verdict_refuse("belief+ requires a verified outcome".into());
return Verdict::refuse("belief+ requires a verified outcome");
}
Json::Obj(vec![
("promote".into(), Json::Bool(true)),
("to".into(), Json::Str(nxt.into())),
(
"reason".into(),
Json::Str(format!("threshold met, promoting to {}", nxt)),
),
])
Verdict::grant(nxt)
}
/// dict(item); item[k] = v — an existing key updates IN PLACE (keeping
@@ -149,17 +243,11 @@ pub fn can_promote(item: &Json) -> Json {
fn set_key(item: &Json, key: &str, val: Json) -> Json {
match item {
Json::Obj(pairs) => {
let mut out: Vec<(String, Json)> = Vec::with_capacity(pairs.len() + 1);
let mut placed = false;
for (k, v) in pairs {
if k == key {
out.push((k.clone(), val.clone()));
placed = true;
} else {
out.push((k.clone(), v.clone()));
}
}
if !placed {
let mut out: Vec<(String, Json)> = pairs
.iter()
.map(|(k, v)| (k.clone(), if k == key { val.clone() } else { v.clone() }))
.collect();
if !pairs.iter().any(|(k, _)| k == key) {
out.push((key.to_string(), val));
}
Json::Obj(out)
@@ -168,26 +256,24 @@ fn set_key(item: &Json, key: &str, val: Json) -> Json {
}
}
fn with_item(mut pairs: Vec<(String, Json)>, item: Json) -> Json {
let mut out: Vec<(String, Json)> = Vec::with_capacity(pairs.len() + 1);
out.push(("item".into(), item));
out.append(&mut pairs);
Json::Obj(out)
}
/// Promote one stage (or return the refusal).
pub fn promote(item: &Json) -> Json {
let verdict = can_promote(item);
let promoted = verdict.get("promote").map(|v| matches!(v, Json::Bool(true)));
match promoted {
Some(true) => {
let to = verdict.get("to").cloned().unwrap_or(Json::Null);
let new_item = set_key(&set_key(item, "stage", to), "sightings", Json::Num(0.0));
let mut out: Vec<(String, Json)> = vec![("item".into(), new_item)];
if let Json::Obj(pairs) = verdict {
out.extend(pairs);
}
Json::Obj(out)
}
_ => {
let mut out: Vec<(String, Json)> = vec![("item".into(), item.clone())];
if let Json::Obj(pairs) = verdict {
out.extend(pairs);
}
Json::Obj(out)
}
}
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);
set_key(&set_key(item, "stage", to), "sightings", Json::Num(0.0))
} else {
item.clone()
};
with_item(v.to_pairs(), out_item)
}