Skip to content

Commit 57c2691

Browse files
committed
docs(ogar): OGAR as the per-class transpile substrate — the power, in one doc
For any future session to understand the power. Names the whole machine: - The two legs of the bidirectional transpiler: PULL-IN (source → ogar-from-<lang> → ModelGraph → lift + mint → CompiledClass) and PULL-BACK (a consumer obtains the CompiledClass via a thin runtime wrapper contract — lance-graph-contract is Rust's — or a codegen emit adapter — ogar-adapter-surrealql is the DDL reference). - The addressing that makes it a substrate, not a dump: classid = (APP_PREFIX<<16)|concept (cross-app join key, the low u16 is the shared concept, the high u16 the render skin), the 16-byte FacetCascade (V3 6×part_of:is_a), cross-app convergence (account.move ↔ WorkPackage ↔ TimeEntry/Stundenzettel/account.analytic.line). - The 85/15 split: ~85% mechanical logic minted into OGAR (consumer = a compile_graph caller); the "impossible" 15% = a per-language adapter + ClassView + ontological grounding (od-posting GoBD is the example). - Grounding = resolve, don't store (classid → ClassView → OGIT; FIBO/DOLCE never copied onto rows). - A worked account.move example (→ 0x0002_0202), the built/next state, and the four moves to extend (source lang / target lang / concept / port). Indexed in CLAUDE.md's doc family so a future session finds it at session start. Companion to OGAR-AS-IR.md (the compiler framing) and the #133 handover (the ERP/planning landing plan). Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0ef3925 commit 57c2691

2 files changed

Lines changed: 283 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,17 @@ alignment costs. Until measured: 3×4 stands.
208208
slot to `KausalSpec`, a new lowering pass, or any other IR-surface
209209
change. The framing changes no existing decision; it changes every
210210
future one.
211+
10. `docs/OGAR-TRANSPILE-SUBSTRATE.md`**the power, in one doc.** OGAR
212+
as the bidirectional per-class transpiler: pull-in (`source →
213+
ogar-from-<lang> → ModelGraph → lift + mint → CompiledClass`),
214+
rail-facet addressing (`classid = (APP_PREFIX<<16)|concept`, the 16-byte
215+
`FacetCascade`, cross-app convergence), pull-back (runtime wrapper
216+
contract like `lance-graph-contract`, or codegen emit like
217+
`ogar-adapter-surrealql`), and the **85/15 split** (mechanical logic
218+
minted into OGAR; the "impossible" 15% = a per-language adapter +
219+
ClassView + ontological grounding). READ to understand why a consumer
220+
collapses to "a compiler-store caller + adapters, at the cost of an
221+
import." Worked example: `account.move → 0x0002_0202`.
211222
8. `docs/ARCHITECTURAL-DECISIONS-2026-06-04.md` — ADR-001..025
212223
(ADR-026 pending).
213224
9. `.claude/agents/` — the 5+3 hardening pattern (5 research savants +

docs/OGAR-TRANSPILE-SUBSTRATE.md

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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

Comments
 (0)