Skip to content

Commit efee87f

Browse files
sam-lippertclaude
andcommitted
Wire rekey + rules + rep into op_compile_model; reconcile at exact parity (#20)
The rules-wiring slice (subagent a69f7ab9, spec sections 1+2): rekey_transitions_native (whole port — python's body is pure host logic, no canon def exists; identity's 20 transitions surrogate-key matching row-for-row), the post-model rules fixpoint through the existing op_run_rules with save/swap/restore purity and a folded_any identity- skip (probe verified byte-equal), and the rep contract (kinds {} verbatim, unparsed, rule_diagnostics post-rules). Plus the two reconcile-parity fixes its differential isolated: - the reassembly's head filter now mirrors python's touched ∩ DERIVED_HEADS ∩ absorbed (engine.py:1523) — a base-populated absorbed ft must not have its column reassembled from its pop (visible on pre-layout stores as columns python leaves holed); - the reassembly writes through setcell_into, python setcell's twin (:1165): replace IN PLACE, append new at the END — store_into's remove+prepend re-topped the written cells and reordered the dump; - op_compile_model harvests the fixpoint from the TRUE store (srv.d, raw_cells_of) — the index Vec appends new-in-rules heads at the end where python's Store prepends them. Acceptance: ALL TEN corpora byte-identical through the full post-rules boundary (store) and json-equal (rep), including core.md and identity- atop-the-resident-base — the two the reassembly gap had held back. core.md end-to-end release (agent measurement, pre-parity-fixes boundary): 7.37s including spawn, thaw, classify, fold, rekey, rules. UNSIGNED (sg-1 signing freeze): batch-resign before any push. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018GmWpnRDQecYKTbhztMgZb
1 parent 32c6f90 commit efee87f

1 file changed

Lines changed: 275 additions & 5 deletions

File tree

engine/rust/src/main.rs

Lines changed: 275 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3084,6 +3084,59 @@ fn store_into(
30843084
}
30853085
}
30863086

