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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ jobs:
# gating by feature keeps the default build lean.
- name: cargo test -p ogar-adapter-surrealql --features surrealdb-parser
run: cargo test -p ogar-adapter-surrealql --features surrealdb-parser
# Exercise the `surrealql-hint` feature on ogar-knowable-from
# — auto-renders the schema_ddl_hint via the adapter's
# emit_surrealql_ddl on register_class_knowable_from. Closes the
# self-describing-registry loop (ADR-023 receipt).
- name: cargo test -p ogar-knowable-from --features surrealql-hint
run: cargo test -p ogar-knowable-from --features surrealql-hint
7 changes: 7 additions & 0 deletions crates/ogar-knowable-from/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ description = "OGAR-side producer seam for the §10.3 `knowable_from` meet-point
[features]
default = []
serde = ["dep:serde", "ogar-vocab/serde"]
# Auto-render the `schema_ddl_hint` parameter from the `Class` via
# `ogar-adapter-surrealql::emit_surrealql_ddl(&[class.clone()])` on
# every `register_class_knowable_from` call. Opt-in because the
# adapter pulls the SurrealDB-related dep graph; the default path
# stays lightweight (only `ogar-vocab` + optional `serde`).
surrealql-hint = ["dep:ogar-adapter-surrealql"]

[dependencies]
ogar-vocab = { path = "../ogar-vocab" }
ogar-adapter-surrealql = { path = "../ogar-adapter-surrealql", optional = true }
serde = { workspace = true, optional = true }
99 changes: 93 additions & 6 deletions crates/ogar-knowable-from/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,25 @@ pub trait KnowableFromStore: Send + Sync {
/// `NiblePath` identity, same VART-as-reference-backend pattern
/// (see crate-level "Reference backends").
///
/// # `schema_ddl_hint` — the self-describing-registry loop
///
/// With the **`surrealql-hint` feature ON**, this function renders the
/// SurrealQL DDL for the class (via
/// `ogar-adapter-surrealql::emit_surrealql_ddl`) and passes it to
/// `store.register(class_identity, Some(ddl))`. The registry then
/// carries the producer's view of the class shape alongside the
/// `knowable_from` stamp — *"the registry is self-describing"* per
/// the `KnowableFromStore::register` docstring, no longer aspirational.
///
/// With the feature OFF (the default), `None` is passed — the
/// lightweight path. The feature is opt-in because the
/// `ogar-adapter-surrealql` dep pulls the SurrealDB AST surface into
/// the build graph.
///
/// Aligns with ADR-023 (IR-as-wire-truth): the canonical `Class` IR
/// is the wire-truth carrier; the DDL string is its serialization for
/// the registry. See `docs/ARCHITECTURAL-DECISIONS-2026-06-04.md`.
///
/// [1]: https://docs.rs/ogar-ontology
pub fn register_class_knowable_from<S: KnowableFromStore>(
class: &Class,
Expand All @@ -241,11 +260,23 @@ pub fn register_class_knowable_from<S: KnowableFromStore>(
ogar_ontology::class_identity)".into(),
));
}
// v1 minimum-shape: pass None for schema_ddl_hint. Future PRs can
// render via ogar-adapter-surrealql::emit_surrealql_ddl(&[class.clone()])
// — the `class` parameter is retained for that future expansion.
let _ = class; // keep the parameter live for forward compatibility
store.register(class_identity, None)
// The `schema_ddl_hint` parameter — `None` by default (lightweight
// path), auto-rendered via `ogar-adapter-surrealql::emit_surrealql_ddl`
// when the `surrealql-hint` feature is on. This closes the loop the
// `KnowableFromStore::register` docstring named: the trait carries a
// `schema_ddl_hint: Option<&str>` slot so the registry is
// self-describing; PR #32 landed the producer; this is where it
// gets wired into the call site.
#[cfg(feature = "surrealql-hint")]
{
let ddl = ogar_adapter_surrealql::emit_surrealql_ddl(std::slice::from_ref(class));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape OGAR class names before emitting DDL hints

When surrealql-hint is enabled this now renders a DDL hint for every registered Class, but register_class_knowable_from accepts OGAR class names such as sale.order via the documented ogit-erp/sale.order identity path. Those names are not safe SurrealQL bare identifiers, and emit_surrealql_ddl currently formats them directly as DEFINE TABLE {class.name}, so enabling this feature for Odoo-style classes stores an invalid schema hint instead of a self-describing registry entry. Please normalize or quote identifiers before passing the hint, or only enable the hint for classes already known to be SurrealQL identifier-safe.

Useful? React with 👍 / 👎.

store.register(class_identity, Some(ddl.as_str()))
}
#[cfg(not(feature = "surrealql-hint"))]
{
let _ = class; // keep parameter live; not used in the default path
store.register(class_identity, None)
}
}

/// Errors from the [`KnowableFromStore`] operations and the
Expand Down Expand Up @@ -344,7 +375,10 @@ mod tests {
let calls = store.register_calls.lock().unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "ogit-erp/Account");
assert!(calls[0].1.is_none(), "v1 minimum-shape passes None for schema_ddl_hint");
// The schema_ddl_hint axis is feature-gated: dedicated tests
// for each path are below (`default_path_passes_none_for_…` and
// `surrealql_hint_feature_renders_ddl_into_registry`). This
// test stays axis-agnostic on the hint.
}

