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
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:
1 parent
4d0883b39c
commit
00bf4d210c
1 file changed
+185
-99
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user