Skip to content

Commit 2fbae6e

Browse files
authored
Merge pull request #331 from AdaWorldAPI/claude/odoo-rs-v3-ogar-transpile-nwriny
ogar-from-ruff lance_sink: key the row by the minted facet, not a V1 tail
2 parents f988514 + d2ee92f commit 2fbae6e

3 files changed

Lines changed: 157 additions & 72 deletions

File tree

‎crates/ogar-from-ruff/src/lance_sink.rs‎

Lines changed: 135 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
//!
99
//! - `lance_graph_contract::network` (harvest → `FacetCascade`, content-blind,
1010
//! one concept mint, deferred row embedding) — the *shape* borrowed here.
11-
//! - `symbiont::bridge` (`NodeRow` + `NodeRowPacket::as_le_bytes` zero-copy Lance
12-
//! byte path) — the *embedding idiom* borrowed here.
11+
//! - `NodeRow` + `NodeRowPacket::as_le_bytes`, the zero-copy Lance byte path
12+
//! (the embedding idiom first used by the since-deprecated `symbiont` crate).
1313
//!
1414
//! # What this does (and deliberately does NOT)
1515
//!
@@ -18,10 +18,19 @@
1818
//! (`facet_classid(4) | 6×(is_a:lo, part_of:hi)`, the L1 **rails** plane,
1919
//! `CascadeShape::G6D2`). So [`compiled_class_to_facet`] is a reinterpret no-op.
2020
//! [`compiled_class_to_noderow`] embeds one class as ONE 512-byte CANON
21-
//! [`NodeRow`] — the render classid in the key `[0..4)`, a **bootstrap** tail
22-
//! (family=0, HHTL=0, identity=caller-supplied), `ValueSchema::Bootstrap`
23-
//! (all-zero value slab). [`compiled_classes_to_le_bytes`] packs a slice onto the
24-
//! zero-copy storage boundary.
21+
//! [`NodeRow`] whose **key IS the minted facet**: the same 16 bytes, carried
22+
//! through lance-graph-contract's byte-identical `FacetCascade → NodeGuid`
23+
//! conversion — render classid in `[0..4)`, the `part_of:is_a` rails in
24+
//! `[4..16)`. `ValueSchema::Bootstrap` (all-zero value slab). The facet is the
25+
//! row's identity, so [`compiled_classes_to_noderows`] refuses a batch in which
26+
//! two classes mint the same facet. [`compiled_classes_to_le_bytes`] packs a
27+
//! slice onto the zero-copy storage boundary.
28+
//!
29+
//! The key used to be built with `NodeGuid::new(classid, 0, 0, 0, 0, identity)`
30+
//! — a V1 `family:identity` tail that dropped the rails. That was deliberate
31+
//! while the rail-chain vs key-tail reconciliation was open; lance-graph has
32+
//! since settled it (a V3 mint never degrades to a V1 tail; `NodeGuid` and
33+
//! `FacetCascade` are the same 16 bytes and convert without reinterpretation).
2534
//!
2635
//! Out of scope (named, not hidden), per the W2 plan:
2736
//! - The actual Lance `Dataset::write` I/O — stops at `as_le_bytes()`, exactly
@@ -30,11 +39,6 @@
3039
//! - The 12-byte rail payload → a NodeRow **value tenant** (`[H2]`: needs a new
3140
//! append-only `ValueTenant`, `v3-envelope-auditor`-gated) — mirror network,
3241
//! which added no lane. The rails ride the `FacetCascade` surface only.
33-
//! - The key **tail from the rail tiers** (`[H1]`: le-contract §3 open item —
34-
//! the `6×(8:8)` rail chain vs the key's contiguous `u24++u24` tail are
35-
//! structurally different bit-groupings, "do not unify silently in code").
36-
//! The tail is bootstrapped; [`compiled_class_to_noderow`] never derives it
37-
//! from the rails (test-enforced).
3842
//! - `CompiledClass.actions` (behavior) — out-of-line, not in this byte payload
3943
//! ("neither half carries behavior").
4044
//! - Ownership: this is an OFFLINE **bake** (source → artifact, single-writer) =
@@ -59,29 +63,74 @@ pub fn compiled_class_to_facet(cc: &CompiledClass) -> FacetCascade {
5963
FacetCascade::from_bytes(&cc.facet.to_bytes())
6064
}
6165

