From f55481d93b8b692d400c2d055512f43838c0f276 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 15:31:21 +0000 Subject: [PATCH 1/4] =?UTF-8?q?docs(transpile-substrate):=20=C2=A71.6=20th?= =?UTF-8?q?ree=20SDKs=20(Rust/C#/Python),=20one=20compiled=20spine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address the operator ask: 'for Rust via lance-graph; for Python and C# we need sort of an SDK that does that for the others.' - New §1.6: every language SDK is three thin layers — (1) the address algebra (FacetCascade + CascadeShape, byte-identical across languages), (2) a ClassView reader over a pulled CompiledClass, (3) the per-language adapter host for the 15% + the late classid→OGIT grounding resolve. Layer 1 is ~80 lines of core-only const fn, so an SDK is a mechanical transliteration; the shared byte format means the SDKs cannot drift on layout. - Two pull-back modes per host: (b) codegen emit (emit_csharp → assembly, emit_python → bytecode-on-import — the strongest 'compiled' form) and (a) a thin runtime reader over the rail-facet artifact (zero-parse, never SurrealQL). - §1.5 status promoted: the per-carving tier-byte arithmetic is now pinned + implemented as lance_graph_contract::facet::CascadeShape (was 'to pin before coding'). §6 roadmap + §7 extension table point at §1.6. Co-Authored-By: Claude --- docs/OGAR-TRANSPILE-SUBSTRATE.md | 89 +++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/docs/OGAR-TRANSPILE-SUBSTRATE.md b/docs/OGAR-TRANSPILE-SUBSTRATE.md index 5674bc2..c2faaec 100644 --- a/docs/OGAR-TRANSPILE-SUBSTRATE.md +++ b/docs/OGAR-TRANSPILE-SUBSTRATE.md @@ -137,9 +137,80 @@ heavier value-SoA is only built when actually needed, then cached. > **Status:** the recombination *principle* + the nested-constructor + lazy-SoA > architecture are operator-specified here as the durable design. The exact -> tier-byte arithmetic of each carving (`6×(1:2)` / `4×(1:2:3)` / `3×(1:2:3:4)` -> over the facet's 12 tier-bytes) is the implementation detail to pin against -> `lance_graph_contract::facet::FacetCascade` before coding — not guessed. +> tier-byte arithmetic of each carving is **now pinned + implemented** as +> `lance_graph_contract::facet::CascadeShape` (`G6D2` / `G4D3` / `G3D4`, with +> `G·D = CASCADE_UNITS = 12`) over `FacetCascade::tier_bytes()` — `index(g,l) = +> g·D + l`, `group_of`/`level_of` the inverses, plus `cascade_byte` and the +> per-group LCP `cascade_group_shared`. The `G3D4`/`G6D2` `group_of` is a shift +> (the canon's "tier-of-level is a shift, never a branch"); `G4D3` is the +> straddle that needs a divide. Zero-dep, `const fn`, probe-verified. This is +> **one algebra for both the facet bytes and a 12-field class** — the shared +> substrate the three language SDKs (§1.6) all read. + +--- + +## 1.6 Three SDKs, one compiled spine (Rust · C# · Python) + +> **Operator, 2026-06-29.** *"For Rust via lance-graph; for Python and C# we +> need sort of an 'SDK' that does that for the others."* + +Rust's wrapper contract is `lance-graph-contract` — the consumer `import`s it +and pulls a `CompiledClass` by `classid`. Python and C# need the **same +capability**, packaged as a thin per-language **SDK**. The crucial constraint +(§1.5): the spine is the **compiled `ClassView`, never a SurrealQL parse**. So +an SDK is *not* a query client — it is a thin reader over the already-compiled +rail, plus a host for that language's 15 % adapter. + +### What every SDK is (three thin layers, nothing more) + +| layer | what it is | Rust (`lance-graph-contract`) | C# SDK | Python SDK | +|---|---|---|---|---| +| **1. address algebra** | the 16-byte facet + the cascade carving math — *byte-identical across languages* | `FacetCascade` + `CascadeShape` (`const fn`, zero-dep) | `readonly struct FacetCascade` (`[StructLayout(Sequential, Size=16)]`) + `enum CascadeShape` | `Facet` (a 16-byte `bytes` view) + `CascadeShape` (`IntEnum`) | +| **2. ClassView reader** | present a pulled `CompiledClass` as native schema objects (fields / relations / computed), grouped by the chosen carving | `ClassView` traits | `IClassView` / records | `@dataclass` ClassView | +| **3. adapter host** | the per-language hook where the 15 % hand-written logic registers, + the late `classid → ClassView → OGIT` grounding resolve | `ActionDef`/`KausalSpec` impls | interface + DI | ABC + registry | + +Layer 1 is the whole reason an SDK can be *thin*: the cascade algebra is **~80 +lines of `core`-only `const fn`** (`CascadeShape::{groups,levels,index, +group_of,level_of}` + `FacetCascade::{tier_bytes,cascade_byte, +cascade_group_shared}`). Nothing in it is Rust-specific — it is integer +shifts/divides over a 16-byte array. Porting it to C# or Python is a +**mechanical transliteration**, and the bytes it reads are produced once by the +OGAR mint, so all three SDKs agree by construction (the same cross-crate +round-trip probe that pins `ruff_spo_address::Facet ≡ FacetCascade` extends to +"≡ the C#/Python `FacetCascade`"). + +### How a class reaches each language (mirrors §1's two pull-back modes) + +- **(b) codegen emit — the strongest "compiled" form per host.** OGAR emits + native source on the **same `&CompiledClass -> String` seam** as `emit_rust`: + - `emit_csharp` → a C# `record`/class compiled by the host into an assembly + (truly compiled, like the Rust struct). + - `emit_python` → a `@dataclass` imported as a module (compiled to bytecode by + CPython; the "cost of an import" literally). + There is **no OGAR-runtime parse** in this mode — the class IS native source + the host toolchain compiles. This is the preferred mode and the direct analog + of "sinks into OGAR and gets compiled into the binary." +- **(a) runtime reader — the thin SDK over the rail artifact.** When codegen is + undesirable (dynamic discovery, late binding), the SDK loads the **compiled + rail artifact** (the facet SoA — the 16-byte-per-class bytes themselves, which + ARE the format) and reads it zero-parse via layer 1. It never loads SurrealQL; + the artifact is the facet bytes, not DDL. + +Either way the 85 % logic stays in OGAR; the SDK carries layer-1 (tiny, +portable) + layer-3 (the language's own 15 %). That is what makes "one +canonical class, N languages, at the cost of an import" concrete for C# and +Python and not just Rust. + +### Why this respects every iron rule + +- **Compiled, not parsed** (§1.5): codegen emits host-native source; the runtime + reader is zero-parse over the facet bytes. SurrealQL is never on an SDK path. +- **Pull, never re-mint** (§7): an SDK *reads* a `CompiledClass`; it never owns a + codebook copy or constructs a bridge — the classid resolution is the mint's. +- **Resolve, don't store** (§4): grounding is layer-3's late `classid → OGIT` + resolve, identical in all three languages; no SDK copies FIBO/DOLCE onto rows. +- **One algebra** (§1.5): layer 1 is the *same* `CascadeShape` carving in every + language — the SDKs cannot drift on layout because they share the byte format. --- @@ -323,10 +394,12 @@ correct because of the `relation_kind` predicate (ruff#35): `target` + `32×GUID` SoA. Pin the per-carving tier-byte arithmetic against `FacetCascade` first (don't guess). This subsumes hardcoded facet "versions". The `emit_rust` codegen leg is the start; this is the depth. -2. **Pull-back breadth** — `emit_csharp` / `emit_python` on the same - `&CompiledClass -> String` seam; refine `OgScalar` once the `field_type` - capture lands (ruff follow-up). Runtime wrapper-contract is the C#/Python - sibling of `lance-graph-contract`. +2. **Pull-back breadth — the C# / Python SDKs (§1.6).** `emit_csharp` / + `emit_python` on the same `&CompiledClass -> String` seam (codegen mode), and + the thin runtime SDKs (layer-1 `FacetCascade` + `CascadeShape` transliterated, + layer-2 ClassView reader, layer-3 adapter host). Layer 1 is mechanical now + that `CascadeShape` is pinned in `lance-graph-contract`. Refine `OgScalar` + once the `field_type` capture lands (ruff follow-up). 3. **Thin the consumer (membrane)** — `odoo-rs` → `compile_graph::` caller + `od-posting` GoBD adapter; delete the native SurrealQL emit fork (W3.3, **CI-gated** — od-ontology pulls surrealdb). Recommended path: keep @@ -343,7 +416,7 @@ correct because of the `relation_kind` predicate (ruff#35): `target` + | To add… | Do this | Convergence is… | |---|---|---| | a **source language** | a `ruff__spo` frontend → `ModelGraph`; reuse `lift` + `mint` | automatic (shared IR) | -| a **target language** | an `ogar-emit-` adapter (`CompiledClass → String`) **or** a thin runtime wrapper contract (traits mirroring `lance-graph-contract`) | the consumer reimplements nothing | +| a **target language** | an SDK (§1.6): `ogar-emit-` codegen **or** a thin runtime SDK = layer-1 `FacetCascade`+`CascadeShape` transliteration + layer-2 ClassView reader + layer-3 adapter host (mirrors `lance-graph-contract`) | the consumer reimplements nothing | | a **concept** | a `class_ids` codebook entry + a `PortSpec` alias | automatic across all ports that map it | | a **port (app)** | one `impl PortSpec for FooPort` block (NAMESPACE, BRIDGE_ID, APP_PREFIX, aliases) | the app's classes get a render skin for free | From 6b3ee31238fea20c362a7d4534645d77bf9e6e0e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 15:45:54 +0000 Subject: [PATCH 2/4] =?UTF-8?q?feat(ogar-from-ruff):=20emit=5Fcsharp=20+?= =?UTF-8?q?=20emit=5Fpython=20=E2=80=94=20the=20SDK=20codegen=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Realize §1.6's codegen mode for two more languages on the same &CompiledClass -> String seam emit_rust already defines. - emit_csharp: a 'public sealed record' with 'public const uint ClassId', OgScalar properties, ToOne/ToMany relations (shared syntax). - emit_python: an '@dataclass' with 'CLASSID: ClassVar[int]', OgScalar annotations, ToOne["T"]/ToMany["T"] forward-ref relations ([T] syntax). - Shared assoc_target() relation classifier drives all three emitters; the wrapper-contract type NAMES (OgScalar/ToOne/ToMany) are identical across languages — only the bracket syntax differs. That is the mechanical transliteration §1.6 promises (the SDK is a reader over one compiled spine, not a re-implementation). The classid travels with the class; compute behaviour stays a trailing comment (the 15% adapter). - emit_rust's association loop refactored onto the shared classifier (no behavioural change). +3 tests (emits_csharp_*, emits_python_*, all_three_emitters_share_the_same_ type_vocabulary); 6 emit tests green, clippy clean, rustfmt clean (probe-workspace verified offline — the OGAR workspace's surrealdb-ast git dep is unresolvable offline; ruff_* patched to the local checkout). Doc: substrate §6 'Built' updated (the emitters ship here); §6.2 'Next' narrowed to the runtime SDKs + wrapper-contract packages. Co-Authored-By: Claude --- crates/ogar-from-ruff/src/emit.rs | 248 +++++++++++++++++++++++++++--- docs/OGAR-TRANSPILE-SUBSTRATE.md | 40 +++-- 2 files changed, 258 insertions(+), 30 deletions(-) diff --git a/crates/ogar-from-ruff/src/emit.rs b/crates/ogar-from-ruff/src/emit.rs index daf5e76..67ed8a1 100644 --- a/crates/ogar-from-ruff/src/emit.rs +++ b/crates/ogar-from-ruff/src/emit.rs @@ -2,18 +2,25 @@ //! //! [`mint`](crate::mint) is the pull-in half (source → `CompiledClass`); this //! is the pull-back half: render a [`CompiledClass`] back into a target -//! language's source. The reference emitter here is **Rust** -//! ([`emit_rust`]); it mirrors the role `ogar-adapter-surrealql` plays for -//! SurrealQL DDL. Other targets (`emit_csharp`, `emit_python`, …) follow the -//! same `&CompiledClass -> String` seam. +//! language's source. Three emitters share the `&CompiledClass -> String` +//! seam — [`emit_rust`] (the reference), [`emit_csharp`], and [`emit_python`] +//! — the **codegen mode** of the per-language SDKs (substrate doc §1.6, "Three +//! SDKs, one compiled spine"). They mirror the role `ogar-adapter-surrealql` +//! plays for SurrealQL DDL. //! -//! **The wrapper-contract pivot.** The emitted struct does not inline native +//! **The wrapper-contract pivot.** The emitted class does not inline native //! types — it uses the consumer's thin wrapper-contract types: `OgScalar` for //! a column, `ToOne` / `ToMany` for a relation. So "the language just -//! needs to put a wrapper contract akin to lance-graph" is literal: a Rust -//! consumer provides those three aliases (its wrapper contract) and the -//! emitted, rail-shaped class compiles. The `classid` is emitted as a `const` -//! — the rail address travels with the class. +//! needs to put a wrapper contract akin to lance-graph" is literal: a consumer +//! provides those three aliases (its wrapper contract) and the emitted, +//! rail-shaped class compiles. The `classid` travels with the class as a +//! `const`/`ClassVar`. +//! +//! **All three emitters use the *same* type names** (`OgScalar` / `ToOne` / +//! `ToMany`); only the generic-bracket syntax differs (`` for Rust and C#, +//! `[T]` for Python). That shared vocabulary is exactly what makes an SDK a +//! **mechanical transliteration** of the same compiled spine rather than a +//! re-implementation — the layer-1 / layer-2 story of substrate doc §1.6. //! //! Scalar attributes currently emit `OgScalar` uniformly: the Odoo field type //! (`Char` / `Monetary` / …) is not yet carried on the SPO `Field`, so there @@ -21,10 +28,25 @@ //! follow-up), `OgScalar` refines to the mapped concrete type with no change //! to this seam. -use ogar_vocab::AssociationKind; +use ogar_vocab::{Association, AssociationKind}; use crate::mint::CompiledClass; +/// Shared relation classifier for every emitter: `(comodel PascalCase, is_many)`. +/// `HasMany` / `HasAndBelongsToMany` → many (`ToMany`); everything else +/// (including a future [`AssociationKind`]) → one (`ToOne`). Only the bracket +/// syntax differs per language (`` Rust/C#, `[T]` Python) — the type *names* +/// are identical, which is what lets an SDK be a mechanical transliteration +/// (substrate doc §1.6). +fn assoc_target(assoc: &Association) -> (String, bool) { + let target = pascal_case(assoc.class_name.as_deref().unwrap_or(&assoc.name)); + let is_many = matches!( + assoc.kind, + AssociationKind::HasMany | AssociationKind::HasAndBelongsToMany + ); + (target, is_many) +} + /// Emit a [`CompiledClass`] as Rust source: a struct whose fields use the /// consumer's wrapper-contract types (`OgScalar` / `ToOne` / `ToMany`), /// prefixed by its rail `classid` const and a facet/concept doc line. @@ -52,14 +74,11 @@ pub fn emit_rust(cc: &CompiledClass) -> String { out.push_str(&format!(" pub {}: OgScalar,\n", attr.name)); } for assoc in &cc.class.associations { - let target = pascal_case(assoc.class_name.as_deref().unwrap_or(&assoc.name)); - let field_ty = match assoc.kind { - AssociationKind::BelongsTo | AssociationKind::HasOne => format!("ToOne<{target}>"), - AssociationKind::HasMany | AssociationKind::HasAndBelongsToMany => { - format!("ToMany<{target}>") - } - // A future AssociationKind defaults to a single typed reference. - _ => format!("ToOne<{target}>"), + let (target, is_many) = assoc_target(assoc); + let field_ty = if is_many { + format!("ToMany<{target}>") + } else { + format!("ToOne<{target}>") }; out.push_str(&format!(" pub {}: {field_ty},\n", assoc.name)); } @@ -77,6 +96,115 @@ pub fn emit_rust(cc: &CompiledClass) -> String { out } +/// Emit a [`CompiledClass`] as **C#** source: a `sealed record` whose members +/// use the C# SDK's wrapper-contract types (`OgScalar` / `ToOne` / +/// `ToMany`), with the rail `classid` as a `public const uint ClassId`. The +/// `` generic syntax is shared with Rust; only Python differs (`[T]`). +/// Computed fields are trailing comments — the compute behaviour is the +/// "impossible" 15 % and lands as an adapter, not inline codegen. This is the +/// codegen mode of the C# SDK (substrate doc §1.6); a host compiles the emitted +/// record into an assembly — the strongest "compiled, not parsed" form. +/// +/// Field/member identifiers are emitted **verbatim** (Odoo `snake_case`), +/// matching [`emit_rust`]'s wire fidelity; idiomatic PascalCase member casing +/// is a future refinement on this same seam (cf. the `OgScalar` `field_type` +/// note above). +#[must_use] +pub fn emit_csharp(cc: &CompiledClass) -> String { + let ty = pascal_case(&cc.class.name); + let mut out = String::new(); + + out.push_str(&format!( + "/// Rail class {} — classid 0x{:08X} (concept 0x{:04X}).\n", + cc.class.name, + cc.facet.facet_classid(), + cc.facet.facet_classid() as u16, + )); + out.push_str(&format!("public sealed record {ty}\n{{\n")); + out.push_str(&format!( + " public const uint ClassId = 0x{:08X};\n", + cc.facet.facet_classid(), + )); + for attr in &cc.class.attributes { + out.push_str(&format!( + " public OgScalar {} {{ get; init; }}\n", + attr.name + )); + } + for assoc in &cc.class.associations { + let (target, is_many) = assoc_target(assoc); + let field_ty = if is_many { + format!("ToMany<{target}>") + } else { + format!("ToOne<{target}>") + }; + out.push_str(&format!( + " public {field_ty} {} {{ get; init; }}\n", + assoc.name + )); + } + for c in &cc.class.computed_fields { + out.push_str(&format!( + " // computed: {} <- {}({})\n", + c.field, + c.compute_method, + c.depends.join(", "), + )); + } + out.push_str("}\n"); + + out +} + +/// Emit a [`CompiledClass`] as **Python** source: a `@dataclass` whose +/// annotations use the Python SDK's wrapper-contract types (`OgScalar` / +/// `ToOne[T]` / `ToMany[T]`), with the rail `classid` as a `ClassVar[int]`. +/// Python uses `[T]` subscripts (not ``), and comodels are forward-ref +/// strings since they may be defined later in the module. Computed fields are +/// trailing comments (the 15 % adapter). This is the codegen mode of the Python +/// SDK (substrate doc §1.6); CPython compiles the emitted module to bytecode on +/// import — the "cost of an import" made literal. +#[must_use] +pub fn emit_python(cc: &CompiledClass) -> String { + let ty = pascal_case(&cc.class.name); + let mut out = String::new(); + + out.push_str("@dataclass\n"); + out.push_str(&format!("class {ty}:\n")); + out.push_str(&format!( + " \"\"\"Rail class `{}` — classid 0x{:08X} (concept 0x{:04X}).\"\"\"\n", + cc.class.name, + cc.facet.facet_classid(), + cc.facet.facet_classid() as u16, + )); + out.push_str(&format!( + " CLASSID: ClassVar[int] = 0x{:08X}\n", + cc.facet.facet_classid(), + )); + for attr in &cc.class.attributes { + out.push_str(&format!(" {}: OgScalar\n", attr.name)); + } + for assoc in &cc.class.associations { + let (target, is_many) = assoc_target(assoc); + let field_ty = if is_many { + format!("ToMany[\"{target}\"]") + } else { + format!("ToOne[\"{target}\"]") + }; + out.push_str(&format!(" {}: {field_ty}\n", assoc.name)); + } + for c in &cc.class.computed_fields { + out.push_str(&format!( + " # computed: {} <- {}({})\n", + c.field, + c.compute_method, + c.depends.join(", "), + )); + } + + out +} + /// `account.move` / `account_move` → `AccountMove`. Treats both `.` and `_` /// as word separators (Odoo dotted comodels and underscore-normalised model /// names both arrive here). @@ -168,6 +296,90 @@ mod tests { assert!(rust.contains("// computed: amount_total <- _compute_amount(line_ids.balance)")); } + #[test] + fn emits_csharp_record_with_wrapper_contract_types() { + let cc = &compile_graph_python::(&account_move_graph())[0]; + let cs = emit_csharp(cc); + + // The rail address travels as a const inside the record. + assert!( + cs.contains("public const uint ClassId = 0x00020202;"), + "got:\n{cs}" + ); + // The type is a PascalCase sealed record. + assert!( + cs.contains("public sealed record AccountMove"), + "got:\n{cs}" + ); + // Scalar -> the wrapper's OgScalar (init-only property). + assert!( + cs.contains("public OgScalar name { get; init; }"), + "got:\n{cs}" + ); + // Many2one -> ToOne; One2many -> ToMany (shared syntax). + assert!( + cs.contains("public ToOne partner_id { get; init; }"), + "got:\n{cs}" + ); + assert!( + cs.contains("public ToMany line_ids { get; init; }"), + "got:\n{cs}" + ); + // Computed behaviour is a comment (the 15% lands as an adapter). + assert!( + cs.contains("// computed: amount_total <- _compute_amount(line_ids.balance)"), + "got:\n{cs}" + ); + } + + #[test] + fn emits_python_dataclass_with_wrapper_contract_types() { + let cc = &compile_graph_python::(&account_move_graph())[0]; + let py = emit_python(cc); + + // The rail address travels as a ClassVar. + assert!( + py.contains("CLASSID: ClassVar[int] = 0x00020202"), + "got:\n{py}" + ); + // A PascalCase @dataclass. + assert!(py.contains("@dataclass"), "got:\n{py}"); + assert!(py.contains("class AccountMove:"), "got:\n{py}"); + // Scalar -> OgScalar annotation. + assert!(py.contains(" name: OgScalar"), "got:\n{py}"); + // Relations use [T] subscripts with forward-ref comodels (not ). + assert!( + py.contains(" partner_id: ToOne[\"ResPartner\"]"), + "got:\n{py}" + ); + assert!( + py.contains(" line_ids: ToMany[\"AccountMoveLine\"]"), + "got:\n{py}" + ); + // Computed behaviour is a comment (the 15% adapter). + assert!( + py.contains("# computed: amount_total <- _compute_amount(line_ids.balance)"), + "got:\n{py}" + ); + } + + #[test] + fn all_three_emitters_share_the_same_type_vocabulary() { + // §1.6: the SDK is a transliteration — same type NAMES, only bracket + // syntax differs. Assert the shared vocabulary across all three. + let cc = &compile_graph_python::(&account_move_graph())[0]; + for src in [emit_rust(cc), emit_csharp(cc), emit_python(cc)] { + assert!(src.contains("OgScalar"), "OgScalar in every emitter"); + assert!(src.contains("ToOne"), "ToOne in every emitter"); + assert!(src.contains("ToMany"), "ToMany in every emitter"); + // The same rail classid concept in every emitter. + assert!( + src.contains("0x00020202"), + "classid travels in every emitter" + ); + } + } + #[test] fn pascal_case_handles_dotted_and_underscored() { assert_eq!(pascal_case("account.move.line"), "AccountMoveLine"); diff --git a/docs/OGAR-TRANSPILE-SUBSTRATE.md b/docs/OGAR-TRANSPILE-SUBSTRATE.md index c2faaec..ef9c881 100644 --- a/docs/OGAR-TRANSPILE-SUBSTRATE.md +++ b/docs/OGAR-TRANSPILE-SUBSTRATE.md @@ -374,12 +374,26 @@ correct because of the `relation_kind` predicate (ruff#35): `target` + - `lift_model_graph_python` + the `project_odoo_fields` schema projection (OGAR #131/#132). - **`ogar-from-ruff::mint`** — per-class minting: `mint_graph

`, `CompiledClass`, `compile_graph_python

` (OGAR #132). -- **`ogar-from-ruff::emit`** — the pull-back **codegen** leg, reference - target Rust: `emit_rust(&CompiledClass) -> String` renders a rail struct - whose fields use the consumer's wrapper-contract types (`OgScalar` / - `ToOne` / `ToMany`) + a `*_CLASSID` const (OGAR #132). - `account.move → struct AccountMove { name: OgScalar, partner_id: - ToOne, line_ids: ToMany }`. +- **`ogar-from-ruff::emit`** — the pull-back **codegen** leg, now **three + emitters on one `&CompiledClass -> String` seam** — the codegen mode of the + per-language SDKs (§1.6): + - `emit_rust` (reference, OGAR #132) → `pub struct AccountMove { name: + OgScalar, partner_id: ToOne, line_ids: ToMany }` + + `ACCOUNT_MOVE_CLASSID`. + - `emit_csharp` → `public sealed record AccountMove { public const uint + ClassId = 0x00020202; public OgScalar name {get;init;} public + ToOne partner_id {…} public ToMany line_ids {…} }`. + - `emit_python` → `@dataclass class AccountMove: CLASSID: ClassVar[int] = + 0x00020202; name: OgScalar; partner_id: ToOne["ResPartner"]; line_ids: + ToMany["AccountMoveLine"]`. + + All three use the **same wrapper-contract type names** (`OgScalar` / `ToOne` / + `ToMany`); only the bracket syntax differs (`` Rust/C#, `[T]` Python) — a + shared `assoc_target` relation classifier drives all three (the mechanical + transliteration §1.6 promises). The `classid` travels with the class. The + compute behaviour stays a trailing comment (the 15% adapter). 6 emit tests + (incl. `all_three_emitters_share_the_same_type_vocabulary`), clippy clean + (probe-verified offline). (C#/Python: this PR.) - **`ogar-adapter-surrealql` `array`** — to-many associations (`HasMany`/`HasAndBelongsToMany`) emit as `array>` (OGAR #136, @@ -394,12 +408,14 @@ correct because of the `relation_kind` predicate (ruff#35): `target` + `32×GUID` SoA. Pin the per-carving tier-byte arithmetic against `FacetCascade` first (don't guess). This subsumes hardcoded facet "versions". The `emit_rust` codegen leg is the start; this is the depth. -2. **Pull-back breadth — the C# / Python SDKs (§1.6).** `emit_csharp` / - `emit_python` on the same `&CompiledClass -> String` seam (codegen mode), and - the thin runtime SDKs (layer-1 `FacetCascade` + `CascadeShape` transliterated, - layer-2 ClassView reader, layer-3 adapter host). Layer 1 is mechanical now - that `CascadeShape` is pinned in `lance-graph-contract`. Refine `OgScalar` - once the `field_type` capture lands (ruff follow-up). +2. **Pull-back breadth — the C# / Python SDKs (§1.6).** Codegen mode is + **shipped** (`emit_csharp` / `emit_python`). Remaining: (a) the **thin + runtime SDKs** — layer-1 `FacetCascade` + `CascadeShape` transliterated to + C#/Python (mechanical now that `CascadeShape` is pinned in + `lance-graph-contract`), layer-2 ClassView reader, layer-3 adapter host; and + (b) the wrapper-contract *packages* themselves (the `OgScalar`/`ToOne`/`ToMany` + aliases each language ships, the analog of `lance-graph-contract`). Refine + `OgScalar` once the `field_type` capture lands (ruff follow-up). 3. **Thin the consumer (membrane)** — `odoo-rs` → `compile_graph::` caller + `od-posting` GoBD adapter; delete the native SurrealQL emit fork (W3.3, **CI-gated** — od-ontology pulls surrealdb). Recommended path: keep From ae2c36ed8266a9e23bbcc1cd024ba77956b97f31 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 16:03:12 +0000 Subject: [PATCH 3/4] =?UTF-8?q?docs(transpile-substrate):=20=C2=A71.5=20vi?= =?UTF-8?q?ew-rotations,=20G4D3=20worst-case,=20classid=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the lance-graph #621 review corrections into the substrate doc: - §1.5(a): carvings are VIEW *rotations*, not co-equal recombinations. The two byte-aligned carvings (6×(1:2), 3×(1:2:3:4)) are defaults (group_of = shift); 4×(1:2:3) is the WORST CASE (group_of divides, straddles tiers) — prevented on the common path, kept only as the rare rotation / escape hatch a ClassView may rotate to when a rare Odoo class needs to relieve classid-stacking entropy. - New callout: carvings address the VIEW only; functions are reached by the classid as an additional THINK/DO switch (facet::ClassArm { View, Functions }), never by slicing tier-bytes. A straddling carve to reach a function is exactly the worst case 4×(1:2:3) warns against. - §1.5 status: updated to the shipped API (CascadeShape::ALIGNED / ROTATIONS, shift(), is_byte_aligned(), ClassArm). Co-Authored-By: Claude --- docs/OGAR-TRANSPILE-SUBSTRATE.md | 64 +++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/docs/OGAR-TRANSPILE-SUBSTRATE.md b/docs/OGAR-TRANSPILE-SUBSTRATE.md index ef9c881..4f6f887 100644 --- a/docs/OGAR-TRANSPILE-SUBSTRATE.md +++ b/docs/OGAR-TRANSPILE-SUBSTRATE.md @@ -85,23 +85,40 @@ This is the heart of the power. A `ClassView` is not a parser and not a fixed record format — it is a **compiled, flexible, composable reader** over the facet, baked into the binary. Three properties: -### a. One ClassView *recombines* the facet layout — no "versions" +### a. One ClassView *rotates* the facet layout — no "versions" -The 16-byte facet's tier payload is not locked to a single carving. The -ClassView reads it as whichever hierarchical cascade fits the class, by -recombination: +The 16-byte facet's tier payload is not locked to a single carving. A ClassView +can **always rotate** — read the SAME 12 cascade bytes under a different +grouping — to fit the class. The carvings (pinned as +`lance_graph_contract::facet::CascadeShape`, `CascadeShape::ROTATIONS`): ``` -6× (1:2) 6 tiers, each a 1:2 hierarchy -4× (1:2:3) 4 tiers, each a 1:2:3 hierarchy -3× (1:2:3:4) 3 tiers, each a 1:2:3:4 hierarchy +6× (1:2) ALIGNED default — 6 tiers, each a 1:2 hierarchy (group_of = i >> 1, a shift) +3× (1:2:3:4) ALIGNED default — 3 tier-pairs, each 1:2:3:4 (group_of = i >> 2, a shift) +4× (1:2:3) WORST CASE — straddles tier boundaries (group_of = i / 3, a DIVIDE) ``` -(the same `3×4`-vs-`4×3` family the GUID canon debates, generalised to the -ClassView's reading). So there is **no need for hardcoded facet "versions" -(V1/V2/V3)** — one compiled ClassView subsumes all the carvings. Hardcoding a -format per version is the thing to *delete*; the ClassView is the flexible -reader that makes versions unnecessary. +**Only the byte-aligned carvings are defaults.** `6×(1:2)` and `3×(1:2:3:4)` +keep `group_of` a pure shift (the canon's "tier-of-level is a shift, never a +branch"). **`4×(1:2:3)` is the worst case, not a co-equal carving** — it +straddles tier boundaries so `group_of` must DIVIDE (`CascadeShape::is_byte_aligned()` +is `false`, `shift()` is `None`). It is *prevented on the common path* and kept +only as the **rare rotation / escape hatch**: a ClassView may rotate to it +deliberately when a rare class (some Odoo models) needs to relieve +**classid-stacking entropy** — rotate the reading rather than mint another +classid. So there is **no need for hardcoded facet "versions" (V1/V2/V3)** — one +compiled ClassView subsumes the rotation set; the straddle stays legal only as a +deliberate, rare rotation. Hardcoding a format per version is the thing to +*delete*. + +> **Carvings address the VIEW, never the functions.** A rotation re-reads the +> data layout; it does NOT reach behaviour. Functions are encoded by the +> **classid acting as an additional switch** — `lance_graph_contract::facet::ClassArm` +> `{ View, Functions }`, the OGAR THINK/DO split (`OGAR-AST-CONTRACT.md`). +> Reaching a function = switch the classid to the `Functions` arm (the +> `ActionDef`/`KausalSpec` on the resolved Core node), *never* slice the +> tier-bytes. A straddling carve to "get to" a function is exactly the worst +> case the `4×(1:2:3)` example warns against. ### b. Sub-range mapping + nested ClassViews stacked into constructors @@ -135,17 +152,22 @@ heavier value-SoA is only built when actually needed, then cached. investment is the compiled, nested, lazy `ClassView` over the GUID SoA. When the two compete for attention, the compiled ClassView wins. -> **Status:** the recombination *principle* + the nested-constructor + lazy-SoA +> **Status:** the rotation *principle* + the nested-constructor + lazy-SoA > architecture are operator-specified here as the durable design. The exact -> tier-byte arithmetic of each carving is **now pinned + implemented** as -> `lance_graph_contract::facet::CascadeShape` (`G6D2` / `G4D3` / `G3D4`, with +> tier-byte arithmetic is **now pinned + implemented** as +> `lance_graph_contract::facet::CascadeShape` (`G6D2` / `G4D3` / `G3D4`, > `G·D = CASCADE_UNITS = 12`) over `FacetCascade::tier_bytes()` — `index(g,l) = -> g·D + l`, `group_of`/`level_of` the inverses, plus `cascade_byte` and the -> per-group LCP `cascade_group_shared`. The `G3D4`/`G6D2` `group_of` is a shift -> (the canon's "tier-of-level is a shift, never a branch"); `G4D3` is the -> straddle that needs a divide. Zero-dep, `const fn`, probe-verified. This is -> **one algebra for both the facet bytes and a 12-field class** — the shared -> substrate the three language SDKs (§1.6) all read. +> g·D + l`, `group_of`/`level_of` inverses, `cascade_byte`, per-group LCP +> `cascade_group_shared`. `CascadeShape::ALIGNED = [G3D4, G6D2]` are the +> shift-`group_of` **defaults** (`shift()` is `Some`); `CascadeShape::ROTATIONS` +> is the full rotation set a ClassView may rotate through. **`G4D3` is the worst +> case** — `is_byte_aligned()` is `false`, `shift()` is `None`, `group_of` +> divides — excluded from `ALIGNED`, kept in `ROTATIONS` only as the rare +> escape-hatch rotation (classid-stacking-entropy relief). **Functions are NOT a +> carving** — `facet::ClassArm { View, Functions }` is the classid's additional +> THINK/DO switch; carvings address `View` only. Zero-dep, `const fn`, +> probe-verified (lance-graph #621). One algebra for both the facet bytes and a +> 12-field class — the shared substrate the three language SDKs (§1.6) all read. --- From 9e050432a4766ff3c351a206b1a7a9e66297b0a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 20:37:42 +0000 Subject: [PATCH 4/4] =?UTF-8?q?docs(transpile-substrate):=20=C2=A71.5c=20t?= =?UTF-8?q?he=2032=C3=97GUID=20is=20literal=20capacity=20=E2=80=94=20SoC?= =?UTF-8?q?=20over=20packed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator (2026-06-29): the 512-byte node = 512/16 = 32 sixteen-byte GUID slots (key + edges = 2; value slab = 30). In the worst case you Tetris each concern into its own clean slot rather than bit-pack — preference clean / SoC over packed. The 32-slot capacity is *why* the CascadeShape::G4D3 straddle (packing) is almost never needed, and the headroom behind 'rotate / spread to a fresh slot instead of minting another classid' (the rare classid-stacking-entropy case). Pinned as lance_graph_contract::canonical_node::GUIDS_PER_NODE = 32. Co-Authored-By: Claude --- docs/OGAR-TRANSPILE-SUBSTRATE.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/OGAR-TRANSPILE-SUBSTRATE.md b/docs/OGAR-TRANSPILE-SUBSTRATE.md index 4f6f887..9d76673 100644 --- a/docs/OGAR-TRANSPILE-SUBSTRATE.md +++ b/docs/OGAR-TRANSPILE-SUBSTRATE.md @@ -139,6 +139,20 @@ key prerenders nodes with zero value-decode (canon: "THE GUID IS THE KEY OF KEY-VALUE"); the compiled ClassView lays them out from keys alone, and the heavier value-SoA is only built when actually needed, then cached. +**The `32×GUID` is literal capacity, and it sets the layout doctrine: clean / +SoC over packed** (operator, 2026-06-29). A 512-byte node is exactly +`512 / 16 = 32` sixteen-byte GUID-sized slots (`key` + `edges` take 2; the +480-byte value slab is the remaining 30) — pinned as +`lance_graph_contract::canonical_node::GUIDS_PER_NODE = 32` (compile-time +asserted). So in the worst case you **Tetris whatever you need across the +slots** — give each concern its own clean slot — rather than bit-pack two +concerns into one. Packing into a single facet via a straddle (`CascadeShape::G4D3`, +§1.5a) is the dispreferred last resort precisely *because* there are 32 slots: +the capacity is what makes separation-of-concerns the default and the straddle +unnecessary. (It is also the headroom behind §1.5a's "rotate / spread to a fresh +slot instead of minting another classid" for the rare classid-stacking-entropy +case.) + ### Why this is the power (and where SurrealQL sits) - **Compiled beats parsed.** The business logic is a `ClassView` compiled into