Skip to content
Closed
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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,51 @@ honest *convention* fallback (`record<invoice_line>`); the parallel
`slice_2_typed_lift.rs` proves the *lift* resolves it to `account_move_line`.
Both stay in the test set — the diff between them is the value the bridge adds.

## Pulling the canonical classid (OGAR)

Under the `ogar-emit` feature, `od-ontology` pulls the canonical OGAR classid
for each Odoo model straight from `ogar_vocab::ports::OdooPort` — no bridge
object, no registry, no TTL hydration. A pure static lookup over the shared
codebook (OGAR #94). This is the "pull OGAR via class" consumer-migration target
(lance-graph #589).

### API

```rust
// feature = "ogar-emit"
concept_classid("account_move") // Some(0x0202) COMMERCIAL_DOCUMENT
concept_classid("account_analytic_line") // Some(0x0103) BILLABLE_WORK_ENTRY
render_classid("account_move") // Some(0x0002_0202) APP 0x0002 (Odoo lens) ‖ concept
schema_classids(&schema) // Vec<(table, Option<u16>)>
```

### APP‖class codebook

The `classid` is a 32-bit value: `APP (high u16) ‖ concept (low u16)`. The **low
u16** is WHAT it is — the shared cross-app concept (RBAC + ontology). The **high
u16** is WHOSE render — the per-app lens; Odoo's is `0x0002`.

### Convergence pin

Planning times align with billable hours through one codebook lookup:

| Surface | Model / entity | Concept classid |
|---|---|---|
| Odoo (ERP) | `account.analytic.line` | `0x0103` `BILLABLE_WORK_ENTRY` |
| WoA / SMB | `Stundenzettel` | `0x0103` `BILLABLE_WORK_ENTRY` |
| OpenProject / Redmine | `TimeEntry` | `0x0103` `BILLABLE_WORK_ENTRY` |

### Name-form caveat

The SPO corpus names models in table form (`account_move`); `OdooPort` aliases
are model form (`account.move`); the bridge normalizes `_`→`.` (the canonical
Odoo `_name.replace('.', '_')` inverted). This is lossless for the codebook's
dot-separated single-word segments; Odoo localization and module classes with an
underscore inside a segment (`l10n_*`, `im_livechat_*`) are intentionally out of
scope and resolve to `None` — a fail-safe miss, never a wrong id.

---

## The cut tail (deferred)

A **codegen / migration convenience layer** — export the typed AST, snapshot it
Expand Down
14 changes: 14 additions & 0 deletions crates/od-ontology/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ serde_json = "1"
# (OdooPort). `ogar-adapter-surrealql` default features are dep-light: its
# emit path is `ogar-vocab` + a hand-written formatter (zero surrealdb-core),
# the same "mirror the catalog shape" design this crate already uses.
# Pin verification (2026-06-22, integration review R5): rev 08a9c979 IS the
# #94 merge commit — the earliest rev carrying OdooPort + all 9 ODOO_ALIASES
# (incl. account.analytic.line -> BILLABLE_WORK_ENTRY 0x0103). OGAR main has
# 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 }

Expand All @@ -42,6 +48,14 @@ name = "od-codegen"
path = "src/bin/od_codegen.rs"
required-features = ["cli"]

# End-to-end "pull OGAR via class" demo. Uses the `ogar-emit`-gated classid
# surface (concept_classid / render_classid / schema_classids /
# emit_via_ogar_annotated), so it only builds under the feature — otherwise a
# default `cargo build --examples` would fail on the gated symbols.
[[example]]
name = "classid_pull"
required-features = ["ogar-emit"]

[lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = "warn"
63 changes: 63 additions & 0 deletions crates/od-ontology/examples/classid_pull.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! End-to-end "pull OGAR via class": from an SPO corpus to canonical classids
//! and annotated `SurrealQL` DDL — the classid stamped into the `DEFINE TABLE`
//! `COMMENT` clause, so it survives into `SurrealDB`'s own catalog.
//!
//! ```bash
//! cargo run -p od-ontology --example classid_pull --features ogar-emit
//! ```
//!
//! This is the consumer-migration target spelled out (lance-graph #589): the
//! canonical id is pulled for each Odoo model straight from `OdooPort` — no
//! bridge, no registry, no hydration. `account_analytic_line` resolves to the
//! SAME `BILLABLE_WORK_ENTRY` id WoA/SMB `Stundenzettel` and `OpenProject` /
//! `Redmine` `TimeEntry` resolve to — planner times align with billable hours
//! through one codebook lookup.

use od_ontology::{
concept_classid, corpus_to_schema, emit_via_ogar_annotated, parse_ndjson, render_classid,
schema_classids,
};

fn main() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../data/slice_2.spo.ndjson");
let ndjson = std::fs::read_to_string(path).expect("slice_2 fixture present");
let triples = parse_ndjson(&ndjson).expect("fixture parses");
let schema = corpus_to_schema(
&triples,
Some(&[
"account_move",
"account_move_line",
"res_partner",
"res_company",
]),
None,
);

// 1. The table → canonical OGAR classid map (concept low-u16 + full render u32).
println!("== canonical classids (table → concept / render) ==");
for (table, concept) in schema_classids(&schema) {
match concept {
Some(lo) => {
let render = render_classid(&table).expect("render present when concept present");
println!(" {table:<22} concept=0x{lo:04X} render=0x{render:08X}");
}
None => println!(" {table:<22} (uncodified — no canonical analogue)"),
}
}

// 2. The planner↔ERP convergence pin (a pure codebook lookup — needs no
// corpus, so it resolves even though account.analytic.line isn't in the
// slice-2 focus set above).
if let Some(id) = concept_classid("account_analytic_line") {
println!(
"\n== convergence pin ==\n \
account_analytic_line → 0x{id:04X} (BILLABLE_WORK_ENTRY)\n \
≡ WoA/SMB Stundenzettel ≡ OpenProject/Redmine TimeEntry"
);
}

// 3. The annotated DDL — the render classid rides into the DEFINE TABLE
// COMMENT clause (queryable later via `INFO FOR TABLE`).
println!("\n== annotated SurrealQL DDL (classid in COMMENT) ==");
print!("{}", emit_via_ogar_annotated(&schema));
}
34 changes: 34 additions & 0 deletions crates/od-ontology/specs/UPSTREAM_WISHLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,40 @@ we can adopt your choice when it lands.
schema-emit consumer. Does ClassView's design cleanly generalize, or
is it tesseract-shaped (single-language-target by construction)?

## PROBE-OGAR-ID-TO-CONCEPT-NAME (P2 · gates the classid→COMMENT name enrichment)

> Filed 2026-06-22 (classid-consume sprint, convergence-architect review R4).
> Concrete pass/fail successor to open-question #2 above. Decoupled from the
> hex-id stamp, which already shipped — see below.

**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
SurrealDB's own catalog (queryable via `INFO FOR TABLE`). This needs only the
forward `OdooPort::class_id` (name → id) we already consume.

**The gap this probe captures:** to put the human-readable concept *name*
(`COMMERCIAL_DOCUMENT`) in that COMMENT — and to collapse `schema_classids`
into a derived view over `Class::canonical_id()` by populating
`Class.canonical_concept` at lowering time — odoo-rs needs a **reverse**
`u16 → &'static str` lookup that `OdooPort`/`class_ids` do NOT expose today
(`OdooPort::aliases()` is forward-only `&[(&str, u16)]`).

- **Capability under test:** an OGAR-side `class_ids::name_of(0x0202) ==
Some("COMMERCIAL_DOCUMENT")` (or equivalent reverse index), derivable from
the existing `class_ids` const module / `CODEBOOK`.
- **PASS:** `table_to_class` sets `class.canonical_concept` (+ a
`COMMERCIAL_DOCUMENT (classid:0x00020202)` COMMENT); `schema_classids`
becomes a one-liner over the populated shells (no second traversal); the
asymmetry guard holds — `sale.order` resolves to `COMMERCIAL_DOCUMENT`,
**not** lexical `order` (this is the regression that proves the fusion safe).
- **FAIL / stay deferred:** OGAR exposes no reverse map → keep the forward-only
`concept_classid` surface and the hex-only COMMENT (the current shipped state).
- **Decoupling note:** the hex-id COMMENT (shipped) has **no** dependency on
this probe; only the concept-*name* enrichment + the `canonical_concept`
fusion wait on it. They are the *same* gate (verified: both need the reverse
lookup), so one OGAR capability unblocks both.

## Current state of odoo-rs (so the design session knows what to design against)

- `corpus_to_schema` projects SPO triples → typed `Schema { tables,
Expand Down
39 changes: 37 additions & 2 deletions crates/od-ontology/src/bin/od_codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,39 @@ fn main() {
process::exit(2);
}

// ── 4. `--classids` — print table → OGAR render classid map ──
if parsed.classids {
#[cfg(feature = "ogar-emit")]
{
for table in &schema.tables {
match od_ontology::render_classid(&table.name) {
Some(id) => println!("{}\t0x{id:08X}", table.name),
None => println!("{}\t(uncodified)", table.name),
}
}
return;
}
#[cfg(not(feature = "ogar-emit"))]
{
eprintln!(
"error: --classids requires the `ogar-emit` feature \
(rebuild: cargo build -p od-ontology --features cli,ogar-emit)"
);
process::exit(1);
}
}

let sql = schema.to_sql();

// ── 4. `--validate` (deferred stub) ──
// ── 5. `--validate` (deferred stub) ──
if parsed.validate {
eprintln!(
"warning: --validate is a deferred stub (surrealdb-core parser not wired yet); \
passing through unverified"
);
}

// ── 5. Write ──
// ── 6. Write ──
if let Err(e) = write_output(&parsed.output, &sql) {
eprintln!("error writing output: {e}");
process::exit(1);
Expand All @@ -181,6 +203,7 @@ struct ParsedArgs {
relations: Option<PathBuf>,
stats: bool,
validate: bool,
classids: bool,
help: bool,
}

Expand All @@ -203,13 +226,15 @@ fn parse_args(args: &[String]) -> Result<ParsedArgs, String> {
let mut relations: Option<PathBuf> = None;
let mut stats = false;
let mut validate = false;
let mut classids = false;
let mut help = false;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"-h" | "--help" => help = true,
"--stats" => stats = true,
"--validate" => validate = true,
"--classids" => classids = true,
"-f" | "--focus" => {
i += 1;
let value = args.get(i).ok_or_else(|| {
Expand Down Expand Up @@ -251,6 +276,7 @@ fn parse_args(args: &[String]) -> Result<ParsedArgs, String> {
relations,
stats,
validate,
classids,
help,
})
}
Expand Down Expand Up @@ -312,6 +338,8 @@ OPTIONS:
--validate Reserved — when wired, routes the emitted DDL through
`surrealdb_core::syn::parse` and exits 2 on syntax error.
Currently a no-op stub (warns and passes through).
--classids Print the `table → canonical OGAR render classid (0xAABBCCDD)`
map instead of DDL. Requires the `ogar-emit` feature.
-h, --help Show this help.

EXIT CODES (lance-graph#512 convention):
Expand All @@ -333,6 +361,13 @@ mod tests {
assert!(p.relations.is_none());
assert!(!p.stats);
assert!(!p.validate);
assert!(!p.classids);
}

#[test]
fn parse_args_classids_flag() {
let p = parse_args(&["--classids".into()]).unwrap();
assert!(p.classids);
}

#[test]
Expand Down
10 changes: 8 additions & 2 deletions crates/od-ontology/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,16 @@ mod triple;

#[cfg(feature = "ogar-emit")]
pub use ogar_bridge::{
concept_classid, emit_via_ogar, render_classid, schema_classids, schema_to_classes,
ODOO_APP_PREFIX,
concept_classid, emit_via_ogar, emit_via_ogar_annotated, render_classid, schema_classids,
schema_to_classes, ODOO_APP_PREFIX,
};

/// Re-export the canonical OGAR codebook constants (e.g.
/// `class_ids::BILLABLE_WORK_ENTRY`) so consumers can symbol-bind to the
/// shared ids rather than copy hex literals.
#[cfg(feature = "ogar-emit")]
pub use ogar_vocab::class_ids;

pub use emit::corpus_to_schema;
pub use inheritance::InheritanceMap;
pub use recompute_dag::{MethodId, MethodKind, RecomputeDag};
Expand Down
Loading