62-
/// Embed one [`CompiledClass`] as ONE 512-byte CANON [`NodeRow`]: the render
63-
/// classid in the key `[0..4)`, a **bootstrap** tail (`family = 0`, HHTL = 0,
64-
/// `identity` = caller-supplied), `ValueSchema::Bootstrap` (all-zero 480-byte
65-
/// value slab), empty edge block. Mirrors `symbiont::bridge::board`.
66+
/// Embed one [`CompiledClass`] as ONE 512-byte CANON [`NodeRow`]: the key is
67+
/// the minted facet itself (render classid + `part_of:is_a` rails, byte for
68+
/// byte), `ValueSchema::Bootstrap` (all-zero 480-byte value slab), empty edge
69+
/// block.
70+
///
71+
/// No V1 `family:identity` tail is minted: the key comes from
72+
/// `NodeGuid::from(FacetCascade)`, never `NodeGuid::new`.
6673
///
67-
/// The key tail is bootstrapped, NOT derived from the rail tiers (`[H1]`
68-
/// freeze — the rail chain vs the key `u24++u24` tail reconciliation is an open
69-
/// operator/envelope ruling). `identity` must fit in 24 bits (`NodeGuid::new`
70-
/// asserts this).
74+
/// # Example
75+
///
76+
/// ```ignore
77+
/// let row = compiled_class_to_noderow(&cc);
78+
/// assert_eq!(row.key.as_bytes(), &cc.facet.to_bytes());
79+
/// ```
7180
#[must_use]
72-
pub fn compiled_class_to_noderow(cc: &CompiledClass, identity: u32) -> NodeRow {
81+
pub fn compiled_class_to_noderow(cc: &CompiledClass) -> NodeRow {
7382
NodeRow {
74-
// classid verbatim from the mint; family/HHTL bootstrap-zero, identity
75-
// caller-supplied → the zero-fallback ladder's bootstrap address while
76-
// the class is unbasined.
77-
key: NodeGuid::new(cc.facet.facet_classid(), 0, 0, 0, 0, identity),
83+
key: NodeGuid::from(compiled_class_to_facet(cc)),
7884
edges: EdgeBlock::default(),
7985
// ValueSchema::Bootstrap == FieldMask::EMPTY == all-zero slab; asserted
8086
// by the constant below so a reader never mistakes it for a live schema.
8187
value: [0u8; 480],
8288
}
8389
}
8490

91+
/// Two classes in one batch minted byte-identical facets, so they would share
92+
/// a row key. Indices are positions in the input slice.
93+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94+
pub struct DuplicateFacetKey {
95+
/// The earlier class with this key.
96+
pub first: usize,
97+
/// The later class that collides with it.
98+
pub second: usize,
99+
}
100+
101+
impl core::fmt::Display for DuplicateFacetKey {
102+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103+
write!(
104+
f,
105+
"classes {} and {} mint the same 16-byte facet, so they would share a row key",
106+
self.first, self.second
107+
)
108+
}
109+
}
110+
111+
impl std::error::Error for DuplicateFacetKey {}
112+
113+
/// Embed a batch of classes, one [`NodeRow`] each, in input order.
114+
///
115+
/// # Errors
116+
///
117+
/// [`DuplicateFacetKey`] if two classes mint the same facet: the facet is the
118+
/// row key, so a duplicate would make two classes indistinguishable.
119+
pub fn compiled_classes_to_noderows(
120+
ccs: &[CompiledClass],
121+
) -> Result<Vec<NodeRow>, DuplicateFacetKey> {
122+
let rows: Vec<NodeRow> = ccs.iter().map(compiled_class_to_noderow).collect();
123+
let mut seen: std::collections::HashMap<[u8; 16], usize> =
124+
std::collections::HashMap::with_capacity(rows.len());
125+
for (i, row) in rows.iter().enumerate() {
126+
if let Some(&first) = seen.get(row.key.as_bytes()) {
127+
return Err(DuplicateFacetKey { first, second: i });
128+
}
129+
seen.insert(*row.key.as_bytes(), i);
130+
}
131+
Ok(rows)
132+
}
133+
85134
/// `ValueSchema` this sink stamps into every row it builds — the all-zero
86135
/// bootstrap slab (no tenant lane written; the rails ride the facet surface).
87136
pub const SINK_VALUE_SCHEMA: ValueSchema = ValueSchema::Bootstrap;
@@ -175,54 +224,69 @@ mod tests {
175224
assert_eq!(CascadeShape::G6D2.groups(), 6);
176225
}
177226