3087+
// setcell_into is engine.py _reconcile_absorbed_heads' setcell (:1165): the
3088+
// reassembly's write REPLACES IN PLACE when the cell exists and APPENDS AT
3089+
// THE END when it doesn't — Store's remove+prepend would re-top the written
3090+
// cells and reorder the dump against python (order forensics, 2026-07-11).
3091+
fn setcell_into(
3092+
d: &mut V,
3093+
cells: &mut Vec<(Leaf, V)>,
3094+
nd: &mut N,
3095+
ncells: &mut Vec<(Leaf, N)>,
3096+
name: &Leaf,
3097+
contents: V,
3098+
) {
3099+
let ncontents = v_to_n(&contents);
3100+
let cellv = seq(from_vec(vec![
3101+
atom(Leaf::S("CELL".into())),
3102+
atom(name.clone()),
3103+
contents.clone(),
3104+
]));
3105+
let mut entries = items(&list_of(d));
3106+
match entries.iter().position(|c| {
3107+
let it = items(&list_of(c));
3108+
it.len() >= 2 && matches!(aval(&it[1]), Some(k) if k.nateq(name))
3109+
}) {
3110+
Some(p) => entries[p] = cellv,
3111+
None => entries.push(cellv),
3112+
}
3113+
*d = seq(from_vec(entries));
3114+
match cells.iter_mut().find(|(k, _)| k.nateq(name)) {
3115+
Some(slot) => slot.1 = contents,
3116+
None => cells.push((name.clone(), contents)),
3117+
}
3118+
let ncellv = N::S(Rc::new(vec![
3119+
N::A(Rc::new(Leaf::S("CELL".into()))),
3120+
N::A(Rc::new(name.clone())),
3121+
ncontents.clone(),
3122+
]));
3123+
let mut nentries: Vec<N> = match nd {
3124+
N::S(v) => v.to_vec(),
3125+
_ => Vec::new(),
3126+
};
3127+
match nentries.iter().position(|c| {
3128+
matches!(c, N::S(it) if it.len() >= 2 && matches!(&it[1], N::A(k) if k.nateq(name)))
3129+
}) {
3130+
Some(p) => nentries[p] = ncellv,
3131+
None => nentries.push(ncellv),
3132+
}
3133+
*nd = N::S(Rc::new(nentries));
3134+
match ncells.iter_mut().find(|(k, _)| k.nateq(name)) {
3135+
Some(slot) => slot.1 = ncontents,
3136+
None => ncells.push((name.clone(), ncontents)),
3137+
}
3138+
}
3139+
30873140
// eval_rules is engine.py's _eval_rules (line 1188): the UNION of the given
30883141
// rules' outputs over the store, deduplicated by row key in first-seen order
30893142
// and keeping only sequence rows (Python keeps only tuples). It is the shared
@@ -4044,7 +4097,21 @@ fn op_run_rules(j: &J, srv: &mut Srv) -> Result<String, String> {
40444097
_ => None,
40454098
};
40464099
let hole = || atom(Leaf::S("#".to_string()));
4100+
// python's reconcile filter (engine.py:1523): touched ∩
4101+
// DERIVED_HEADS ∩ absorbed — a base-populated absorbed ft that
4102+
// changed in the round must NOT have its column reassembled
4103+
// from its pop here (its storage is the routed write's, not the
4104+
// derive cache's; visible on pre-layout stores where the extra
4105+
// write fills columns python leaves holed)
4106+
let derived: HashSet<String> = rules
4107+
.iter()
4108+
.chain(agg_rules.iter())
4109+
.map(|rr| leaf_text(&rr.head))
4110+
.collect();
40474111
for ftname in changed.iter() {
4112+
if !derived.contains(ftname) {
4113+
continue;
4114+
}
40484115
let (table, col) = match layout.get(ftname) {
40494116
Some((t, c)) => (t.clone(), *c),
40504117
None => continue,
@@ -4100,7 +4167,7 @@ fn op_run_rules(j: &J, srv: &mut Srv) -> Result<String, String> {
41004167
}
41014168
if !eqobj(&row[col - 1], &v) {
41024169
row[col - 1] = v;
4103-
store_into(&mut d, &mut cells, &mut nd, &mut ncells, &rc,
4170+
setcell_into(&mut d, &mut cells, &mut nd, &mut ncells, &rc,
41044171
seq(from_vec(row)));
41054172
}
41064173
}
@@ -4130,12 +4197,12 @@ fn op_run_rules(j: &J, srv: &mut Srv) -> Result<String, String> {
41304197
row.push(hole());
41314198
}
41324199
row[col - 1] = v;
4133-
store_into(&mut d, &mut cells, &mut nd, &mut ncells, &rc,
4200+
setcell_into(&mut d, &mut cells, &mut nd, &mut ncells, &rc,
41344201
seq(from_vec(row)));
41354202
tbl.push(seq(from_vec(vec![ka])));
41364203
}
41374204
if grew {
4138-
store_into(&mut d, &mut cells, &mut nd, &mut ncells, &tleaf,
4205+
setcell_into(&mut d, &mut cells, &mut nd, &mut ncells, &tleaf,
41394206
seq(from_vec(tbl)));
41404207
}
41414208
}
@@ -5690,6 +5757,131 @@ fn fold_fire(fire: &cooks::Fire, cells: &mut Vec<(Leaf, V)>) -> Result<(), Strin
56905757
Ok(())
56915758
}
56925759

5760+
// ======================= rekey_transitions (#20, native pipeline tail slice 1) ===
5761+
// engine.py:1608 rekey_transitions, ported whole (NOT canon-backed: its body is
5762+
// pure host list/dict manipulation over from_lam(D), no _apply/reduce call
5763+
// anywhere in it — confirmed against every DEF in shared/*.canon; none is named
5764+
// or shaped for this). Machine-scope each Transition's IDENTITY: Core.png/
5765+
// GraphDL model Transition(.id) as a SURROGATE, not the readings' name, so a
5766+
// base-vs-app reuse of a transition NAME must not merge one entity carrying
5767+
// two machines' froms/tos. Runs PER COMPILE PASS (the base is rekeyed first,
5768+
// frozen; an app compiling atop it via context_from:"resident" sees the
5769+
// base's transitions ALREADY surrogate-keyed and skips them, so the name->SMD
5770+
// map stays unambiguous per pass): a bare-named transition gets the surrogate
5771+
// "txn:{SMD}\x1f{name}" keyed by its defined-in SMD; rows already
5772+
// surrogate-keyed are skipped. Rewrites EVERY Transition-typed position — the
5773+
// role metamodel's declared referencing fact types PLUS the hardcoded
5774+
// machinery cells (smFrom/smTo/smTrigger/smGuard/smEmit/smMoore) and
5775+
// Guard_prevents_Transition — so no reference dangles. A bare name mapping to
5776+
// more than one SMD in one pass (genuinely ambiguous) is left as-is, never a
5777+
// partial rekey.
5778+
const TXN_SUR: &str = "txn:";
5779+
5780+
fn rekey_transitions_native(cells: &mut Vec<(Leaf, V)>) {
5781+
use std::collections::HashSet;
5782+
let leaf = |s: &str| Leaf::S(s.to_string());
5783+
// name_smd: bare transition-name key -> (name V, smd V), built from
5784+
// Transition_is_defined_in_State_Machine_Definition rows; a name seen
5785+
// with two DIFFERENT smd values anywhere in the population is ambiguous
5786+
// and excluded whole (python's unconditional overwrite-then-pop)
5787+
let mut name_smd: HashMap<String, (V, V)> = HashMap::new();
5788+
let mut ambiguous: HashSet<String> = HashSet::new();
5789+
for r in pop_rows(cells, &leaf("Transition_is_defined_in_State_Machine_Definition")) {
5790+
let it = items(&list_of(&r));
5791+
if it.len() >= 2 {
5792+
let already_sur = match aval(&it[0]) {
5793+
Some(l) => leaf_text(&l).starts_with(TXN_SUR),
5794+
None => false,
5795+
};
5796+
if !already_sur {
5797+
let k = key_of(&it[0]);
5798+
if let Some((_, existing_smd)) = name_smd.get(&k) {
5799+
if !eqobj(existing_smd, &it[1]) {
5800+
ambiguous.insert(k.clone());
5801+
}
5802+
}
5803+
name_smd.insert(k, (it[0].clone(), it[1].clone()));
5804+
}
5805+
}
5806+
}
5807+
for k in &ambiguous {
5808+
name_smd.remove(k);
5809+
}
5810+
if name_smd.is_empty() {
5811+
return;
5812+
}
5813+
// surro: bare-name key -> the surrogate atom, ready to substitute in place
5814+
let mut surro: HashMap<String, V> = HashMap::new();
5815+
for (k, (nm, smd)) in &name_smd {
5816+
if let (Some(nl), Some(sl)) = (aval(nm), aval(smd)) {
5817+
let sur = format!("{}{}\x1f{}", TXN_SUR, leaf_text(&sl), leaf_text(&nl));
5818+
surro.insert(k.clone(), atom(Leaf::S(sur)));
5819+
}
5820+
}
5821+
// pos_of: fact-type-name key -> 0-based Transition column position, from
5822+
// the role metamodel's Transition-typed declarations plus the hardcoded
5823+
// machinery cells (python's pos_of.update literal, unconditional so it
5824+
// overrides any role-derived entry for the same name)
5825+
let mut pos_of: HashMap<String, i64> = HashMap::new();
5826+
for r in pop_rows(cells, &leaf("role")) {
5827+
let it = items(&list_of(&r));
5828+
if it.len() >= 4 {
5829+
let is_transition =
5830+
matches!(aval(&it[3]).as_deref(), Some(Leaf::S(s)) if s == "Transition");
5831+
if is_transition {
5832+
if let Some(Leaf::I(p)) = aval(&it[2]).as_deref() {
5833+
pos_of.insert(key_of(&it[1]), *p - 1);
5834+
}
5835+
}
5836+
}
5837+
}
5838+
for (name, pos) in [
5839+
("smFrom", 0i64),
5840+
("smTo", 0),
5841+
("smTrigger", 0),
5842+
("smGuard", 0),
5843+
("smEmit", 0),
5844+
("smMoore", 0),
5845+
("Guard_prevents_Transition", 1),
5846+
] {
5847+
pos_of.insert(key_of(&atom(leaf(name))), pos);
5848+
}
5849+
// the walk: every cell in D, in place; only a cell named in pos_of has
5850+
// its rows visited, and only the row's value AT that column, when it is
5851+
// a bare name in surro, is replaced — everything else copies through
5852+
for i in 0..cells.len() {
5853+
let nk = key_of(&atom(cells[i].0.clone()));
5854+
let p = match pos_of.get(&nk) {
5855+
Some(&pp) if pp >= 0 => pp as usize,
5856+
_ => continue,
5857+
};
5858+
let rows = items(&list_of(&cells[i].1));
5859+
if rows.is_empty() {
5860+
continue;
5861+
}
5862+
let mut changed = false;
5863+
let mut new_rows: Vec<V> = Vec::with_capacity(rows.len());
5864+
for row in rows {
5865+
let mut out_row = row.clone();
5866+
if let Shape::Seq(rl) = shape(&row) {
5867+
let mut cols = items(&rl);
5868+
if cols.len() > p {
5869+
let ck = key_of(&cols[p]);
5870+
if let Some(sur) = surro.get(&ck) {
5871+
cols[p] = sur.clone();
5872+
out_row = seq(from_vec(cols));
5873+
changed = true;
5874+
}
5875+
}
5876+
}
5877+
new_rows.push(out_row);
5878+
}
5879+
if changed {
5880+
cells[i].1 = seq(from_vec(new_rows));
5881+
}
5882+
}
5883+
}
5884+
56935885
fn op_compile_model(j: &J, srv: &mut Srv) -> Result<String, String> {
56945886
use std::collections::{BTreeMap, HashMap, HashSet};
56955887
// args parse before anything runs (op_run_rules' discipline: a malformed
@@ -5903,6 +6095,10 @@ fn op_compile_model(j: &J, srv: &mut Srv) -> Result<String, String> {
59036095
let mut prose: Vec<String> = Vec::new();
59046096
let mut blocked: Vec<String> = Vec::new();
59056097
let mut classified = 0usize;
6098+
// slice 1's identity-skip witness: true the instant ANY fold_fire or
6099+
// canon-DEF adoption actually mutates model_cells — "the fold produced
6100+
// no cells beyond the seed" (the probe-app case) is exactly !folded_any
6101+
let mut folded_any = false;
59066102
// the native cook context (#20): the SAME names/subs/fts/plain/vals the
59076103
// ctx operand carries, in cooks form, built once per compile
59086104
let kn = cooks::Known::new(&names, &subs, &fts, &plain, &vals);
@@ -6019,6 +6215,7 @@ fn op_compile_model(j: &J, srv: &mut Srv) -> Result<String, String> {
60196215
// future seam, kept correct even though dormant today)
60206216
model_cells = raw_cells_of(&res);
60216217
accepted = true;
6218+
folded_any = true;
60226219
continue;
60236220
}
60246221
// the panic fence (#32): a cook tripping a guarded-by-construction
@@ -6051,6 +6248,7 @@ fn op_compile_model(j: &J, srv: &mut Srv) -> Result<String, String> {
60516248
e, stmt
60526249
));
60536250
}
6251+
folded_any = true;
60546252
if trace_on {
60556253
let mut f = String::new();
60566254
cooks::fire_json(t, &fire, &mut f);
@@ -6095,6 +6293,62 @@ fn op_compile_model(j: &J, srv: &mut Srv) -> Result<String, String> {
60956293
translated.push(e);
60966294
}
60976295
}
6296+
// slice 1 tail (native pipeline tail, #20): rekey_transitions
6297+
// (machine-scope transition identity — compiler.py's compile_model
6298+
// wrapper applies it right after the fold: D2 = system.rekey_transitions(D2))
6299+
rekey_transitions_native(&mut model_cells);
6300+
// then the post-model rules fixpoint (protocol.py:1815's separate
6301+
// system.run_rules(D, ...) call, made after compile_model returns) through
6302+
// the EXISTING native op_run_rules machinery — the SAME save/swap/restore
6303+
// discipline the batch-classification derive above already uses, so this
6304+
// op stays pure: the resident's own store is read for dispatch throughout
6305+
// and left exactly as found, never overwritten with the compiled model.
6306+
// Identity-skip: a fold that never fired (no cell mutation, no canon-DEF
6307+
// adoption — the probe-app case) leaves model_cells identical to the seed,
6308+
// and running the fixpoint over an unchanged, already-derived seed is a
6309+
// proven no-op (round one finds nothing new and breaks immediately) — so
6310+
// it is skipped here to save the setup cost, never to change the answer.
6311+
if folded_any {
6312+
let saved_d2 = srv.d.clone();
6313+
let saved_cells2 = srv.cells.clone();
6314+
let saved_nd2 = srv.nd.clone();
6315+
let saved_ncells2 = srv.ncells.clone();
6316+
srv.d = cells_to_d(&model_cells);
6317+
srv.cells = model_cells.clone();
6318+
srv.nd = v_to_n(&srv.d);
6319+
srv.ncells = n_cells_of(&srv.nd);
6320+
let rules_req = J::O(Vec::new());
6321+
let derived2 = op_run_rules(&rules_req, srv);
6322+
if derived2.is_ok() {
6323+
// harvest from the TRUE store (srv.d), not the index Vec: python's
6324+
// Store re-tops every write, so a head cell NEW in the rules phase
6325+
// sits at the FRONT of D — the index Vec appends it at the end,
6326+
// which is a different (wrong) dump order (core.md's two
6327+
// new-in-rules heads, found by the order forensics 2026-07-11)
6328+
model_cells = raw_cells_of(&srv.d);
6329+
}
6330+
srv.d = saved_d2;
6331+
srv.cells = saved_cells2;
6332+
srv.nd = saved_nd2;
6333+
srv.ncells = saved_ncells2;
6334+
derived2?;
6335+
}
6336+
// rule_diagnostics: the ruleDiag population AFTER rules (python's own
6337+
// compiler.py:2489 reads it right after rekey, BEFORE the pipeline's
6338+
// separate run_rules call — but ruleDiag is a STAGE-1 COMPILE diagnostic,
6339+
// written only by the rule_if/rule_iff cook when a rule body fails to
6340+
// compile, never a derivation rule's HEAD in any known corpus, so
6341+
// run_rules never adds to it; reading it here, after everything, equals
6342+
// python's snapshot by construction)
6343+
let rulediag_rows: Vec<V> = pop_rows(&model_cells, &leaf("ruleDiag"));
6344+
let mut rulediag_json = String::from("[");
6345+
for (i, row) in rulediag_rows.iter().enumerate() {
6346+
if i > 0 {
6347+
rulediag_json.push(',');
6348+
}
6349+
write_v(row, &mut rulediag_json);
6350+
}
6351+
rulediag_json.push(']');
60986352
// the report: the seed contract's surviving keys (total/unclassified/prose)
60996353
// plus the honest diagnostics (classified/grammar/missing/blocked)
61006354
let mut r = String::from("{\"total\":");
@@ -6121,15 +6375,31 @@ fn op_compile_model(j: &J, srv: &mut Srv) -> Result<String, String> {
61216375
arr(&missing, &mut r);
61226376
r.push_str(",\"blocked\":");
61236377
arr(&blocked, &mut r);
6378+
// slice 2 (native pipeline tail, #20): the Python rep contract's exact
6379+
// field names (compiler.py:2490), alongside the skeleton's own honest
6380+
// diagnostics above — total/prose already match; kinds is always the
6381+
// selfhost path's empty dict (a seed leftover, emitted verbatim, never
6382+
// innovated on); unparsed mirrors unclassified under python's external
6383+
// name; rule_diagnostics is the ruleDiag rows built above
6384+
r.push_str(",\"kinds\":{}");
6385+
r.push_str(",\"unparsed\":");
6386+
arr(&unclassified, &mut r);
6387+
r.push_str(",\"rule_diagnostics\":");
6388+
r.push_str(&rulediag_json);
61246389
if trace_on {
61256390
// the differential dump: per statement, the fires' ⟨asserts, objs⟩
61266391
r.push_str(",\"translated\":[");
61276392
r.push_str(&translated.join(","));
61286393
r.push(']');
61296394
}
61306395
if dump_store_on {
6131-
// the folded store itself: python compile_model_selfhost's returned
6132-
// D, full from_lam — the fold's own differential surface
6396+
// the store as of THIS slice's own boundary: fold -> rekey_transitions
6397+
// -> the post-model run_rules fixpoint (when it ran) — python's
6398+
// compile_model (WITH rekey) + the pipeline's separate run_rules call,
6399+
// full from_lam. (The fold slice's own narrower boundary,
6400+
// compile_model_selfhost's bare return, is no longer what dump_store
6401+
// answers — that comparison now lives in fold_pydump.py calling
6402+
// compile_model_selfhost directly, pre-rekey, pre-rules.)
61336403
r.push_str(",\"store\":");
61346404
write_v(&cells_to_d(&model_cells), &mut r);
61356405
}

0 commit comments

Comments
 (0)