Skip to content

Commit 3506751

Browse files
authored
Merge pull request #33 from AdaWorldAPI/claude/wire-schema-ddl-hint-and-adr-023
feat(knowable-from): wire schema_ddl_hint via surrealql-hint feature + ADR-023
2 parents 0442071 + 3bdfdc5 commit 3506751

4 files changed

Lines changed: 215 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ jobs:
3636
# gating by feature keeps the default build lean.
3737
- name: cargo test -p ogar-adapter-surrealql --features surrealdb-parser
3838
run: cargo test -p ogar-adapter-surrealql --features surrealdb-parser
39+
# Exercise the `surrealql-hint` feature on ogar-knowable-from
40+
# — auto-renders the schema_ddl_hint via the adapter's
41+
# emit_surrealql_ddl on register_class_knowable_from. Closes the
42+
# self-describing-registry loop (ADR-023 receipt).
43+
- name: cargo test -p ogar-knowable-from --features surrealql-hint
44+
run: cargo test -p ogar-knowable-from --features surrealql-hint

crates/ogar-knowable-from/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ description = "OGAR-side producer seam for the §10.3 `knowable_from` meet-point
1111
[features]
1212
default = []
1313
serde = ["dep:serde", "ogar-vocab/serde"]
14+
# Auto-render the `schema_ddl_hint` parameter from the `Class` via
15+
# `ogar-adapter-surrealql::emit_surrealql_ddl(&[class.clone()])` on
16+
# every `register_class_knowable_from` call. Opt-in because the
17+
# adapter pulls the SurrealDB-related dep graph; the default path
18+
# stays lightweight (only `ogar-vocab` + optional `serde`).
19+
surrealql-hint = ["dep:ogar-adapter-surrealql"]
1420

1521
[dependencies]
1622
ogar-vocab = { path = "../ogar-vocab" }
23+
ogar-adapter-surrealql = { path = "../ogar-adapter-surrealql", optional = true }
1724
serde = { workspace = true, optional = true }

crates/ogar-knowable-from/src/lib.rs

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,25 @@ pub trait KnowableFromStore: Send + Sync {
223223
/// `NiblePath` identity, same VART-as-reference-backend pattern
224224
/// (see crate-level "Reference backends").
225225
///
226+
/// # `schema_ddl_hint` — the self-describing-registry loop
227+
///
228+
/// With the **`surrealql-hint` feature ON**, this function renders the
229+
/// SurrealQL DDL for the class (via
230+
/// `ogar-adapter-surrealql::emit_surrealql_ddl`) and passes it to
231+
/// `store.register(class_identity, Some(ddl))`. The registry then
232+
/// carries the producer's view of the class shape alongside the
233+
/// `knowable_from` stamp — *"the registry is self-describing"* per
234+
/// the `KnowableFromStore::register` docstring, no longer aspirational.
235+
///
236+
/// With the feature OFF (the default), `None` is passed — the
237+
/// lightweight path. The feature is opt-in because the
238+
/// `ogar-adapter-surrealql` dep pulls the SurrealDB AST surface into
239+
/// the build graph.
240+
///
241+
/// Aligns with ADR-023 (IR-as-wire-truth): the canonical `Class` IR
242+
/// is the wire-truth carrier; the DDL string is its serialization for
243+
/// the registry. See `docs/ARCHITECTURAL-DECISIONS-2026-06-04.md`.
244+
///
226245
/// [1]: https://docs.rs/ogar-ontology
227246
pub fn register_class_knowable_from<S: KnowableFromStore>(
228247
class: &Class,
@@ -241,11 +260,23 @@ pub fn register_class_knowable_from<S: KnowableFromStore>(
241260
ogar_ontology::class_identity)".into(),
242261
));
243262
}
244-
// v1 minimum-shape: pass None for schema_ddl_hint. Future PRs can
245-
// render via ogar-adapter-surrealql::emit_surrealql_ddl(&[class.clone()])
246-
// — the `class` parameter is retained for that future expansion.
247-
let _ = class; // keep the parameter live for forward compatibility
248-
store.register(class_identity, None)
263+
// The `schema_ddl_hint` parameter — `None` by default (lightweight
264+
// path), auto-rendered via `ogar-adapter-surrealql::emit_surrealql_ddl`
265+
// when the `surrealql-hint` feature is on. This closes the loop the
266+
// `KnowableFromStore::register` docstring named: the trait carries a
267+
// `schema_ddl_hint: Option<&str>` slot so the registry is
268+
// self-describing; PR #32 landed the producer; this is where it
269+
// gets wired into the call site.
270+
#[cfg(feature = "surrealql-hint")]
271+
{
272+
let ddl = ogar_adapter_surrealql::emit_surrealql_ddl(std::slice::from_ref(class));
273+
store.register(class_identity, Some(ddl.as_str()))
274+
}
275+
#[cfg(not(feature = "surrealql-hint"))]
276+
{
277+
let _ = class; // keep parameter live; not used in the default path
278+
store.register(class_identity, None)
279+
}
249280
}
250281

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