227+
/// A second, unrelated Odoo model, so two DISTINCT classes can be sunk
228+
/// (one class now yields exactly one key).
229+
fn res_partner_compiled() -> CompiledClass {
230+
let mut g = ModelGraph::new("odoo");
231+
g.models.push(Model::new("res_partner"));
232+
crate::mint::compile_graph_python::<OdooPort>(&g)
233+
.into_iter()
234+
.find(|c| c.class.name == "res_partner")
235+
.expect("res_partner compiles")
236+
}
237+
178238
// ── T-C: the render classid lands in the key ──
179239
#[test]
180240
fn classid_lands_in_key() {
181241
let cc = account_move_compiled();
182-
let row = compiled_class_to_noderow(&cc, 7);
242+
let row = compiled_class_to_noderow(&cc);
183243
assert_eq!(
184244
row.key.classid(),
185245
cc.facet.facet_classid(),
186246
"key[0..4) decodes the render classid"
187247
);
188248
}
189249

190-
// ── T-D: bootstrap tail / [H1] guard — rails did NOT leak into the key tail ──
250+
// ── T-D: the key IS the minted facet — rails included, no V1 tail ──
191251
#[test]
192-
fn key_tail_is_bootstrap_not_derived_from_rails() {
252+
fn key_is_the_minted_facet_rails_included() {
193253
let cc = account_move_compiled();
194-
let row = compiled_class_to_noderow(&cc, 7);
195-
assert_eq!(row.key.family(), 0, "family bootstrap-zero (unbasined)");
196-
assert_eq!(
197-
row.key.identity(),
198-
7,
199-
"identity is the caller value, not a rail byte"
254+
// Anti-vacuity: with all-zero rails a bootstrap V1 key and the facet
255+
// key would be indistinguishable in [4..16), and this test would pass
256+
// for the old code too.
257+
assert_ne!(
258+
cc.facet.to_bytes()[4..],
259+
[0u8; 12],
260+
"fixture must carry non-zero rails"
200261
);
201-
assert!(
202-
row.key.is_unbasined(),
203-
"family==0 → unbasined; the rail tiers must not populate the tail ([H1])"
262+
let row = compiled_class_to_noderow(&cc);
263+
assert_eq!(row.key.as_bytes(), &cc.facet.to_bytes(), "byte for byte");
264+
let back = row.key.facet();
265+
assert_eq!(
266+
back.hi_chain(),
267+
cc.facet.part_of_chain(),
268+
"part_of in the key"
204269
);
270+
assert_eq!(back.lo_chain(), cc.facet.is_a_chain(), "is_a in the key");
205271
}
206272

207273
// ── T-E: round-trip through the storage byte boundary ──
208274
#[test]
209-
fn le_bytes_round_trip_preserves_classid() {
210-
let cc = account_move_compiled();
211-
let rows = vec![
212-
compiled_class_to_noderow(&cc, 1),
213-
compiled_class_to_noderow(&cc, 2),
214-
];
275+
fn le_bytes_round_trip_preserves_keys() {
276+
let ccs = [account_move_compiled(), res_partner_compiled()];
277+
let rows = compiled_classes_to_noderows(&ccs)
278+
.unwrap_or_else(|e| panic!("distinct classes refused: {e}"));
215279

216280
// The true zero-copy decode reads the packet's OWN bytes (a borrow of the
217-
// 64-aligned `&[NodeRow]`, per canonical_node's align(64) contract) —
218-
// mirrors symbiont::bridge's round-trip.
281+
// 64-aligned `&[NodeRow]`, per canonical_node's align(64) contract).
219282
let packet = NodeRowPacket::new(&rows, 0);
220283
let aligned = packet.as_le_bytes();
221284
assert_eq!(aligned.len(), 2 * 512, "two 512-byte CANON rows");
222285
let decoded = node_rows_from_le_bytes(aligned).expect("aligned zero-copy decode");
223286
assert_eq!(decoded.len(), 2);
224-
assert_eq!(decoded[0].key.classid(), cc.facet.facet_classid());
225-
assert_eq!(decoded[1].key.identity(), 2);
287+
for (cc, row) in ccs.iter().zip(decoded) {
288+
assert_eq!(row.key.as_bytes(), &cc.facet.to_bytes());
289+
}
226290

227291
// The owned-`Vec` helper carries the identical bytes (a copy off the
228292
// storage boundary — Lance owns re-alignment on read-back).
@@ -231,24 +295,37 @@ mod tests {
231295

232296
// ── T-F: field isolation (I-LEGACY-API-FEATURE-GATED matrix) ──
233297
#[test]
234-
fn field_isolation_classid_and_identity_are_independent() {
235-
let cc = account_move_compiled();
236-
// Same class, two identities → classid identical, identity distinct,
237-
// value slab + edges untouched (all zero) in both.
238-
let a = compiled_class_to_noderow(&cc, 10);
239-
let b = compiled_class_to_noderow(&cc, 20);
240-
assert_eq!(
241-
a.key.classid(),
242-
b.key.classid(),
243-
"identity write left classid unchanged"
244-
);
245-
assert_ne!(a.key.identity(), b.key.identity());
298+
fn field_isolation_key_value_and_edges_are_independent() {
299+
let a = compiled_class_to_noderow(&account_move_compiled());
300+
let b = compiled_class_to_noderow(&res_partner_compiled());
301+
assert_ne!(a.key, b.key, "distinct classes, distinct keys");
246302
assert_eq!(
247303
a.value, [0u8; 480],
248304
"no value lane written (bootstrap schema)"
249305
);
250306
assert_eq!(b.value, [0u8; 480]);
251307
assert_eq!(a.edges, EdgeBlock::default(), "edge block reserved-zero");
308+
assert_eq!(b.edges, EdgeBlock::default());
309+
}
310+
311+
// ── T-H: the batch refuses a duplicate key, and only a duplicate ──
312+
#[test]
313+
fn batch_refuses_duplicate_facets_and_only_those() {
314+
let am = account_move_compiled();
315+
let rp = res_partner_compiled();
316+
// can it fire: the same class twice is the same key
317+
assert_eq!(
318+
compiled_classes_to_noderows(&[am.clone(), rp.clone(), am.clone()]).err(),
319+
Some(DuplicateFacetKey {
320+
first: 0,
321+
second: 2
322+
})
323+
);
324+
// can it stay silent: distinct classes pass, in input order
325+
let rows = compiled_classes_to_noderows(&[rp.clone(), am.clone()])
326+
.unwrap_or_else(|e| panic!("distinct classes refused: {e}"));
327+
assert_eq!(rows[0].key.as_bytes(), &rp.facet.to_bytes());
328+
assert_eq!(rows[1].key.as_bytes(), &am.facet.to_bytes());
252329
}
253330

254331
// ── T-G: this sink moves ZERO bytes at rest — no layout-version bump ──

‎crates/ogar-from-ruff/tests/odoo_v3_sink_e2e.rs‎

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//! -> ruff_python_spo::extract_from_source (AST -> ModelGraph)
77
//! -> ogar_from_ruff::mint::compile_graph_python::<OdooPort>
88
//! -> Vec<CompiledClass { class, facet, actions:+kausal }> (W1 arm)
9-
//! -> ogar_from_ruff::lance_sink::compiled_class_to_noderow (W2 arm)
9+
//! -> ogar_from_ruff::lance_sink::compiled_classes_to_noderows (W2 arm)
1010
//! -> NodeRowPacket::as_le_bytes (storage boundary)
1111
//! -> node_rows_from_le_bytes (zero-copy decode round-trip)
1212
//! ```
@@ -23,7 +23,7 @@
2323

2424
use lance_graph_contract::canonical_node::{NodeRowPacket, node_rows_from_le_bytes};
2525
use lance_graph_contract::soa_envelope::SoaEnvelope;
26-
use ogar_from_ruff::lance_sink::{compiled_class_to_facet, compiled_class_to_noderow};
26+
use ogar_from_ruff::lance_sink::{compiled_class_to_facet, compiled_classes_to_noderows};
2727
use ogar_from_ruff::mint::compile_graph_python;
2828
use ogar_vocab::ports::OdooPort;
2929
use ruff_python_spo::extract_from_source;
@@ -38,25 +38,20 @@ fn odoo_source_transpiles_to_v3_soa_noderows() {
3838
let compiled = compile_graph_python::<OdooPort>(&graph);
3939
assert_eq!(compiled.len(), 2, "two models transpile end to end");
4040

41-
// ── sink (W2 arm): each CompiledClass → one CANON NodeRow ──
42-
let rows: Vec<_> = compiled
43-
.iter()
44-
.enumerate()
45-
.map(|(i, cc)| compiled_class_to_noderow(cc, i as u32))
46-
.collect();
41+
// ── sink (W2 arm): each CompiledClass → one CANON NodeRow keyed by its facet ──
42+
let rows = compiled_classes_to_noderows(&compiled).expect("two models, two distinct facets");
4743

48-
// Each row's key carries the class's render classid verbatim; the facet
49-
// reinterpret preserves the rail (part_of/is_a) chains.
44+
// Each row's key IS the minted facet: render classid + part_of/is_a rails.
5045
for (i, (cc, row)) in compiled.iter().zip(&rows).enumerate() {
5146
assert_eq!(
5247
row.key.classid(),
5348
cc.facet.facet_classid(),
5449
"row {i}: key classid == mint classid"
5550
);
56-
assert_eq!(row.key.identity(), i as u32, "row {i}: bootstrap identity");
57-
assert!(
58-
row.key.is_unbasined(),
59-
"row {i}: bootstrap tail (no rail leak)"
51+
assert_eq!(
52+
row.key.as_bytes(),
53+
&cc.facet.to_bytes(),
54+
"row {i}: the key is the minted facet, byte for byte"
6055
);
6156
let fc = compiled_class_to_facet(cc);
6257
assert_eq!(

‎docs/DISCOVERY-MAP.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,19 @@ isolation. The map's job is to keep them visible.
13281328
(semantics unruled). This is an offline BAKE (BOOTSTRAP-OK, envelope
13291329
owner 0), not an online write. Cross-ref D-EXEC-ONE-ACTION,
13301330
D-KAUSAL-CONSUME-PIN-ODOO, OGAR-TRANSPILE-SUBSTRATE (pull-back contract).
1331+
- **2026-10-10 — `[H1]` CLOSED, key moved to the facet (VERIFIED-IN-CODE,
1332+
TEST-PINNED).** The freeze above is lifted: lance-graph settled the
1333+
reconciliation (a V3 mint never degrades to a V1 tail, #1213; `NodeGuid` ↔
1334+
`FacetCascade` is a byte-identical conversion, canonical_node.rs). The row
1335+
key is now `NodeGuid::from(compiled_class_to_facet(cc))` — the minted
1336+
facet, rails included — instead of `NodeGuid::new(classid, 0,0,0,0,
1337+
identity)`, which dropped the rails and minted a V1 `family:identity`
1338+
tail. `compiled_class_to_noderow` loses its `identity` parameter (the facet
1339+
is the row's identity); new `compiled_classes_to_noderows` refuses a batch
1340+
whose classes mint the same facet (`DuplicateFacetKey`). T-D is inverted
1341+
(key == facet byte for byte, with an anti-vacuity guard that the fixture's
1342+
rails are non-zero); T-H pins can-fire + can-stay-silent. Disable run:
1343+
restoring the V1 key turns T-D, T-E and T-H red. `[H2]`/`[H3]` unchanged.
13311344

13321345
- **D-OGAR-DOC-LAYER** (2026-07-12; **[G] — W4 mints + ActionDefs council-ratified
13331346
and shipped 2026-08-25**, executor body still open — see the Status paragraph

0 commit comments

Comments
 (0)