|
| 1 | +# SurrealQL AST trap — pre-flight introspection (the spellbook) |
| 2 | + |
| 3 | +> **Operational extract** of `SURREAL-AST-AS-ADAPTER.md` (the design). |
| 4 | +> Read this BEFORE the keyboard fires — five questions, 90 seconds, one |
| 5 | +> mirror. If you finish all five, you've cleared the trap; if you stop |
| 6 | +> at any one, you've caught it pre-materialization. |
| 7 | +> |
| 8 | +> The pattern this protects against is **pre-cognitive**, not reasoned: |
| 9 | +> sessions don't argue their way in, they *reflex* their way in. This |
| 10 | +> doc exists because a mirror that fires before the reflex is cheaper |
| 11 | +> than a refactor that fires after. |
| 12 | +> |
| 13 | +> Status: **SPELLBOOK v0** (2026-06-22). Append-only. |
| 14 | +> |
| 15 | +> Grounded in: `SURREAL-AST-AS-ADAPTER.md` §0 (the carved decision), |
| 16 | +> `OGAR-AST-CONTRACT.md` §1 (the canonical IR — `Class` + `ActionDef` |
| 17 | +> + `Identity` + `KausalSpec`), `THE-FIREWALL.md` (ADR-022/023 — no |
| 18 | +> serialization in the hot path), `ARCHITECTURE.md` (the AR-as-spine |
| 19 | +> doctrine). |
| 20 | +> |
| 21 | +> READ BEFORE: any producer→IR / transcode / codegen / `ogar-from-<lang>` |
| 22 | +> reader / `.surql` authoring session. The agent ensemble |
| 23 | +> (`core-first-architect`, `adapter-shaper`, `core-gap-auditor`, |
| 24 | +> the `Plan` subagent) loads this Tier-1 before output. |
| 25 | +
|
| 26 | +## The trap, in one paragraph |
| 27 | + |
| 28 | +You're reading an AR-shaped producer — Ruby callbacks, Odoo |
| 29 | +`@api.constrains`, Ecto `Multi`, Elixir `GenStateMachine`, TypeScript |
| 30 | +class with decorators. The first reflex is *"I'll walk the structural |
| 31 | +DDL form and parse it from there."* That reflex is **System-1** — it |
| 32 | +looks like a clean parse step. **System-2** is the harder move: route |
| 33 | +through OGAR's `Class` + `ActionDef` first, lift the behavior into |
| 34 | +typed fields, *then* emit to SurrealQL (or any other surface) as a |
| 35 | +lossy adapter step. The trap is the absence of OGAR — substituting |
| 36 | +SurrealQL AST for it. Once you commit to SurrealQL as your primary IR, |
| 37 | +the lifecycle has nowhere to go, and the pressure to encode it in |
| 38 | +`DEFINE EVENT … WHEN … THEN …` becomes *structural*, not avoidable. |
| 39 | +What follows is the negative-beauty hijack the design doc explicitly |
| 40 | +rejects: comments, sentinels, DDL pretending to be a state machine. |
| 41 | + |
| 42 | +The trap is silent at the time of writing and loud at the time of |
| 43 | +re-derivation. Avoiding it costs five questions. Recovering from it |
| 44 | +costs a fork. |
| 45 | + |
| 46 | +## The five questions — run BEFORE the keyboard fires |
| 47 | + |
| 48 | +``` |
| 49 | +Q1. WHAT am I reading FROM? |
| 50 | + ├─ Ruby AR / Python @api / Ecto / Elixir GenStateMachine / |
| 51 | + │ TypeScript class with decorators / Smalltalk methods / … |
| 52 | + │ → HAS BEHAVIOR. Route through a producer-side reader. |
| 53 | + │ |
| 54 | + └─ SurrealQL DDL / .surql / RDF triples / pure schema / |
| 55 | + Avro / JSON Schema / protobuf |
| 56 | + → HAS NO BEHAVIOR to recover. Structural-only is honest. |
| 57 | +
|
| 58 | +Q2. Does the source have LIFECYCLE VOCABULARY? |
| 59 | + Look for, in order of frequency: |
| 60 | + • callbacks before_save / after_commit / after_update |
| 61 | + • decorators @api.constrains / @api.depends / @validates |
| 62 | + • state machines Statesman / Ecto.Multi / GenStateMachine |
| 63 | + • validations validates_presence_of / Zod refine |
| 64 | + • lifecycle hooks on_enter / on_exit / state_timeout |
| 65 | + • timeout / retry state_timeout_millis / retry-with-backoff |
| 66 | + • guards can?(:transition) / GuardFailurePolicy |
| 67 | + ├─ ANY of the above present → MUST land as ActionDef / KausalSpec. |
| 68 | + │ NOT in DDL. Stop reaching for .surql. |
| 69 | + └─ NONE present → structural-only is the honest scope. |
| 70 | +
|
| 71 | +Q3. What is the TARGET IR I'm about to commit to? |
| 72 | + ├─ ogar_vocab::Class + ActionDef + Identity + KausalSpec |
| 73 | + │ → canonical; behavior has a home. Proceed. |
| 74 | + │ |
| 75 | + └─ surreal_ast::Library / DefineTable / DefineEvent / .surql files |
| 76 | + → if Q2 was YES, STOP. You're about to hijack DDL. |
| 77 | + The target should be OGAR Class FIRST. |
| 78 | + Then emit via ogar-adapter-surrealql as a SEPARATE step. |
| 79 | +
|
| 80 | +Q4. Which way does my arrow point? |
| 81 | + ├─ producer → OGAR → adapter(SurrealQL/REST/codegen) |
| 82 | + │ → correct. Canonical retains behavior; adapter is lossy |
| 83 | + │ egress; the loss is paid once at the boundary, by design. |
| 84 | + │ |
| 85 | + └─ producer → SurrealQL → ??? |
| 86 | + → trap. Behavior loses its home. Pressure to encode |
| 87 | + lifecycle in DEFINE EVENT is now structural, not avoidable. |
| 88 | +
|
| 89 | +Q5. Would the INVERSE recover the behavior? |
| 90 | + Imagine: emit this DDL, hand it to a fresh session, ask them to |
| 91 | + distill back to AR. Do they recover ActionDef / lifecycle / guards? |
| 92 | + ├─ Yes, recoverable → you're smuggling. Behavior is |
| 93 | + │ via sentinel comments, hiding in conventions you'll forget |
| 94 | + │ hijacked DEFINE EVENT, in 6 months and a reviewer in 2. |
| 95 | + │ string-typed state names That IS the trap. Stop. |
| 96 | + │ |
| 97 | + └─ No, irrecoverable → the behavior was never canonical |
| 98 | + from the DDL alone here. Find its real home (Q1's |
| 99 | + producer) and read it there. |
| 100 | +``` |
| 101 | + |
| 102 | +## Diagnostic signatures — what the trap looks like in code review |
| 103 | + |
| 104 | +If you weren't in the room for the decisions, these signatures let you |
| 105 | +spot the trap post-hoc: |
| 106 | + |
| 107 | +- **`.surql` files with `DEFINE EVENT … WHEN … THEN …` doing more than |
| 108 | + a single row trigger** — chains, conditional cascades, state-name |
| 109 | + string literals in the WHEN clause, multi-step transitions |
| 110 | +- **`DEFINE FUNCTION` used as a state-machine transition** rather than |
| 111 | + a stored procedure (no return value, side-effects on other rows, |
| 112 | + reads `state` field and writes a new one) |
| 113 | +- **Sentinel comments in DDL**: `-- @action:`, `-- @state:`, |
| 114 | + `-- @lifecycle:`, `-- @before_save:`, `-- @kausal:`, `-- @rubicon:` |
| 115 | + — any `-- @<vocab-word>` in a DDL file is a smell |
| 116 | +- **An IR file named `surreal_ast.rs` / `*_ast.rs` that is the |
| 117 | + project's primary producer-side IR** — not an adapter, not a |
| 118 | + projection target, *the IR itself*. The name is the tell. Adapters |
| 119 | + are named adapters; spines are named IRs. |
| 120 | +- **Test files asserting `parse(emit(parse(x))) == parse(x)` for |
| 121 | + *behavioral* roundtrips** — structural roundtrips through DDL are |
| 122 | + fine (the adapter doc says so); behavioral roundtrips cannot survive |
| 123 | + DDL, so the assertion only passes because of hijacked encoding |
| 124 | +- **`From<DdlAst> for ActionDef`** / `to_action_def()` / |
| 125 | + `extract_lifecycle_from_ddl()` — any function with this shape is |
| 126 | + admitting that behavior is being smuggled through DDL |
| 127 | +- **String-typed state machines**: `state: "pending" | "committed"` |
| 128 | + fields in DDL, with transitions implied by `DEFINE EVENT` triggers |
| 129 | + reading those strings |
| 130 | +- **A `triple.rs` or `recompute_dag.rs` next to `surreal_ast.rs`** — |
| 131 | + the colocation pattern of the odoo-rs `od-ontology` fork. If you |
| 132 | + see the cluster, the IR has metastasized |
| 133 | + |
| 134 | +A single signature is suspicious; two or more on the same crate is |
| 135 | +the trap with high confidence. |
| 136 | + |
| 137 | +## Remediation — if you're already inside |
| 138 | + |
| 139 | +The trap is recoverable, but not by patching. The remediation is |
| 140 | +three structural moves; trying to patch in-place re-creates the trap |
| 141 | +at a different layer. |
| 142 | + |
| 143 | +1. **Find the original producer.** Whose lifecycle was it really? |
| 144 | + Ruby `before_validation`? Odoo `@api.constrains`? Elixir |
| 145 | + `GenStateMachine.handle_event`? That source IS the truth — the |
| 146 | + DDL is just where its echo materialized. Read the original. |
| 147 | + |
| 148 | +2. **Write an `ogar-from-<lang>` reader** that parses the original |
| 149 | + producer (Ruby AST / Python AST / Elixir AST / TypeScript AST) |
| 150 | + directly into `Class` + `ActionDef`. Behavior lifts cleanly here; |
| 151 | + no sentinels, no DDL hijack. This reader is the new canonical |
| 152 | + path — name it for what it does. |
| 153 | + |
| 154 | +3. **Demote the SurrealQL form to egress only.** Keep emitting via |
| 155 | + `ogar-adapter-surrealql` (structural arm, lossy on behavior — fine; |
| 156 | + the adapter is *meant* to be lossy on the behavioral arm). Delete |
| 157 | + the DDL hijacks; they were never canonical, and the canonical reader |
| 158 | + from step 2 supersedes them. The fork (if any) collapses to the |
| 159 | + adapter call. |
| 160 | + |
| 161 | +odoo-rs is the worked example: W3 of `APP-CODEBOOK-MIGRATION-PLAN.md` |
| 162 | +already says this — lower `od-ontology::{surreal_ast,triple,emit}` onto |
| 163 | +`ogar_vocab::Class`, emit via the canonical adapter, delete the fork. |
| 164 | +The remediation is the migration plan. |
| 165 | + |
| 166 | +## When this doc fires |
| 167 | + |
| 168 | +For **author** sessions (before keyboard): |
| 169 | +- Any producer → IR transcode work touching a language with lifecycle |
| 170 | + vocabulary (Ruby AR, Python `@api`, Ecto, Elixir, TypeScript classes) |
| 171 | +- Any codegen session producing `.surql`, `DEFINE EVENT/FIELD/TABLE`, |
| 172 | + `surreal_ast.rs`, or anything named `*_ast.rs` as a *spine* |
| 173 | +- Any "distill from SurrealQL" / "lower onto `ogar_vocab::Class`" task |
| 174 | +- Any new `ogar-from-<lang>` reader authoring |
| 175 | +- Any "behavior-preserving roundtrip" claim involving DDL |
| 176 | + |
| 177 | +For **review** sessions (PR landing): |
| 178 | +- Any PR introducing or modifying `.surql` files beyond row-trigger scope |
| 179 | +- Any PR adding `From<DdlAst>` / `to_action_def()` conversions |
| 180 | +- Any PR claiming `parse(emit(parse(x))) == parse(x)` for behavioral |
| 181 | + roundtrips |
| 182 | +- Any PR that touches `surreal_ast.rs` / `triple.rs` / `recompute_dag.rs` |
| 183 | + in the same crate |
| 184 | + |
| 185 | +For the **agent ensemble**: |
| 186 | +- `core-first-architect` (loads at Knowledge Activation Tier-1) |
| 187 | +- `adapter-shaper` (loads at Knowledge Activation Tier-1) |
| 188 | +- `core-gap-auditor` (loads at Knowledge Activation Tier-1) |
| 189 | +- The `Plan` subagent on any task mentioning the trigger phrases above |
| 190 | + |
| 191 | +## Trigger phrases (for Knowledge Activation Protocol) |
| 192 | + |
| 193 | +`SurrealQL AST` · `DDL spine` · `DEFINE EVENT` · `DEFINE FUNCTION` · |
| 194 | +`.surql` · `surreal_ast.rs` · `*_ast.rs` (as IR, not adapter) · |
| 195 | +`distill from SurrealQL` · `lower onto ogar_vocab::Class` · |
| 196 | +`lifecycle in schema` · `from_triples` · `recompute_dag` · |
| 197 | +`producer-side reader` · `behavioral roundtrip` |
| 198 | + |
| 199 | +If the prompt mentions ANY of these, this doc is mandatory pre-read. |
| 200 | + |
| 201 | +## What this doc is NOT |
| 202 | + |
| 203 | +- **Not a ban on `.surql`.** SurrealDB is a real backing store; DDL is |
| 204 | + a real interop format; `ogar-adapter-surrealql` is the right emitter. |
| 205 | + The trap is using DDL as a *spine*, not as an *adapter*. |
| 206 | +- **Not a ban on `DEFINE EVENT`.** Genuine row triggers are fine. The |
| 207 | + trap is `DEFINE EVENT` carrying lifecycle / state-machine semantics. |
| 208 | +- **Not retroactive.** Code that already lives in the trap (the named |
| 209 | + W3 odoo-rs case) doesn't get a citation; it gets the three-move |
| 210 | + remediation. Citations are for future sessions. |
| 211 | + |
| 212 | +## Cross-refs |
| 213 | + |
| 214 | +- `docs/SURREAL-AST-AS-ADAPTER.md` — the design decision (the *why*) |
| 215 | +- `docs/OGAR-AST-CONTRACT.md` — the canonical IR (the *what*) |
| 216 | +- `docs/THE-FIREWALL.md` — the structural reason DDL can't be the spine |
| 217 | + (ADR-022/023) |
| 218 | +- `docs/ARCHITECTURE.md` — the AR-as-spine doctrine |
| 219 | +- `docs/ADAPTERS-AND-ACTORS.md` — the Action / SPO+TeKaMoLo vocabulary |
| 220 | + that has no DDL home |
| 221 | +- `docs/APP-CODEBOOK-MIGRATION-PLAN.md` W3 — the worked remediation case |
| 222 | + (odoo-rs) |
0 commit comments