Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,27 @@ render_classid("account_move") // Some(0x0002_0202) APP 0x0002 (Odoo
schema_classids(&schema) // Vec<(table, Option<u16>)>
```

The APP prefix (`OdooPort::APP_PREFIX`, OGAR #97) and the `(prefix << 16) |
concept` composition (`ogar_vocab::app::render_classid_for`) come from the Core
— never a local literal or a hand-rolled shift.

### Annotated DDL — classid in the catalog

`emit_via_ogar_annotated(&schema)` lowers each table onto `ogar_vocab::Class`
and emits via the canonical `ogar-adapter-surrealql`, stamping each codebook
table's **concept name + classid** into the `DEFINE TABLE … COMMENT` clause —
so it rides into SurrealDB's own catalog (queryable via `INFO FOR TABLE`),
human-readable, not just the `.surql` text:

```surql
DEFINE TABLE account_move SCHEMAFULL COMMENT 'commercial_document (classid:0x00020202)';
```

The concept name comes from `ogar_vocab::canonical_concept_name` (OGAR #98's
`id → name` reverse map), never re-derived locally. The COMMENT carries
**identity only** — never lifecycle/behavior (per OGAR's SurrealQL-AST-trap
governance, #99: SurrealQL is an adapter, not a spine).

### APP‖class codebook

The `classid` is a 32-bit value: `APP (high u16) ‖ concept (low u16)`. The **low
Expand Down
10 changes: 8 additions & 2 deletions crates/od-ontology/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ serde_json = "1"
# since advanced to 5ee87b5 (post-#96); the intervening #95/#96 commits are
# additive/test-only (no OdooPort API change), so no bump is required. A bump
# is a deliberate, reviewable, re-gated step — not a maintenance default.
ogar-vocab = { git = "https://github.com/AdaWorldAPI/OGAR", rev = "08a9c979d8939e1303770ea32aa2010928e79318", optional = true }
ogar-adapter-surrealql = { git = "https://github.com/AdaWorldAPI/OGAR", rev = "08a9c979d8939e1303770ea32aa2010928e79318", optional = true }
# BUMPED 2026-06-22 to OGAR main 7e677185 (#98 merged): adds
# `canonical_concept_name` (the id -> concept-name reverse map) so
# emit_via_ogar_annotated can stamp the readable concept into the catalog
# COMMENT. The bump crosses #95 (medcare, additive) + #96 (convergence-pin
# test) + #97 + #98 (reverse map); OdooPort + the 9 ODOO_ALIASES are
# byte-identical across the whole range (re-verified).
ogar-vocab = { git = "https://github.com/AdaWorldAPI/OGAR", rev = "7e6771858e5138e066b13d93e9791f7db4df6077", optional = true }
ogar-adapter-surrealql = { git = "https://github.com/AdaWorldAPI/OGAR", rev = "7e6771858e5138e066b13d93e9791f7db4df6077", optional = true }

[features]
# Opt-in CLI (mirrors openproject-nexgen-rs#31's `--features cli` discipline).
Expand Down
21 changes: 21 additions & 0 deletions crates/od-ontology/specs/UPSTREAM_WISHLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,27 @@ we can adopt your choice when it lands.
> Concrete pass/fail successor to open-question #2 above. Decoupled from the
> hex-id stamp, which already shipped — see below.

> **✓ REVERSE MAP + NAME-IN-COMMENT SHIPPED 2026-06-22.** OGAR #98 landed
> `canonical_concept_name(u16) -> Option<&'static str>` (round-trip-verified
> over all 39 CODEBOOK entries). `emit_via_ogar_annotated` now consumes it:
> the COMMENT carries the readable concept name + classid —
> `COMMENT 'commercial_document (classid:0x00020202)'` — via
> `ogar_vocab::canonical_concept_name`, never re-derived locally. The APP
> prefix + `(prefix<<16)|concept` composition likewise route through OGAR #97
> (`OdooPort::APP_PREFIX`, `app::render_classid_for`). **Still follow-on:** the
> `Class.canonical_concept`-on-shells fusion that collapses `schema_classids`
> into a derived view over `Class::canonical_id()` — R4 graded that
> WORTH-EXPLORING (it changes the shared lowering path), and it carries the
> `sale.order` lexical-vs-alias asymmetry as its own guard. Not blocking; the
> shipped readable COMMENT already delivers the operator-facing value.
>
> **#99 reframing (SurrealQL-AST-trap governance):** W3.3 ("delete the
> `surreal_ast`/`triple`/`recompute_dag` fork") is NOT "grow the canonical
> adapter to emit `DEFINE EVENT`" — that would re-enter the trap (SurrealQL AST
> carrying lifecycle). The fork's reactive layer must move to OGAR
> `Class + ActionDef`; the adapter emits identity/structure DDL only. This
> probe's COMMENT work is identity-layer and stays clear of that boundary.