#[test]
Expand Down Expand Up @@ -461,4 +495,57 @@ mod tests {
assert!(format!("{b}").contains("backend error"));
assert!(format!("{b}").contains("nope"));
}

// ── `schema_ddl_hint` loop closure (ADR-023 / IR-as-wire-truth) ──
// Feature-off (default): `None` is passed to store.register, the
// lightweight path. Feature-on (`surrealql-hint`): the function
// renders the DDL via `ogar-adapter-surrealql::emit_surrealql_ddl`
// and passes it as `Some(&ddl)`. Two tests, conditionally compiled.
// ─────────────────────────────────────────────────────────────────

#[cfg(not(feature = "surrealql-hint"))]
#[test]
fn default_path_passes_none_for_schema_ddl_hint() {
let c = Class::new("Account");
let store = MockKnowableFromStore::new(0);
register_class_knowable_from(&c, "ogit-erp/Account", &store).unwrap();
let calls = store.register_calls.lock().unwrap();
assert_eq!(calls.len(), 1);
// Default path: the registry receives no DDL hint.
assert!(
calls[0].1.is_none(),
"default (feature-off) path must pass None for schema_ddl_hint, got: {:?}",
calls[0].1
);
}

#[cfg(feature = "surrealql-hint")]
#[test]
fn surrealql_hint_feature_renders_ddl_into_registry() {
// With the `surrealql-hint` feature on, the registry receives
// the SurrealQL DDL rendering of the class — the "self-
// describing registry" claim from KnowableFromStore::register's
// docstring becomes concrete.
let mut c = Class::new("Account");
let mut email = ogar_vocab::Attribute::new("email");
email.type_name = Some("string".into());
c.attributes.push(email);

let store = MockKnowableFromStore::new(0);
register_class_knowable_from(&c, "ogit-erp/Account", &store).unwrap();

let calls = store.register_calls.lock().unwrap();
assert_eq!(calls.len(), 1);
let hint = calls[0].1.as_deref().expect("feature-on path must populate the hint");
// The DDL must mention the table and the field — the canonical
// shape `emit_surrealql_ddl` produces.
assert!(
hint.contains("DEFINE TABLE Account SCHEMAFULL;"),
"expected DEFINE TABLE in the hint, got: {hint}"
);
assert!(
hint.contains("DEFINE FIELD email ON Account TYPE string;"),
"expected DEFINE FIELD in the hint, got: {hint}"
);
}
}
109 changes: 109 additions & 0 deletions docs/ARCHITECTURAL-DECISIONS-2026-06-04.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
| ADR-020 | SDK endgame is deeper than Foundry going OSS via three structural differentiators (migration scaffold, self-hosting reference, substrate-layer OSS) | **Pinned** | OGAR PR #20 §5.3 |
| ADR-021 | **Meta-hygiene**: always grep peer crates before copying manifest patterns (the `[lints] workspace = true` cascade lesson) | **Pinned** | OGAR PR #15 + PR #17/#18 follow-ups |
| ADR-022 | **The Firewall** — absolute inner/outer boundary; no serialization in hot path; inner = compile-time HHTL; outer = contract-trait pluggable | **Pinned** | OGAR (this PR); `docs/THE-FIREWALL.md` |
| ADR-023 | **IR-as-wire-truth** — the source-language AST is *input dialect*; the canonical `Class`/`Attribute`/`Association`/`EnumDecl`/`ActionDef` IR is *wire truth*. Adapters lift dialects into IR; the IR routes everything (registry key, actor mailbox, Lance version, audit-log dimension) | **Pinned** | OGAR (this PR); `crates/ogar-vocab/`; `bardioc/substrate-b-shadow::EdgeDecoder<E>` (PR #19) |

## ADR-001: `State = ActionState` (lifecycle), not domain state, for Rubicon binding

Expand Down Expand Up @@ -1058,6 +1059,114 @@ designed to produce.
- lance-graph PR #470 (`.claude/handovers/2026-06-05-0445-bardioc-to-
lance-graph-bindspace-arch-delta.md` — the lance-graph-side pointer).

## ADR-023: IR-as-wire-truth — Class is the wire format, not the source AST

**Status:** Pinned (2026-06-05). Companion to ADR-022 (The Firewall);
captures the framing principle the firewall's inner side has been
operating under.

**Context.** Cross-session conversation surfaced the question
*"what's the wire format between source-language frontends and the
substrate?"* — raised in the context of Elixir ASTs, ClickHouse DDL,
SurrealQL DDL, FIBO/FMA TTL, and the planned `ch`/`ecto_ch` shadow
extraction. The naive answer ("forward the source AST as-is") is
wrong; the firewall's inner discipline already implies the right
answer, but it hadn't been named explicitly.

**Decision.** The canonical wire format is the OGAR IR — `Class`,
`Attribute`, `Association`, `EnumDecl`, `ActionDef`, `KausalSpec`,
`Identity` (the `NiblePath` prefix-radix). Source-language ASTs
(Elixir quoted form, SurrealQL DDL AST, Ruby AR macro tree, Odoo
Python `models.Model` shape, ClickHouse CREATE TABLE, OWL TTL
triples) are *input dialects* — each lifted into the canonical IR
by a dedicated **adapter crate**. Once lifted, everything downstream
(registry key, actor mailbox routing, Lance version stamp,
audit-log dimension, HHTL compile-time codegen) routes through the
*same* IR.

The aphorism: **"Elixir AST is input dialect; the canonical IR is
wire truth."** Generalizes to any source dialect; the IR is the
shared substrate.

**Alternatives considered.**

- *Forward the source AST as the wire format.* Rejected: leaks
source-language syntax + semantics into every downstream consumer;
breaks the firewall's "no serialization in hot path" invariant
(source ASTs are too rich + heterogeneous to be compile-time-
HHTL-resolvable); makes cross-source comparison (e.g. §14 oracle
equivalence-running between OLD-stack Elixir + NEW-stack Rust)
arbitrarily hard because the comparison surface differs per source.
- *Use a "least common denominator" subset of OWL DL.* Rejected: OWL
doesn't model state machines or lifecycle behaviour (the
`ActionDef` + `KausalSpec` axis OGAR adds past OWL DL); the LCD
surface would be insufficient. OGAR sits *above* OWL DL (OWL is
one of OGAR's supported source dialects, not a constraint on the
IR).

**Consequences.**

- **Same IR → same hash → same actor routing → same Lance row →
same audit dimension.** Content-addressing primitive. Already
realized in code: `ogar-ontology::class_identity(prefix, name)`
produces the canonical identity string; PR #31 closed the
collision hazard; bardioc PR #19 (`substrate-b-shadow::EdgeDecoder<E>`)
consumes the IR as `ActionInvocation` at the OLD-stack-shadow seam.

- **Adapters are pluggable, the IR is fixed.** New source dialects
ship as new adapter crates (`ogar-adapter-surrealql`,
`ogar-adapter-ttl` planned, `ogar-from-elixir`, `ogar-from-ecto`
proposed). The `Class` IR doesn't change; only the lift code does.

- **Round-trip is the adapter contract.** `parse_<dialect>` →
`Vec<Class>` → `emit_<dialect>` should reproduce the source. OGAR
PR #32 demonstrated this for SurrealQL DDL; round-trip tests
are now part of the adapter contract.

- **The `schema_ddl_hint` loop closes here.** PR #25 introduced
`KnowableFromStore::register(class_identity, schema_ddl_hint:
Option<&str>)` with the docstring claim *"so the registry is
self-describing"*. PR #32 landed `emit_surrealql_ddl`. This PR
wires the two together (feature-gated `surrealql-hint`): the
registry now carries the producer's `Class` IR projected into
SurrealQL DDL alongside the `knowable_from` stamp. The IR-as-wire-
truth claim is no longer aspirational.

- **Cross-session triangulation receipt.** bardioc PR #19
(`substrate-b-shadow`) consumes `ogar-vocab` as a direct
dependency — its `EdgeDecoder<E>` trait IS the IR-as-wire-truth
pattern in code. The HIRO-Graph + ClickHouse decoders return
`ActionInvocation` regardless of source; the rest of the substrate
consumes one shape.

**Change policy.** Adding a new source dialect (new adapter crate)
is routine. Changing the IR — adding a field to `Class`,
`AssociationKind`, `EnumSource`, `KausalSpec` — is a substrate-wide
contract change requiring (a) backward-compatible default (typically
`Option<…>` field), (b) round-trip preservation in all adapter
crates, (c) consultation with the runtime session (bardioc /
lance-graph) before merge.

**References.**

- `crates/ogar-vocab/` — the canonical IR.
- `crates/ogar-ontology/` — identity routing + canonical-form helpers.
- `crates/ogar-knowable-from/` — the registry seam; this PR wires
the `schema_ddl_hint` loop via the `surrealql-hint` feature.
- `crates/ogar-adapter-surrealql/` — first round-trip adapter (PR
#24 wired the parser; PR #32 closed the walk + round-trip).
- `crates/ogar-from-elixir/` — Elixir SchemaSource scaffold.
- ADR-022 (The Firewall) — the invariant ADR-023 makes explicit.
- ADR-016 (SurrealQL DDL AST is not the universal IR) — the
predecessor; ADR-023 generalizes ADR-016's claim from SurrealQL
to *all* source dialects.
- bardioc PR #17 (Rubicon Phases 1-5) — consumer of `ogar-vocab`
for actor dispatch.
- bardioc PR #19 (`substrate-b-shadow::EdgeDecoder<E>`) — the
pattern materialized in runtime-side code.
- `docs/RDF-OWL-ALIGNMENT.md` §3 (OGAR's position in L1-L5) — the
IR sits at the AR-pattern lift seam.

## Implementation receipts — ADR ↔ commit cross-reference

> **Added in follow-up addendum (2026-06-05).** Records the implementation
Expand Down
Loading