diff --git a/README.md b/README.md index c9936c4..df4f997 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,27 @@ render_classid("account_move") // Some(0x0002_0202) APP 0x0002 (Odoo schema_classids(&schema) // Vec<(table, Option)> ``` +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 diff --git a/crates/od-ontology/Cargo.toml b/crates/od-ontology/Cargo.toml index 65dfe55..c21161b 100644 --- a/crates/od-ontology/Cargo.toml +++ b/crates/od-ontology/Cargo.toml @@ -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). diff --git a/crates/od-ontology/specs/UPSTREAM_WISHLIST.md b/crates/od-ontology/specs/UPSTREAM_WISHLIST.md index 6841b4b..924b8a6 100644 --- a/crates/od-ontology/specs/UPSTREAM_WISHLIST.md +++ b/crates/od-ontology/specs/UPSTREAM_WISHLIST.md @@ -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 diff --git a/crates/od-ontology/src/ogar_bridge.rs b/crates/od-ontology/src/ogar_bridge.rs index d454d4c..f8af947 100644 --- a/crates/od-ontology/src/ogar_bridge.rs +++ b/crates/od-ontology/src/ogar_bridge.rs @@ -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}; @@ -68,18 +69,20 @@ 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 -/// today — the 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 = schema @@ -87,11 +90,18 @@ pub fn emit_via_ogar_annotated(schema: &Schema) -> String { .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 ''`). - // 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 ''`). Uncodified tables + // keep `description = None` → no classid clause. + if let Some(lo) = concept_classid(&table.name) { + let render = render_classid_for::(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 }) @@ -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_`. -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 @@ -154,12 +169,16 @@ pub fn concept_classid(model: &str) -> Option { } /// 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 { - concept_classid(model).map(|lo| (u32::from(ODOO_APP_PREFIX) << 16) | u32::from(lo)) + concept_classid(model).map(render_classid_for::) } /// Resolve every table in `schema` to its canonical OGAR concept classid, @@ -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!(