|
| 1 | +# OGAR as the Per-Class Transpile Substrate |
| 2 | + |
| 3 | +> **Read this to understand the power.** OGAR is not "a codebook" or "a DTO |
| 4 | +> store" — it is a **bidirectional transpiler** whose unit of currency is the |
| 5 | +> *per-class, rail-shaped, language-agnostic compiled class*. This doc names |
| 6 | +> the whole machine: how a class is pulled IN from any source language, minted |
| 7 | +> into a rail address, and pulled BACK into any consumer language through a |
| 8 | +> thin wrapper contract. Companion to `OGAR-AS-IR.md` (the compiler framing) |
| 9 | +> and the `#133` handover (the ERP/planning landing plan). |
| 10 | +
|
| 11 | +--- |
| 12 | + |
| 13 | +## 0. The power in one paragraph |
| 14 | + |
| 15 | +OGAR compiles business logic from any source language (Python/Odoo, |
| 16 | +Ruby/Rails, C#, …) into **per-class compiled classes**, each addressed by a |
| 17 | +16-byte **rail facet** whose `classid` is a cross-app join key. ~**85 %** of a |
| 18 | +consumer's logic — the mechanical, data-shaped part (fields, relations, |
| 19 | +computed values, validations, the schema) — lives in OGAR as these minted |
| 20 | +classes. A consumer in **any** language pulls a class back through a thin |
| 21 | +**wrapper contract** (`lance-graph-contract` is Rust's) and reimplements |
| 22 | +**nothing**. The **"impossible" 15 %** — intrusive, stateful, or |
| 23 | +genuinely-language-specific logic — is a small per-language **adapter + |
| 24 | +ClassView + ontological grounding**. One canonical class, N languages, |
| 25 | +cross-app convergence, **at the cost of an import.** "ERP, OpenProject, … |
| 26 | +for everything" falls out of the codebook, not out of per-app reimplementation. |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## 1. The two legs (the transpiler is bidirectional) |
| 31 | + |
| 32 | +``` |
| 33 | + ┌─────────────────────── OGAR substrate ───────────────────────┐ |
| 34 | + PULL-IN │ │ PULL-BACK |
| 35 | + (source → OGAR) │ ModelGraph ──lift──► ogar_vocab::Class (the schema) │ (OGAR → language) |
| 36 | + │ │ ──mint──► Facet (16B rail addr) (the address) │ |
| 37 | + Python/Odoo ─┐ │ │ │ ┌─► Rust : import lance-graph-contract |
| 38 | + ruff_python_spo │ └───────────────► CompiledClass { class, facet } ──────┼───┤ (ClassView + FacetCascade, runtime) |
| 39 | + Ruby/Rails ─┤ ruff_* │ ▲ │ ├─► C# : a thin C# wrapper contract |
| 40 | + ruff_ruby_spo ──IR──► │ │ pulled by classid │ ├─► Python: a thin Python wrapper contract |
| 41 | + C#/… ─┘ (shared) │ │ │ └─► any DDL/codegen: ogar-adapter-* |
| 42 | + └────────────────────────────────────────────────────────────────┘ |
| 43 | +``` |
| 44 | + |
| 45 | +**Pull-in** — `source → ogar-from-<lang> → ModelGraph → lift + mint → CompiledClass`: |
| 46 | + |
| 47 | +| step | crate / fn | output | |
| 48 | +|---|---|---| |
| 49 | +| parse | `ruff_python_spo` / `ruff_ruby_spo` (ruff frontends) | `ruff_spo_triplet::ModelGraph` (the shared, language-neutral IR) | |
| 50 | +| schema | `ogar-from-ruff::lift_model_graph_python` (+ `..._ruby`) | `Vec<ogar_vocab::Class>` — attributes / associations / computed_fields | |
| 51 | +| address | `ogar-from-ruff::mint::mint_graph<P>` | `ruff_spo_address::Mint` — a 16-byte `Facet` per node | |
| 52 | +| compile | `ogar-from-ruff::mint::compile_graph_python<P>` | `Vec<CompiledClass { class, facet }>` — **the unit a consumer pulls** | |
| 53 | + |
| 54 | +**Pull-back** — a consumer obtains a `CompiledClass` by either: |
| 55 | + |
| 56 | +- **(a) runtime wrapper contract** — the consumer imports a thin contract and |
| 57 | + resolves the class by `classid` at runtime. `lance-graph-contract` is the |
| 58 | + Rust contract: `ClassView` (the schema/render surface), `FacetCascade` (the |
| 59 | + 16-byte address), `ActionDef`/`KausalSpec` (behaviour). No codegen — the |
| 60 | + class is *data* interpreted through the contract's traits. A C#/Python |
| 61 | + consumer ships an analogous thin contract. |
| 62 | +- **(b) codegen emit adapter** — OGAR emits the class as target-language |
| 63 | + source/DDL. `ogar-adapter-surrealql` (`Class → SurrealQL DDL`) is the |
| 64 | + reference emitter; per-language emitters (`ogar-emit-rust`, …) follow the |
| 65 | + same `CompiledClass → String` seam. |
| 66 | + |
| 67 | +The two modes are not rivals: (a) is the live "pull a class and render it" |
| 68 | +path; (b) is the "materialise the class as source for a build target" path. |
| 69 | +Both consume the same `CompiledClass`. |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## 2. Why it is a *substrate*, not a dump — the addressing |
| 74 | + |
| 75 | +A minted class is **addressable without decoding its value**. That is the |
| 76 | +whole power: a renderer/router/planner lays out, groups, and skeleton-renders |
| 77 | +classes from the 16-byte key alone. |
| 78 | + |
| 79 | +### 2.1 The classid (the cross-app join key) |
| 80 | + |
| 81 | +``` |
| 82 | +render classid (u32) = (APP_PREFIX as u32) << 16 | concept (u16) |
| 83 | + └── high u16 ──┘ └── low u16 ──┘ |
| 84 | + the app RENDER skin the SHARED concept |
| 85 | +``` |
| 86 | + |
| 87 | +- **low u16 = the shared concept** — resolved by `PortSpec::class_id` against |
| 88 | + the OGAR codebook (`ogar_vocab::class_ids` / `ogar_codebook`). This is the |
| 89 | + de-facto `owl:equivalentClass` expressed as a u16. |
| 90 | +- **high u16 = the app render skin** — `PortSpec::APP_PREFIX`. Picks the |
| 91 | + per-app `ClassView` / template; **carries no behaviour**. |
| 92 | +- composed by the canonical `ogar_vocab::app::render_classid_for::<P>(concept)`. |
| 93 | + |
| 94 | +**Cross-app convergence is the payoff.** The same concept across apps gets the |
| 95 | +same low u16, so a consumer joins across apps with a `==`: |
| 96 | + |
| 97 | +| concept | id | Odoo (`0x0002`) | OpenProject (`0x0001`) | Redmine (`0x0007`) | WoA/SMB (`0x0003`/`0x0004`) | |
| 98 | +|---|---|---|---|---|---| |
| 99 | +| `commercial_document` | `0x0202` | `account.move`, `sale.order` | — | — | `Rechnung`/`Invoice`/`Vorgang` | |
| 100 | +| `project_work_item` | `0x0102` | — | `WorkPackage` | `Issue` | — | |
| 101 | +| `billable_work_entry` | `0x0103` | `account.analytic.line` | `TimeEntry` | `TimeEntry` | `Stundenzettel` | |
| 102 | + |
| 103 | +`billable_work_entry` is the **named first cross-domain bridge**: a logged |
| 104 | +unit of work is one concept whether it arrives from the planning arm |
| 105 | +(OpenProject) or the commerce arm (Odoo). Pinned in |
| 106 | +`ogar_vocab::ports::tests::billable_work_entry_converges_across_all_five_ports`. |
| 107 | + |
| 108 | +### 2.2 The 16-byte rail facet (`FacetCascade` — the "V3" schema) |
| 109 | + |
| 110 | +``` |
| 111 | +facet_classid : u32 rows 0 ← the classid above |
| 112 | +tiers[0..6] : FacetTier rows 1-3 ← 6× (lo:hi) = (is_a : part_of) byte pairs |
| 113 | + hi_chain() = part_of cascade (containment) |
| 114 | + lo_chain() = is_a cascade (inheritance) |
| 115 | +``` |
| 116 | + |
| 117 | +- 16 bytes, content-blind, SIMD-transpose-native. `ruff_spo_address::Facet` |
| 118 | + and `lance_graph_contract::facet::FacetCascade` are **byte-identical** — |
| 119 | + the mint's bytes round-trip losslessly into the Foundry's facet (proven by |
| 120 | + the cross-crate round-trip probe). |
| 121 | +- `prefix_distance()` = `8 − shared_prefix_tiles()` → O(1) hierarchy distance, |
| 122 | + no value decode. |
| 123 | +- The mint builds two forests from the SPO triples — `part_of` (inverted |
| 124 | + `has_field`/`has_function`) and `is_a` (`inherits_from`, fallback |
| 125 | + `rdf:type`) — and stamps each node's coarse→fine rank chains. |
| 126 | + |
| 127 | +### 2.3 Compression never costs addressability |
| 128 | + |
| 129 | +A node is `key(128/GUID) + value`. Lance may compress the value arbitrarily |
| 130 | +(columnar, dictionary, PQ); the key is never compressed and never needs the |
| 131 | +value decoded to route. (Canon: "THE GUID IS THE KEY OF KEY-VALUE.") |
| 132 | + |
| 133 | +--- |
| 134 | + |
| 135 | +## 3. The 85 / 15 split (the consumer model) |
| 136 | + |
| 137 | +> The operator's framing: *"in case of lance-graph we would still pull odoo-rs |
| 138 | +> but 85 % would be in the OGAR transpile substrate, and the transcode then is |
| 139 | +> just a generic compiler-store caller with some adapters."* |
| 140 | +
|
| 141 | +- **85 % — mechanical, minted into OGAR.** Fields, relations, computed values, |
| 142 | + validations, the schema. The consumer is a **thin compiler-store caller**: |
| 143 | + `compile_graph::<P>(graph)` → pull `CompiledClass` → render via `ClassView`. |
| 144 | +- **15 % — "impossible", a per-language adapter.** Intrusive / stateful / |
| 145 | + truly language-specific logic that does not fit a clean mold becomes a |
| 146 | + **custom adapter + ClassView + ontological grounding**. Worked example: |
| 147 | + `odoo-rs`'s `od-posting` — the GoBD double-entry posting host (gapless |
| 148 | + Belegnummer + inalterability hash chain) — stays a hand-written Rust adapter; |
| 149 | + *everything else* of `account.move` is minted. |
| 150 | + |
| 151 | +This is the **Core-First Transcode Doctrine** (`.claude/knowledge` / |
| 152 | +`core-first-transcode-doctrine.md`) restated for consumers: mechanical leaf |
| 153 | +methods → thin `classid`-keyed adapters that **assume the Core**; intrusive |
| 154 | +methods → hand-port; a Core gap → **extend the Core deliberately**, never hack |
| 155 | +the adapter. |
| 156 | + |
| 157 | +### What a thinned consumer looks like (the #2 target) |
| 158 | + |
| 159 | +``` |
| 160 | +odoo-rs (today) odoo-rs (thinned) |
| 161 | +───────────────── ────────────────── |
| 162 | +od-ontology bespoke Schema+triples od-ontology = a compile_graph::<OdooPort> caller |
| 163 | +schema_to_classes → DDL (pulls CompiledClass from the substrate) |
| 164 | +od-posting GoBD logic od-posting = unchanged (the 15% adapter) |
| 165 | +alignment FIBO/DOLCE seed grounding = resolved late via classid → ClassView → OGIT |
| 166 | + + a thin wrapper contract (lance-graph-contract) |
| 167 | +``` |
| 168 | + |
| 169 | +The consumer shrinks to **(import the substrate) + (a compile_graph call) + |
| 170 | +(the GoBD adapter) + (a wrapper contract)**. That is the "cost of an import." |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +## 4. Grounding: resolve, don't store |
| 175 | + |
| 176 | +FIBO / DOLCE / OGIT grounding is **not** stored on the facet or the codebook |
| 177 | +rows. The contract is deliberate (`lance-graph-contract`): *"the meta-DTO |
| 178 | +resolves; it does not store."* Grounding is resolved **late** via |
| 179 | +`classid → ClassView → OGIT registry`: |
| 180 | + |
| 181 | +- the OGIT hydrator inheritance chain `odoo → fibo-fnd → dolce` |
| 182 | + (`lance-graph-ontology::hydrators`), plus `classify_odoo(model)` → DOLCE |
| 183 | + category (e.g. `account.move` → `Perdurant`); |
| 184 | +- the FIBO pivot (`account.move` ⇒ `fibo:Transaction`) lives in |
| 185 | + `od-ontology::alignment::ODOO_SEED` + `odoo-to-fibo.ttl`. |
| 186 | + |
| 187 | +So **"retain grounding" = keep the `classid` correct** (never ship `0` outside |
| 188 | +the bootstrap address). A 16-byte facet suffices for a richly-grounded class |
| 189 | +because the grounding is one resolve away, not copied onto every row. (This is |
| 190 | +why the `#133` handover's "add `{ogit_uri, dolce_category, fibo_equivalent}` |
| 191 | +to codebook rows" gap was **declined** — it fights this design.) |
| 192 | + |
| 193 | +--- |
| 194 | + |
| 195 | +## 5. Worked example — `account.move` |
| 196 | + |
| 197 | +``` |
| 198 | +account.move (Odoo Python) |
| 199 | + └─ ruff_python_spo ─► ModelGraph { ns: "odoo", model "account_move" } |
| 200 | + fields: name(Char), partner_id(Many2one res.partner), |
| 201 | + line_ids(One2many account.move.line, inverse move_id), |
| 202 | + amount_total(Monetary, compute=_compute_amount, depends line_ids.balance) |
| 203 | + ├─ lift_model_graph_python ─► Class "account_move" |
| 204 | + │ attributes: [name] |
| 205 | + │ associations: [partner_id → BelongsTo res.partner, |
| 206 | + │ line_ids → HasMany account.move.line (inverse move_id)] |
| 207 | + │ computed_fields: [amount_total ← _compute_amount, depends [line_ids.balance]] |
| 208 | + └─ mint_graph::<OdooPort> ─► Facet |
| 209 | + facet_classid = 0x0002_0202 (Odoo 0x0002 | commercial_document 0x0202) |
| 210 | + ⇒ CompiledClass { class, facet } |
| 211 | +
|
| 212 | + Cross-checks (all probe-verified): |
| 213 | + • facet.to_bytes() ≡ lance_graph_contract::FacetCascade(0x0002_0202) (byte-exact) |
| 214 | + • canonical_concept_domain(0x0202) == Commerce (routes to the right ClassView) |
| 215 | + • grounding resolvable: 0x0202 → fibo:Transaction / DOLCE Perdurant (late, via OGIT) |
| 216 | + • the GoBD posting (account.move._post) stays od-posting's Rust adapter (the 15%) |
| 217 | +``` |
| 218 | + |
| 219 | +The **relation-aware** shape (`Many2one` vs `Many2many` vs `One2many`) is only |
| 220 | +correct because of the `relation_kind` predicate (ruff#35): `target` + |
| 221 | +`inverse_name` alone cannot separate a Many2one from a Many2many. |
| 222 | + |
| 223 | +--- |
| 224 | + |
| 225 | +## 6. What is built / what is next |
| 226 | + |
| 227 | +**Built (this arc):** |
| 228 | +- `ruff_python_spo` — Odoo/Python SPO frontend (ruff #34). |
| 229 | +- `relation_kind` predicate — Many2one/One2many/Many2many cardinality (ruff #35). |
| 230 | +- `lift_model_graph_python` + the `project_odoo_fields` schema projection (OGAR #131/#132). |
| 231 | +- **`ogar-from-ruff::mint`** — per-class minting: `mint_graph<P>`, |
| 232 | + `CompiledClass`, `compile_graph_python<P>` (OGAR #132). |
| 233 | + |
| 234 | +**Next (the transpiler direction):** |
| 235 | +1. **Pull-back emit** — `ogar-emit-<lang>` adapters (`CompiledClass → <lang>`), |
| 236 | + mirroring `ogar-adapter-surrealql`. Plus the thin runtime wrapper-contract |
| 237 | + pattern for C#/Python (lance-graph-contract is the Rust reference). |
| 238 | +2. **Thin the consumer** — `odoo-rs` collapses to a `compile_graph::<OdooPort>` |
| 239 | + caller + the `od-posting` GoBD adapter (the 15%). |
| 240 | +3. **Scale** — run the `odoo_blueprint` 404 entities through `compile_graph`; |
| 241 | + over-cap god-models (`≥ 256` members) branch via the SoC lint |
| 242 | + (`ruff_spo_address::soc`), never widen. |
| 243 | + |
| 244 | +--- |
| 245 | + |
| 246 | +## 7. For a future session — how to extend (the four moves) |
| 247 | + |
| 248 | +| To add… | Do this | Convergence is… | |
| 249 | +|---|---|---| |
| 250 | +| a **source language** | a `ruff_<lang>_spo` frontend → `ModelGraph`; reuse `lift` + `mint` | automatic (shared IR) | |
| 251 | +| a **target language** | an `ogar-emit-<lang>` adapter (`CompiledClass → String`) **or** a thin runtime wrapper contract (traits mirroring `lance-graph-contract`) | the consumer reimplements nothing | |
| 252 | +| a **concept** | a `class_ids` codebook entry + a `PortSpec` alias | automatic across all ports that map it | |
| 253 | +| 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 | |
| 254 | + |
| 255 | +**Iron rules that bind this surface** (don't relearn the hard way): |
| 256 | +- `classid` is **pure address**; the magic is what it resolves to. Neither u16 |
| 257 | + half carries behaviour (`ActionDef`/`KausalSpec` is a property of the Core |
| 258 | + node, never the address). See `OGAR-CONSUMER-BEST-PRACTICES.md`. |
| 259 | +- **Pull, never re-mint.** The codebook is single-source |
| 260 | + (`ogar_vocab::class_ids`); a consumer pulls via `*Port::class_id`, never |
| 261 | + copies the table or constructs a `*Bridge`. |
| 262 | +- **SurrealQL is an adapter, not a spine.** Behaviour flows |
| 263 | + producer → OGAR `Class` + `ActionDef` → adapter; never producer → DDL. |
| 264 | +- **Resolve, don't store** (grounding) — §4. |
| 265 | +- **No serialization in the hot path** (the Firewall, ADR-022/023); the IR is |
| 266 | + wire-truth. |
| 267 | + |
| 268 | +--- |
| 269 | + |
| 270 | +*Authored alongside the `ogar-from-ruff::mint` per-class minting (OGAR #132). |
| 271 | +The pull-in + mint legs are shipped and probe-verified; the pull-back emit and |
| 272 | +consumer-thinning legs (§6.1, §6.2) are the next deliverables.* |
0 commit comments