Operational extract of
SURREAL-AST-AS-ADAPTER.md(the design). Read this BEFORE the keyboard fires — five questions, 90 seconds, one mirror. If you finish all five, you've cleared the trap; if you stop at any one, you've caught it pre-materialization.The pattern this protects against is pre-cognitive, not reasoned: sessions don't argue their way in, they reflex their way in. This doc exists because a mirror that fires before the reflex is cheaper than a refactor that fires after.
Status: SPELLBOOK v0 (2026-06-22). Append-only.
Grounded in:
SURREAL-AST-AS-ADAPTER.md§0 (the carved decision),OGAR-AST-CONTRACT.md§1 (the canonical IR —Class+ActionDef
Identity+KausalSpec),THE-FIREWALL.md(ADR-022/023 — no serialization in the hot path),ARCHITECTURE.md(the AR-as-spine doctrine).READ BEFORE: any producer→IR / transcode / codegen /
ogar-from-<lang>reader /.surqlauthoring session. The agent ensemble (core-first-architect,adapter-shaper,core-gap-auditor, thePlansubagent) loads this Tier-1 before output.
The classid is pure address. The magic is what the address resolves to. (Drilled with worked examples in
OGAR-CONSUMER-BEST-PRACTICES.md§0.)
The classid (0xDDCC_AAAA) is pure address. Both halves —
hi u16 (canonical concept) and lo u16 (APP / render lens) — are
address dimensions. Neither carries behavior. (Order flipped
2026-07-02 — canon HIGH / custom LOW; pre-flip docs and baked data use
the legacy order — see docs/DISCOVERY-MAP.md D-CLASSID-CANON-HIGH-FLIP.)
Where does the behavior live? At the resolution target:
classid ──┬──► ClassView the SKIN (render — per-app)
├──► Class (structural) the SHAPE (canonical — shared)
└──► ActionDef + KausalSpec the MAGIC (behavioral — lifecycle/
callbacks/validations,
ALWAYS in OGAR Core)
This is the Core-First identity = classid rule: the address is dumb, the magic is what it points at. Class-magic (callbacks, lifecycle, validations — the rails ActiveRecord blob) is a property of the Core node the address resolves to, never a property of the address itself.
This is why the SurrealQL trap is structurally impossible from inside
the DDL: SurrealQL has the address but not the Core types it
resolves to. So encoding lifecycle in DEFINE EVENT … WHEN … THEN …
or sentinel comments is trying to put the value into the key — to
forge behavior at the wrong layer entirely. It looks like it works in
the narrow case and rots immediately at every other layer.
The five questions below are the operational mirror that catches this before keyboard. If you find yourself reaching for DDL to express something the address can't carry, the question isn't "how do I encode this in DDL?" — it's "which Core type does this address resolve to, and have I lifted the behavior into THAT?"
You're reading an AR-shaped producer — Ruby callbacks, Odoo
@api.constrains, Ecto Multi, Elixir GenStateMachine, TypeScript
class with decorators. The first reflex is "I'll walk the structural
DDL form and parse it from there." That reflex is System-1 — it
looks like a clean parse step. System-2 is the harder move: route
through OGAR's Class + ActionDef first, lift the behavior into
typed fields, then emit to SurrealQL (or any other surface) as a
lossy adapter step. The trap is the absence of OGAR — substituting
SurrealQL AST for it. Once you commit to SurrealQL as your primary IR,
the lifecycle has nowhere to go, and the pressure to encode it in
DEFINE EVENT … WHEN … THEN … becomes structural, not avoidable.
What follows is the negative-beauty hijack the design doc explicitly
rejects: comments, sentinels, DDL pretending to be a state machine.
The trap is silent at the time of writing and loud at the time of re-derivation. Avoiding it costs five questions. Recovering from it costs a fork.
Q1. WHAT am I reading FROM?
├─ Ruby AR / Python @api / Ecto / Elixir GenStateMachine /
│ TypeScript class with decorators / Smalltalk methods / …
│ → HAS BEHAVIOR. Route through a producer-side reader.
│
└─ SurrealQL DDL / .surql / RDF triples / pure schema /
Avro / JSON Schema / protobuf
→ HAS NO BEHAVIOR to recover. Structural-only is honest.
Q2. Does the source have LIFECYCLE VOCABULARY?
Look for, in order of frequency:
• callbacks before_save / after_commit / after_update
• decorators @api.constrains / @api.depends / @validates
• state machines Statesman / Ecto.Multi / GenStateMachine
• validations validates_presence_of / Zod refine
• lifecycle hooks on_enter / on_exit / state_timeout
• timeout / retry state_timeout_millis / retry-with-backoff
• guards can?(:transition) / GuardFailurePolicy
├─ ANY of the above present → MUST land as ActionDef / KausalSpec.
│ NOT in DDL. Stop reaching for .surql.
└─ NONE present → structural-only is the honest scope.
Q3. What is the TARGET IR I'm about to commit to?
├─ ogar_vocab::Class + ActionDef + Identity + KausalSpec
│ → canonical; behavior has a home. Proceed.
│
└─ surreal_ast::Library / DefineTable / DefineEvent / .surql files
→ if Q2 was YES, STOP. You're about to hijack DDL.
The target should be OGAR Class FIRST.
Then emit via ogar-adapter-surrealql as a SEPARATE step.
Q4. Which way does my arrow point?
├─ producer → OGAR → adapter(SurrealQL/REST/codegen)
│ → correct. Canonical retains behavior; adapter is lossy
│ egress; the loss is paid once at the boundary, by design.
│
└─ producer → SurrealQL → ???
→ trap. Behavior loses its home. Pressure to encode
lifecycle in DEFINE EVENT is now structural, not avoidable.
Q5. Would the INVERSE recover the behavior?
Imagine: emit this DDL, hand it to a fresh session, ask them to
distill back to AR. Do they recover ActionDef / lifecycle / guards?
├─ Yes, recoverable → you're smuggling. Behavior is
│ via sentinel comments, hiding in conventions you'll forget
│ hijacked DEFINE EVENT, in 6 months and a reviewer in 2.
│ string-typed state names That IS the trap. Stop.
│
└─ No, irrecoverable → the behavior was never canonical
from the DDL alone here. Find its real home (Q1's
producer) and read it there.
If you weren't in the room for the decisions, these signatures let you spot the trap post-hoc:
.surqlfiles withDEFINE EVENT … WHEN … THEN …doing more than a single row trigger — chains, conditional cascades, state-name string literals in the WHEN clause, multi-step transitionsDEFINE FUNCTIONused as a state-machine transition rather than a stored procedure (no return value, side-effects on other rows, readsstatefield and writes a new one)- Sentinel comments in DDL:
-- @action:,-- @state:,-- @lifecycle:,-- @before_save:,-- @kausal:,-- @rubicon:— any-- @<vocab-word>in a DDL file is a smell - An IR file named
surreal_ast.rs/*_ast.rsthat is the project's primary producer-side IR — not an adapter, not a projection target, the IR itself. The name is the tell. Adapters are named adapters; spines are named IRs. - Test files asserting
parse(emit(parse(x))) == parse(x)for behavioral roundtrips — structural roundtrips through DDL are fine (the adapter doc says so); behavioral roundtrips cannot survive DDL, so the assertion only passes because of hijacked encoding From<DdlAst> for ActionDef/to_action_def()/extract_lifecycle_from_ddl()— any function with this shape is admitting that behavior is being smuggled through DDL- String-typed state machines:
state: "pending" | "committed"fields in DDL, with transitions implied byDEFINE EVENTtriggers reading those strings - A
triple.rsorrecompute_dag.rsnext tosurreal_ast.rs— the colocation pattern of the odoo-rsod-ontologyfork. If you see the cluster, the IR has metastasized
A single signature is suspicious; two or more on the same crate is the trap with high confidence.
The trap is recoverable, but not by patching. The remediation is three structural moves; trying to patch in-place re-creates the trap at a different layer.
-
Find the original producer. Whose lifecycle was it really? Ruby
before_validation? Odoo@api.constrains? ElixirGenStateMachine.handle_event? That source IS the truth — the DDL is just where its echo materialized. Read the original. -
Write an
ogar-from-<lang>reader that parses the original producer (Ruby AST / Python AST / Elixir AST / TypeScript AST) directly intoClass+ActionDef. Behavior lifts cleanly here; no sentinels, no DDL hijack. This reader is the new canonical path — name it for what it does. -
Demote the SurrealQL form to egress only. Keep emitting via
ogar-adapter-surrealql(structural arm, lossy on behavior — fine; the adapter is meant to be lossy on the behavioral arm). Delete the DDL hijacks; they were never canonical, and the canonical reader from step 2 supersedes them. The fork (if any) collapses to the adapter call.
odoo-rs is the worked example: W3 of APP-CODEBOOK-MIGRATION-PLAN.md
already says this — lower od-ontology::{surreal_ast,triple,emit} onto
ogar_vocab::Class, emit via the canonical adapter, delete the fork.
The remediation is the migration plan.
For author sessions (before keyboard):
- Any producer → IR transcode work touching a language with lifecycle
vocabulary (Ruby AR, Python
@api, Ecto, Elixir, TypeScript classes) - Any codegen session producing
.surql,DEFINE EVENT/FIELD/TABLE,surreal_ast.rs, or anything named*_ast.rsas a spine - Any "distill from SurrealQL" / "lower onto
ogar_vocab::Class" task - Any new
ogar-from-<lang>reader authoring - Any "behavior-preserving roundtrip" claim involving DDL
For review sessions (PR landing):
- Any PR introducing or modifying
.surqlfiles beyond row-trigger scope - Any PR adding
From<DdlAst>/to_action_def()conversions - Any PR claiming
parse(emit(parse(x))) == parse(x)for behavioral roundtrips - Any PR that touches
surreal_ast.rs/triple.rs/recompute_dag.rsin the same crate
For the agent ensemble:
core-first-architect(loads at Knowledge Activation Tier-1)adapter-shaper(loads at Knowledge Activation Tier-1)core-gap-auditor(loads at Knowledge Activation Tier-1)- The
Plansubagent on any task mentioning the trigger phrases above
SurrealQL AST · DDL spine · DEFINE EVENT · DEFINE FUNCTION ·
.surql · surreal_ast.rs · *_ast.rs (as IR, not adapter) ·
distill from SurrealQL · lower onto ogar_vocab::Class ·
lifecycle in schema · from_triples · recompute_dag ·
producer-side reader · behavioral roundtrip
If the prompt mentions ANY of these, this doc is mandatory pre-read.
- Not a ban on
.surql. SurrealDB is a real backing store; DDL is a real interop format;ogar-adapter-surrealqlis the right emitter. The trap is using DDL as a spine, not as an adapter. - Not a ban on
DEFINE EVENT. Genuine row triggers are fine. The trap isDEFINE EVENTcarrying lifecycle / state-machine semantics. - Not retroactive. Code that already lives in the trap (the named W3 odoo-rs case) doesn't get a citation; it gets the three-move remediation. Citations are for future sessions.
docs/OGAR-CONSUMER-BEST-PRACTICES.md— the muscle-memory guide: classid-address-vs-class-magic with worked examples across every consumer (medcare, woa, smb, odoo, openproject, redmine); the four canonical patterns; anti-pattern catalogue. Read this if you're working on a consumer-side path.docs/SURREAL-AST-AS-ADAPTER.md— the design decision (the why)docs/OGAR-AST-CONTRACT.md— the canonical IR (the what)docs/THE-FIREWALL.md— the structural reason DDL can't be the spine (ADR-022/023)docs/ARCHITECTURE.md— the AR-as-spine doctrinedocs/ADAPTERS-AND-ACTORS.md— the Action / SPO+TeKaMoLo vocabulary that has no DDL homedocs/APP-CODEBOOK-MIGRATION-PLAN.mdW3 — the worked remediation case (odoo-rs)- Parallel-session sibling:
lance-graph/.claude/knowledge/ogar-consumer-preflight.md(lance-graph #591) — same spellbook pattern cast in lance-graph's consumer domain; ships theISS-CONTRACT-APP-PREFIX-MIRRORCore-gap entry.