350384
#[test]
@@ -461,4 +495,57 @@ mod tests {
461495
assert!(format!("{b}").contains("backend error"));
462496
assert!(format!("{b}").contains("nope"));
463497
}
498+
499+
// ── `schema_ddl_hint` loop closure (ADR-023 / IR-as-wire-truth) ──
500+
// Feature-off (default): `None` is passed to store.register, the
501+
// lightweight path. Feature-on (`surrealql-hint`): the function
502+
// renders the DDL via `ogar-adapter-surrealql::emit_surrealql_ddl`
503+
// and passes it as `Some(&ddl)`. Two tests, conditionally compiled.
504+
// ─────────────────────────────────────────────────────────────────
505+
506+
#[cfg(not(feature = "surrealql-hint"))]
507+
#[test]
508+
fn default_path_passes_none_for_schema_ddl_hint() {
509+
let c = Class::new("Account");
510+
let store = MockKnowableFromStore::new(0);
511+
register_class_knowable_from(&c, "ogit-erp/Account", &store).unwrap();
512+
let calls = store.register_calls.lock().unwrap();
513+
assert_eq!(calls.len(), 1);
514+
// Default path: the registry receives no DDL hint.
515+
assert!(
516+
calls[0].1.is_none(),
517+
"default (feature-off) path must pass None for schema_ddl_hint, got: {:?}",
518+
calls[0].1
519+
);
520+
}
521+
522+
#[cfg(feature = "surrealql-hint")]
523+
#[test]
524+
fn surrealql_hint_feature_renders_ddl_into_registry() {
525+
// With the `surrealql-hint` feature on, the registry receives
526+
// the SurrealQL DDL rendering of the class — the "self-
527+
// describing registry" claim from KnowableFromStore::register's
528+
// docstring becomes concrete.
529+
let mut c = Class::new("Account");
530+
let mut email = ogar_vocab::Attribute::new("email");
531+
email.type_name = Some("string".into());
532+
c.attributes.push(email);
533+
534+
let store = MockKnowableFromStore::new(0);
535+
register_class_knowable_from(&c, "ogit-erp/Account", &store).unwrap();
536+
537+
let calls = store.register_calls.lock().unwrap();
538+
assert_eq!(calls.len(), 1);
539+
let hint = calls[0].1.as_deref().expect("feature-on path must populate the hint");
540+
// The DDL must mention the table and the field — the canonical
541+
// shape `emit_surrealql_ddl` produces.
542+
assert!(
543+
hint.contains("DEFINE TABLE Account SCHEMAFULL;"),
544+
"expected DEFINE TABLE in the hint, got: {hint}"
545+
);
546+
assert!(
547+
hint.contains("DEFINE FIELD email ON Account TYPE string;"),
548+
"expected DEFINE FIELD in the hint, got: {hint}"
549+
);
550+
}
464551
}

