|
| 1 | +# Odoo digest → OGIT TTL templates → agnostic relive |
| 2 | + |
| 3 | +> **For colleagues building anything that needs Odoo as a typed ontology |
| 4 | +> outside the Odoo Python runtime.** This is the architectural shape: |
| 5 | +> digest Odoo source once into OGIT-shaped TTL templates (stored |
| 6 | +> in-tree at `vocab/imports/ogit/NTO/<Domain>/`), then relive any model |
| 7 | +> or workflow action agnostically via `ogar-render-askama`. |
| 8 | +> |
| 9 | +> Status: **FRAMING v0** (2026-06-22). Companion to |
| 10 | +> `docs/ODOO-TRANSCODING.md` (the producer spec) and |
| 11 | +> `docs/VERB-AS-CLASS-TEMPLATE.md` (the askama-template framing this |
| 12 | +> reuses). |
| 13 | +
|
| 14 | +The shape in one sentence: **`ogar-from-python` digests Odoo source |
| 15 | +once into the OGAR IR; `ttl_emit` writes the IR back as OGIT-shaped |
| 16 | +TTL templates stored in the existing OGIT NTO tree; consumers |
| 17 | +re-instantiate any model or workflow action by rendering against the |
| 18 | +TTL via `ogar-render-askama`, never touching Odoo Python.** |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## §1. The pipeline |
| 23 | + |
| 24 | +``` |
| 25 | + Odoo Python source |
| 26 | + (addons/<module>/models/*.py) |
| 27 | + │ |
| 28 | + │ ogar-from-python (AST schema filter) |
| 29 | + │ — keeps structural arm: _name, _inherit, fields.*, selections |
| 30 | + │ — keeps behavioural-arm SIGNATURES (decorators, action def names) |
| 31 | + │ — drops bodies (computed methods, action implementations) |
| 32 | + ▼ |
| 33 | + OGAR Class IR (in memory) |
| 34 | + │ |
| 35 | + │ ttl_emit::emit_entity (semantic bijection) |
| 36 | + │ — entity-as-class for models |
| 37 | + │ — verb-as-class for workflow action signatures |
| 38 | + ▼ |
| 39 | + OGIT-shaped TTL templates |
| 40 | + (vocab/imports/ogit/NTO/<Domain>/<DigestedClass>.ttl) |
| 41 | + — dcterms:creator = bus-compiler (digester provenance) |
| 42 | + — alongside upstream arago TTL (Viktor Voss et al) |
| 43 | + │ |
| 44 | + │ ogar-render-askama (entity render → views; verb render → actions) |
| 45 | + ▼ |
| 46 | + Materialized output (any consumer, any medium) |
| 47 | + — HTML view (per-app skin) |
| 48 | + — JSON / OpenAPI surface |
| 49 | + — SurrealQL DDL / Postgres CREATE TABLE |
| 50 | + — SPO triple emit + ACL gate + audit record (action render) |
| 51 | +``` |
| 52 | + |
| 53 | +The Python runtime is **only** touched at digest time. Consumers |
| 54 | +(`woa-rs`, `smb-office-rs`, `medcare-rs`, `q2`, any future renderer) |
| 55 | +never depend on Odoo Python, only on TTL + the askama renderer. |
| 56 | + |
| 57 | +## §2. Why store digests in OGIT NTO (not a parallel `vocab/imports/odoo/`) |
| 58 | + |
| 59 | +The `dcterms:creator` author-scan (`OGIT-DOMAIN-LIFT-CATALOGUE.md |
| 60 | +§ Verifying domain authorship`) gives clean provenance without |
| 61 | +needing a separate storage namespace: |
| 62 | + |
| 63 | +| `dcterms:creator` value | Meaning | Who can change | |
| 64 | +|---|---|---| |
| 65 | +| `Viktor Voss`, `chris.boos@almato.com`, `fotto@arago.de`, … | Upstream arago/almato | Re-vendor requires arago PR; OGAR consumes via the SHA pin | |
| 66 | +| `bus-compiler`, `family-codec-smith`, `Claude (...)`, … | Internal agent digest | Re-run the digester; no external coordination | |
| 67 | + |
| 68 | +The precedent exists today: `vocab/imports/ogit/NTO/Accounting/` |
| 69 | +already has 11 files from a prior `Claude (AdaWorldAPI/lance-graph |
| 70 | +3-hop optim)` digest sitting alongside Viktor Voss's 23 originals. The |
| 71 | +two co-exist, the author-scan distinguishes them, and lifting both |
| 72 | +into OGAR's `Class` IR is symmetric. |
| 73 | + |
| 74 | +So **the digest lives where the concept belongs** — `account.move` → |
| 75 | +`Accounting/`, `sale.order` → `SalesDistribution/`, `stock.picking` → |
| 76 | +`Transport/` — and the author scan is the discriminator. |
| 77 | + |
| 78 | +## §3. The four shapes the digester produces |
| 79 | + |
| 80 | +| Shape | Source pattern | TTL form | Render path | |
| 81 | +|---|---|---|---| |
| 82 | +| **Entity-as-class** | `class Foo(models.Model): _name = '...'; <fields>` | `rdfs:Class` + `mandatory-attributes` enumerating field names | view render (`ogar-render-askama::views`) | |
| 83 | +| **Datatype attribute** | `field_name = fields.Char/Integer/Selection(...)` (with selection) | `owl:DatatypeProperty` + `ogit:validation-type "fixed"` + `validation-parameter "a,b,c"` | binding validation | |
| 84 | +| **Association** | `partner_id = fields.Many2one('res.partner', ...)` | `owl:ObjectProperty` (lifted as SGO verb) OR an entry in the parent class's `ogit:allowed (...)` block | edge in lifted SPO graph | |
| 85 | +| **Verb-as-class** | `def action_confirm(self)` workflow method | `rdfs:Class` + slot list (subject = SaleOrder, object = confirmation event) + policy attrs (`requires-perm`, `emits-audit`) | action render (`ogar-render-askama::actions`, queued) | |
| 86 | + |
| 87 | +The last shape is what makes "relive agnostically" possible for |
| 88 | +workflow actions: the Python method body stays in Odoo (behavioural |
| 89 | +arm), but the **action contract** (what it requires, what it emits, |
| 90 | +what it depends on) becomes a typed template any consumer can render |
| 91 | +against. |
| 92 | + |
| 93 | +## §4. The mapping table — v0 from the existing codebook |
| 94 | + |
| 95 | +The six commerce/ERP concepts already minted in `ogar-vocab::class_ids` |
| 96 | +give the digester its first six targets. Concepts beyond these need |
| 97 | +the 5+3 codebook pass (per `docs/APP-CLASS-CODEBOOK-LAYOUT.md §4`) |
| 98 | +before mint; the digester can lift them in the meantime with |
| 99 | +`Class.name` carrying the Odoo model name and no concept_id assigned. |
| 100 | + |
| 101 | +| Odoo model | Source addon path | OGIT NTO target | OGAR concept | Shape | Concept-mint status | |
| 102 | +|---|---|---|---|---|---| |
| 103 | +| `res.partner` | `addons/base/models/res_partner.py` | `Accounting/` | `BILLING_PARTY` `0x0204` | entity-as-class | minted | |
| 104 | +| `account.move` | `addons/account/models/account_move.py` | `Accounting/` | `COMMERCIAL_DOCUMENT` `0x0202` | entity-as-class | minted | |
| 105 | +| `account.move.line` | `addons/account/models/account_move_line.py` | `Accounting/` | `COMMERCIAL_LINE_ITEM` `0x0201` | entity-as-class | minted | |
| 106 | +| `account.tax` | `addons/account/models/account_tax.py` | `Accounting/` | `TAX_POLICY` `0x0203` | entity-as-class | minted | |
| 107 | +| `account.payment` | `addons/account/models/account_payment.py` | `Accounting/` | `PAYMENT_RECORD` `0x0205` | entity-as-class | minted | |
| 108 | +| `res.currency` | `addons/base/models/res_currency.py` | `Accounting/` | `CURRENCY_POLICY` `0x0206` | entity-as-class | minted | |
| 109 | +| `sale.order` | `addons/sale/models/sale_order.py` | `SalesDistribution/` | TBD `0x02??` | entity-as-class | needs mint | |
| 110 | +| `sale.order.line` | `addons/sale/models/sale_order_line.py` | `SalesDistribution/` | TBD | entity-as-class | needs mint | |
| 111 | +| `stock.picking` | `addons/stock/models/stock_picking.py` | `Transport/` | TBD | entity-as-class | needs mint | |
| 112 | +| `hr.employee` | `addons/hr/models/hr_employee.py` | `HR/` | TBD | entity-as-class | needs mint | |
| 113 | +| `crm.lead` | `addons/crm/models/crm_lead.py` | (new NTO?) | TBD | entity-as-class | needs mint + domain decision | |
| 114 | +| `product.product` | `addons/product/models/product_product.py` | (new NTO?) | TBD | entity-as-class | needs mint + domain decision | |
| 115 | +| `account.move::action_post` | workflow method on `account.move` | `Accounting/verbs/Post.ttl` | TBD | **verb-as-class** | needs mint | |
| 116 | +| `sale.order::action_confirm` | workflow method on `sale.order` | `SalesDistribution/verbs/Confirm.ttl` | TBD | **verb-as-class** | needs mint | |
| 117 | +| `stock.picking::action_assign` | workflow method on `stock.picking` | `Transport/verbs/Assign.ttl` | TBD | **verb-as-class** | needs mint | |
| 118 | + |
| 119 | +The table grows additively. Each new Odoo addon module digested by |
| 120 | +the v0 producer adds rows; concept-mint passes work in parallel. |
| 121 | + |
| 122 | +## §5. The drift detector — Foundry's "ontology change management" for free |
| 123 | + |
| 124 | +``` |
| 125 | +Day 1 — digest Odoo at SHA-A |
| 126 | + ogar-from-python addons/account → vocab/imports/ogit/NTO/Accounting/*.ttl |
| 127 | + git commit (the TTL set is the frozen contract) |
| 128 | +
|
| 129 | +Day N — Odoo upstream releases SHA-B |
| 130 | + ogar-from-python addons/account → /tmp/odoo-shaB-digest/ |
| 131 | + diff -r vocab/imports/ogit/NTO/Accounting/ /tmp/odoo-shaB-digest/ |
| 132 | +
|
| 133 | + Any output line is a structural change Odoo just made: |
| 134 | + - added field → diff shows a new ogit:optional-attributes entry |
| 135 | + - renamed column → diff shows the rename |
| 136 | + - extended selection → diff shows the new validation-parameter values |
| 137 | + - changed _inherit → diff shows the rdfs:subClassOf rewire |
| 138 | + - dropped a model → diff shows file deletion |
| 139 | +``` |
| 140 | + |
| 141 | +Same diff fires in CI on the same PR if a contributor edits an Odoo |
| 142 | +model without re-running the digest. **The TTL templates are the |
| 143 | +contract; the digest re-run is the audit; the diff is the gate.** |
| 144 | +Foundry sells this as "ontology change management" for a recurring |
| 145 | +license fee. |
| 146 | + |
| 147 | +## §6. What blocks doing it today |
| 148 | + |
| 149 | +| Piece | Status | |
| 150 | +|---|---| |
| 151 | +| Storage location (`vocab/imports/ogit/NTO/<Domain>/`) | exists; 72 domains imported, MARS oracle proven | |
| 152 | +| TTL emitter for the structural arm | exists (`ttl_emit::emit_entity`); semantic bijection proven on 29 MARS + 176 SGO TTLs | |
| 153 | +| Verb-as-class template surface | exists (WorkOrder convention; `docs/VERB-AS-CLASS-TEMPLATE.md`) | |
| 154 | +| Author-provenance discriminator | exists (`dcterms:creator` scan in `OGIT-DOMAIN-LIFT-CATALOGUE.md`) | |
| 155 | +| `ogar-render-askama::actions` (verb-as-class render path) | **does not exist** — ~200 LOC mirroring the existing `views/` path | |
| 156 | +| `ogar-from-python` (the digester) | **does not exist** — needs `libcst` or `rustpython-parser` to walk Python AST; ~1500 LOC for the structural-arm filter | |
| 157 | +| Concept mints for non-Accounting Odoo models | needs the 5+3 codebook pass per `APP-CLASS-CODEBOOK-LAYOUT.md` | |
| 158 | + |
| 159 | +`ogar-from-python` and `ogar-render-askama::actions` are independent |
| 160 | +and can ship in parallel PRs. Concept mints are the slow path |
| 161 | +(codebook discipline) and don't block the digest — a digested model |
| 162 | +without a minted concept_id just gets `Class.name = "sale.order"` and |
| 163 | +gets the id assigned later. |
| 164 | + |
| 165 | +## §7. The Foundry-parity collapse |
| 166 | + |
| 167 | +This section is the punchline. Foundry's platform pitch decomposes |
| 168 | +into four layers; each layer maps to a free, open-source piece in |
| 169 | +this architecture: |
| 170 | + |
| 171 | +| Foundry layer | Vendor cost | Our equivalent | Marginal cost | |
| 172 | +|---|---|---|---| |
| 173 | +| Ingest (vendor pipelines) | $ | `ogar-from-python` digest run (one-shot per Odoo upgrade) | engineer-hours per digester ~1500 LOC | |
| 174 | +| Storage (vendor platform) | $$ | `vocab/imports/ogit/NTO/<Domain>/` TTL templates with `dcterms:creator` provenance | zero | |
| 175 | +| Render (vendor UI) | $$ | `ogar-render-askama::{views, actions}` | engineer-hours per render path ~200 LOC each | |
| 176 | +| Access control / audit (vendor IAM) | $$$ | verb-as-class `requires-perm` slot + `emits-audit` + Lance-version-as-audit (ADR-013) | zero (the substrate already does it) | |
| 177 | +| Ontology change management (vendor feature) | $$$ | `diff -r` of digest output (§5) | zero | |
| 178 | + |
| 179 | +The substrate eats every layer the platform sells, **using artifacts |
| 180 | +that already exist in this repo**, in less than 2000 lines of new |
| 181 | +code. The architecture has been latent the whole time — your |
| 182 | +"digest → relive agnostically" framing is what makes it visible as |
| 183 | +one shape. |
| 184 | + |
| 185 | +## §8. Cross-references |
| 186 | + |
| 187 | +- `docs/ODOO-TRANSCODING.md` — the existing producer spec (sections 1-18: |
| 188 | + module discovery, field type mapping, attribute kwargs, association |
| 189 | + kwargs, enum sources, class-level metadata, state machines, `_inherit` |
| 190 | + resolution, decorator mapping, CRUD overrides, registered prefixes, |
| 191 | + conformance corpus). The "how" of the digest. |
| 192 | +- `docs/VERB-AS-CLASS-TEMPLATE.md` — the askama-template framing the |
| 193 | + digest reuses for workflow actions. |
| 194 | +- `docs/HIRO-IN-CLASSES.md` — the bardioc-efficiency story (the same |
| 195 | + digest pattern applied to MARS; this doc generalises it to Odoo). |
| 196 | +- `docs/FOUNDRY-ODOO-MARS-LENS.md` — the three-postures cross-reading |
| 197 | + (MARS frozen schema, Odoo extensible source, Foundry vendor platform). |
| 198 | +- `docs/OGIT-DOMAIN-LIFT-CATALOGUE.md` — coverage register; new Odoo |
| 199 | + digests advance the rows from Imported → Lift-tested → Cross-walked. |
| 200 | +- `docs/APP-CLASS-CODEBOOK-LAYOUT.md` — the 5+3 mint protocol for |
| 201 | + new concept_ids (the §6 slow path). |
| 202 | +- `crates/ogar-from-schema/` — the schema-arm producer (TTL today; |
| 203 | + XSD queued) that the Odoo digester pairs with on the structural |
| 204 | + boundary. |
| 205 | +- `crates/ogar-render-askama/` — the askama renderer (views today; |
| 206 | + actions queued). |
0 commit comments