**Already shipped, no capability needed:** `emit_via_ogar_annotated`
(`ogar_bridge.rs`) stamps the full APP‖class render id into the
`DEFINE TABLE … COMMENT 'classid:0x00020202'` clause, so the id rides into
Expand Down
70 changes: 45 additions & 25 deletions crates/od-ontology/src/ogar_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@
//! - **`DEFINE INDEX`** (`_sql_constraints` unique tuples): not part of the
//! OGAR IR; dropped.

use ogar_vocab::app::render_classid_for;
use ogar_vocab::ports::{OdooPort, PortSpec};
use ogar_vocab::{Association, AssociationKind, Attribute, Class};
use ogar_vocab::{canonical_concept_name, Association, AssociationKind, Attribute, Class};

use crate::surreal_ast::{FieldDefinition, Kind, Schema, TableDefinition};

Expand All @@ -68,30 +69,39 @@ pub fn emit_via_ogar(schema: &Schema) -> String {
ogar_adapter_surrealql::emit_surrealql_ddl(&schema_to_classes(schema))
}

/// [`emit_via_ogar`] DDL, but with each codebook table's canonical OGAR render
/// classid stamped into its `DEFINE TABLE … COMMENT 'classid:0xAABBCCDD'` clause
/// — so the id rides into `SurrealDB`'s own catalog metadata (queryable via
/// `INFO FOR TABLE`), not just the emitted `.surql` text. A `--` line-comment
/// would evaporate at parse time; the `COMMENT` clause survives ingestion.
/// [`emit_via_ogar`] DDL, with each codebook table's canonical OGAR concept
/// **name** + full render classid stamped into its `DEFINE TABLE …
/// COMMENT 'commercial_document (classid:0x00020202)'` clause — so the shared
/// concept rides into `SurrealDB`'s own catalog metadata (queryable via
/// `INFO FOR TABLE`), human-readable, not just the emitted `.surql` text. A
/// `--` line-comment would evaporate at parse time; the `COMMENT` clause
/// survives ingestion.
///
/// Purely additive over [`schema_to_classes`]: a table outside the codebook
/// gets no classid comment (its structural DDL is byte-identical to the native
/// [`emit_via_ogar`]). Only the full APP‖class render id (the hex) is stamped
/// todaythe human-readable concept *name* awaits an OGAR-side
/// `id → concept-name` reverse lookup (see `specs/UPSTREAM_WISHLIST.md`,
/// `PROBE-OGAR-ID-TO-CONCEPT-NAME`).
/// [`emit_via_ogar`]). The concept name comes from
/// [`ogar_vocab::canonical_concept_name`]OGAR's `id → name` reverse map
/// (the `PROBE-OGAR-ID-TO-CONCEPT-NAME` capability, OGAR #98) — never
/// re-derived or copied locally, per the Core-First doctrine.
#[must_use]
pub fn emit_via_ogar_annotated(schema: &Schema) -> String {
let classes: Vec<Class> = schema
.tables
.iter()
.map(|table| {
let mut class = table_to_class(table);
// Identity stamp: the render classid rides into the catalog COMMENT
// (the emitter renders `class.description` as `… COMMENT '<desc>'`).
// Uncodified tables keep `description = None` → no classid clause.
if let Some(id) = render_classid(&table.name) {
class.description = Some(format!("classid:0x{id:08X}"));
// Identity stamp: the canonical concept name + full render classid
// ride into the catalog COMMENT (the emitter renders
// `class.description` as `… COMMENT '<desc>'`). Uncodified tables
// keep `description = None` → no classid clause.
if let Some(lo) = concept_classid(&table.name) {
let render = render_classid_for::<OdooPort>(lo);
class.description = Some(match canonical_concept_name(lo) {
Some(name) => format!("{name} (classid:0x{render:08X})"),
// `lo` came from the codebook, so the reverse lookup is
// total — the bare-hex arm is a defensive fallback only.
None => format!("classid:0x{render:08X}"),
});
}
class
})
Expand All @@ -117,7 +127,12 @@ pub fn emit_via_ogar_annotated(schema: &Schema) -> String {
/// `u16` is the shared cross-app concept (WHAT it is — RBAC + ontology); the
/// high `u16` is the per-app render lens (WHOSE template). `0x0002` is Odoo's
/// lens, so every Odoo-rendered id is `0x0002_<concept>`.
pub const ODOO_APP_PREFIX: u16 = 0x0002;
///
/// **Bound to the Core** (OGAR #97): this is [`OdooPort::APP_PREFIX`], not a
/// local literal — the prefix allocation lives once, in OGAR's `PortSpec`. If
/// OGAR ever re-allocates Odoo's prefix this follows automatically; there is
/// no second copy to drift.
pub const ODOO_APP_PREFIX: u16 = OdooPort::APP_PREFIX;

/// Pull the canonical OGAR **concept** classid (the shared low `u16`) for an
/// Odoo model name, straight through [`OdooPort`] — the static alias table, no
Expand Down Expand Up @@ -154,12 +169,16 @@ pub fn concept_classid(model: &str) -> Option<u16> {
}

/// The full 32-bit **render** classid for an Odoo model: Odoo's APP prefix
/// ([`ODOO_APP_PREFIX`], `0x0002`) in the high `u16`, the shared canonical
/// [`concept_classid`] in the low `u16`. `account_move` → `0x0002_0202`;
/// `account_analytic_line` → `0x0002_0103`. `None` for an unaliased model.
/// (`0x0002`) in the high `u16`, the shared canonical [`concept_classid`] in
/// the low `u16`. `account_move` → `0x0002_0202`; `account_analytic_line` →
/// `0x0002_0103`. `None` for an unaliased model.
///
/// Composition is OGAR's canonical [`render_classid_for`] (OGAR #97), not a
/// hand-rolled shift — the `(prefix << 16) | concept` layout lives in one
/// place (the Core), never re-implemented per consumer.
#[must_use]
pub fn render_classid(model: &str) -> Option<u32> {
concept_classid(model).map(|lo| (u32::from(ODOO_APP_PREFIX) << 16) | u32::from(lo))
concept_classid(model).map(render_classid_for::<OdooPort>)
}

/// Resolve every table in `schema` to its canonical OGAR concept classid,
Expand Down Expand Up @@ -409,12 +428,13 @@ mod tests {
events: Vec::new(),
};
let ddl = emit_via_ogar_annotated(&schema);
// Codebook hit: account_move render classid 0x0002_0202 rides into the
// catalog COMMENT (single-quoted SurrealQL string literal), so it
// survives SurrealDB ingestion rather than evaporating as a `--` line.
// Codebook hit: account_move carries the canonical concept NAME +
// render classid 0x0002_0202 in a catalog COMMENT (single-quoted
// SurrealQL literal), so it survives SurrealDB ingestion rather than
// evaporating as a `--` line. The name comes from OGAR's reverse map.
assert!(
ddl.contains("COMMENT 'classid:0x00020202'"),
"account_move must carry its render classid in a COMMENT clause; got:\n{ddl}"
ddl.contains("COMMENT 'commercial_document (classid:0x00020202)'"),
"account_move must carry its concept name + classid in a COMMENT clause; got:\n{ddl}"
);
// Codebook miss: ir_cron stays unstamped — exactly one classid clause.
assert_eq!(
Expand Down