docs/ARCHITECTURAL-DECISIONS-2026-06-04.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
| 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 |
5151
| 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 |
5252
| 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` |
53+
| 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) |
5354

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

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

1062+
## ADR-023: IR-as-wire-truth — Class is the wire format, not the source AST
1063+
1064+
**Status:** Pinned (2026-06-05). Companion to ADR-022 (The Firewall);
1065+
captures the framing principle the firewall's inner side has been
1066+
operating under.
1067+
1068+
**Context.** Cross-session conversation surfaced the question
1069+
*"what's the wire format between source-language frontends and the
1070+
substrate?"* — raised in the context of Elixir ASTs, ClickHouse DDL,
1071+
SurrealQL DDL, FIBO/FMA TTL, and the planned `ch`/`ecto_ch` shadow
1072+
extraction. The naive answer ("forward the source AST as-is") is
1073+
wrong; the firewall's inner discipline already implies the right
1074+
answer, but it hadn't been named explicitly.
1075+
1076+
**Decision.** The canonical wire format is the OGAR IR — `Class`,
1077+
`Attribute`, `Association`, `EnumDecl`, `ActionDef`, `KausalSpec`,
1078+
`Identity` (the `NiblePath` prefix-radix). Source-language ASTs
1079+
(Elixir quoted form, SurrealQL DDL AST, Ruby AR macro tree, Odoo
1080+
Python `models.Model` shape, ClickHouse CREATE TABLE, OWL TTL
1081+
triples) are *input dialects* — each lifted into the canonical IR
1082+
by a dedicated **adapter crate**. Once lifted, everything downstream
1083+
(registry key, actor mailbox routing, Lance version stamp,
1084+
audit-log dimension, HHTL compile-time codegen) routes through the
1085+
*same* IR.
1086+
1087+
The aphorism: **"Elixir AST is input dialect; the canonical IR is
1088+
wire truth."** Generalizes to any source dialect; the IR is the
1089+
shared substrate.
1090+
1091+
**Alternatives considered.**
1092+
1093+
- *Forward the source AST as the wire format.* Rejected: leaks
1094+
source-language syntax + semantics into every downstream consumer;
1095+
breaks the firewall's "no serialization in hot path" invariant
1096+
(source ASTs are too rich + heterogeneous to be compile-time-
1097+
HHTL-resolvable); makes cross-source comparison (e.g. §14 oracle
1098+
equivalence-running between OLD-stack Elixir + NEW-stack Rust)
1099+
arbitrarily hard because the comparison surface differs per source.
1100+
- *Use a "least common denominator" subset of OWL DL.* Rejected: OWL
1101+
doesn't model state machines or lifecycle behaviour (the
1102+
`ActionDef` + `KausalSpec` axis OGAR adds past OWL DL); the LCD
1103+
surface would be insufficient. OGAR sits *above* OWL DL (OWL is
1104+
one of OGAR's supported source dialects, not a constraint on the
1105+
IR).
1106+
1107+
**Consequences.**
1108+
1109+
- **Same IR → same hash → same actor routing → same Lance row →
1110+
same audit dimension.** Content-addressing primitive. Already
1111+
realized in code: `ogar-ontology::class_identity(prefix, name)`
1112+
produces the canonical identity string; PR #31 closed the
1113+
collision hazard; bardioc PR #19 (`substrate-b-shadow::EdgeDecoder<E>`)
1114+
consumes the IR as `ActionInvocation` at the OLD-stack-shadow seam.
1115+
1116+
- **Adapters are pluggable, the IR is fixed.** New source dialects
1117+
ship as new adapter crates (`ogar-adapter-surrealql`,
1118+
`ogar-adapter-ttl` planned, `ogar-from-elixir`, `ogar-from-ecto`
1119+
proposed). The `Class` IR doesn't change; only the lift code does.
1120+
1121+
- **Round-trip is the adapter contract.** `parse_<dialect>`
1122+
`Vec<Class>``emit_<dialect>` should reproduce the source. OGAR
1123+
PR #32 demonstrated this for SurrealQL DDL; round-trip tests
1124+
are now part of the adapter contract.
1125+
1126+
- **The `schema_ddl_hint` loop closes here.** PR #25 introduced
1127+
`KnowableFromStore::register(class_identity, schema_ddl_hint:
1128+
Option<&str>)` with the docstring claim *"so the registry is
1129+
self-describing"*. PR #32 landed `emit_surrealql_ddl`. This PR
1130+
wires the two together (feature-gated `surrealql-hint`): the
1131+
registry now carries the producer's `Class` IR projected into
1132+
SurrealQL DDL alongside the `knowable_from` stamp. The IR-as-wire-
1133+
truth claim is no longer aspirational.
1134+
1135+
- **Cross-session triangulation receipt.** bardioc PR #19
1136+
(`substrate-b-shadow`) consumes `ogar-vocab` as a direct
1137+
dependency — its `EdgeDecoder<E>` trait IS the IR-as-wire-truth
1138+
pattern in code. The HIRO-Graph + ClickHouse decoders return
1139+
`ActionInvocation` regardless of source; the rest of the substrate
1140+
consumes one shape.
1141+
1142+
**Change policy.** Adding a new source dialect (new adapter crate)
1143+
is routine. Changing the IR — adding a field to `Class`,
1144+
`AssociationKind`, `EnumSource`, `KausalSpec` — is a substrate-wide
1145+
contract change requiring (a) backward-compatible default (typically
1146+
`Option<…>` field), (b) round-trip preservation in all adapter
1147+
crates, (c) consultation with the runtime session (bardioc /
1148+
lance-graph) before merge.
1149+
1150+
**References.**
1151+
1152+
- `crates/ogar-vocab/` — the canonical IR.
1153+
- `crates/ogar-ontology/` — identity routing + canonical-form helpers.
1154+
- `crates/ogar-knowable-from/` — the registry seam; this PR wires
1155+
the `schema_ddl_hint` loop via the `surrealql-hint` feature.
1156+
- `crates/ogar-adapter-surrealql/` — first round-trip adapter (PR
1157+
#24 wired the parser; PR #32 closed the walk + round-trip).
1158+
- `crates/ogar-from-elixir/` — Elixir SchemaSource scaffold.
1159+
- ADR-022 (The Firewall) — the invariant ADR-023 makes explicit.
1160+
- ADR-016 (SurrealQL DDL AST is not the universal IR) — the
1161+
predecessor; ADR-023 generalizes ADR-016's claim from SurrealQL
1162+
to *all* source dialects.
1163+
- bardioc PR #17 (Rubicon Phases 1-5) — consumer of `ogar-vocab`
1164+
for actor dispatch.
1165+
- bardioc PR #19 (`substrate-b-shadow::EdgeDecoder<E>`) — the
1166+
pattern materialized in runtime-side code.
1167+
- `docs/RDF-OWL-ALIGNMENT.md` §3 (OGAR's position in L1-L5) — the
1168+
IR sits at the AR-pattern lift seam.
1169+
10611170
## Implementation receipts — ADR ↔ commit cross-reference
10621171

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

0 commit comments

Comments
 (0)