# SurrealGuard - Full LLM Reference > Static analysis and type inference for SurrealQL. SurrealGuard parses your > `.surql` schema and queries into a typed, span-carrying AST, infers the > response type of every statement, and reports violations of each construct's > contract - before a query reaches SurrealDB. The same engine powers a CLI, a > language server, a Rust proc-macro, and a set of TypeScript packages. This document is the complete reference intended for LLMs and coding agents. A shorter overview lives at `/llms.txt`. Human docs: https://surrealguard.dev/docs/ ## Table of contents 1. Model - why contract-first 2. Install 3. CLI (`surrealguard`): init, check, check --json, generate, exit codes 4. Configuration (`surrealguard.toml`) 5. Diagnostics: the eight code families with example codes 6. Parameter constraints and context parameters 7. Rust: `surrealguard-rs` (`query!` / `surql!`), schema resolution, Kind->Rust 8. TypeScript: `@surrealguard/client`, `generate`, `@surrealguard/query`, `@surrealguard/next`, `@surrealguard/svelte` 9. Editors and the language server (LSP) 10. Using SurrealGuard with a coding agent 11. Links --- ## 1. Model - why contract-first SurrealDB is permissive at runtime: cross-kind comparisons order by kind instead of failing, coercions succeed silently, and a misspelled field just returns `NONE`. That is exactly where bugs hide - the database accepts them. SurrealGuard is **contract-first**. Every construct has a contract - what the author must mean for the statement to make sense. A diagnostic exists when the contract is provably violated; whether SurrealDB throws, silently tolerates, or coerces is irrelevant to severity. Tolerated misuse is the reason the tool exists. Principles: - **Contracts, not engine behavior.** One code per contract; variations of the same violation are message variants, never new codes. - **Inference is never affected.** A finding never changes an inferred type; `UPDATE person SET age = 'x'` still types as `person` rows. Poison (`any`) appears only where the type is genuinely unknowable. - **Detection site = emission site.** The analyzer that owns a statement emits its findings; composite analyzers learn about inner findings by span overlap. - **Statically provable only.** Every code is checkable from source + schema. When a value is a host parameter, the check is exported as a constraint instead of emitted as a finding. Why tree-sitter: SurrealDB's own parser discards spans before producing its AST (true in 2.x and 3.0), so it cannot power an analyzer or editor tooling. SurrealGuard parses with tree-sitter into a typed, span-carrying AST and runs all analysis on that. Schema leaf kinds use upstream `surrealdb_types::Kind`; the project does not maintain a competing scalar/type hierarchy. What it analyzes: - **Full statement coverage** - SELECT (projections, graph traversals, FETCH/SPLIT/GROUP/OMIT), the six mutations, RELATE, LET/RETURN/IF/FOR/blocks, transactions, DEFINE/REMOVE/ALTER, LIVE SELECT/KILL, and the rest. - **Type inference** - response kinds as `surrealdb_types::Kind`: closed object literals for known rows, IF/ELSE unions, record-link and graph-edge shapes, the full builtin function table plus `fn::` declarations, closure and subquery inference, and constant-value evaluation. - **90 contract diagnostics** across eight families, with rust-analyzer-style messages (lead with the consequence, `help:` fixes, `note:` definition spans). - **Flow-sensitive analysis** - control-flow type narrowing (occurrence typing that narrows `option` and `record` unions via `= NONE` guards and `type::table()` discriminants, field paths included), PERMISSIONS-predicate checks, and comparison footguns (`= NONE`/`= NULL` on a kind that excludes it, `IN`/`CONTAINS` element-kind mismatch, disjoint record-link equality). - **Parameter constraints** - every `$param` a source reads is exported with the kind and value domain its uses imply. - **Byte-precise spans** on every finding. --- ## 2. Install ```bash # CLI, via npm - zero install, no toolchain needed npx surrealguard check # or install the CLI from source (Rust) cargo install --git https://github.com/DrewRidley/surrealguard surrealguard # editor support: install the Zed extension - it auto-downloads the LSP for you # https://github.com/DrewRidley/zed-surreal # TypeScript typed client (published on npm) npm i @surrealguard/client # + @surrealguard/{query,next,svelte} ``` The Rust macro crate is a git dependency until the crates.io release: ```toml # Cargo.toml [dependencies] surrealguard-rs = { git = "https://github.com/DrewRidley/surrealguard" } ``` Release note: the crates.io publish is gated on the tree-sitter SurrealQL grammar being published upstream; the npm packages have no such dependency. --- ## 3. CLI (`surrealguard`) Three subcommands. The CLI walks up from the current directory to the nearest `surrealguard.toml` to find the workspace root, then discovers every `.surql`/`.surrealql` file under it (skipping ignored directories), analyzed in sorted path order so schema defined earlier is visible to later queries. ### `surrealguard init` Writes a starter `surrealguard.toml` in the current directory. If one already exists it is left untouched. ``` $ surrealguard init Created surrealguard.toml ``` ### `surrealguard check [--json]` Analyzes the workspace and reports findings. Severity is resolved through the workspace policy (`warnings_as_errors` promotion, per-lint levels) at this edge. Without `--json` it prints rustc-style diagnostic blocks and a summary line. ``` $ surrealguard check Checking SurrealQL sources... Checked 2 source(s), found 0 diagnostic(s) All checks passed! ``` ### `surrealguard check --json` Emits machine-readable diagnostics. The document has two top-level keys, `summary` and `diagnostics`: ```json { "summary": { "sources_checked": 2, "diagnostics": 1, "errors": 1 }, "diagnostics": [ { "code": "E1002", "severity": "error", "source": "queries/users.surql", "range": { "start": 41, "end": 45 }, "message": "unknown field `team` on table `user`", "help": ["did you mean `name`?"], "related": [ { "source": "schema/user.surql", "range": { "start": 0, "end": 26 }, "message": "table `user` defined here" } ] } ] } ``` Field reference: - `summary.sources_checked` (number) - analyzed sources. - `summary.diagnostics` (number) - findings that survived policy (errors + warnings + hints). - `summary.errors` (number) - error-severity findings; non-zero fails the check. - `code` (string) - severity-prefixed code: `E` error, `W` warning, `I` hint, `S` syntax; four digits (e.g. `E1002`, `W4022`, `I7007`, `S0001`). The prefix reflects the *resolved* severity, so a promoted warning renders as `E...`. - `severity` (string) - `"error"`, `"warning"`, or `"hint"`. - `source` (string) - the source path/id the span belongs to. - `range` ({ start, end }) - byte offsets into that source. - `message` (string) - the finding message. - `help` (string[]) - zero or more help lines. - `related` (object[]) - related spans, each `{ source, range, message }`. On a clean run the `diagnostics` array is empty and only the `summary` is populated. The `help` and `related` arrays are always present, often empty. ### `surrealguard generate [--out ]` Scans host-language files for embedded SurrealQL, analyzes each query against the workspace schema, and writes a TypeScript module that re-exports a ready `SurrealGuardClient` and augments its query registry. Host files are discovered by extension - `.ts`, `.tsx`, `.js`, `.jsx`, `.svelte`, `.vue`, `.astro` - honoring the same ignore patterns as `.surql` discovery. Because the file re-exports a runtime value (the client), write it to a `.ts` file via `--out` (e.g. `src/surrealguard.generated.ts`). The emitted file both re-exports `SurrealGuardClient` and augments `@surrealguard/client` (see Section 8). ``` $ surrealguard generate --out src/surrealguard.generated.ts Generated src/surrealguard.generated.ts ``` ### Exit codes - `0` - no error-severity findings survived policy. Warnings and hints do not fail the check unless `warnings_as_errors` promotes them. - `1` - the check found one or more error-severity findings, or a command failed (missing/invalid config, unreadable source, I/O error). --- ## 4. Configuration (`surrealguard.toml`) Every key is optional; unset keys fall back to defaults. With no config at all, SurrealGuard analyzes every `.surql`/`.surrealql` file under the project root, skipping `target/`, `node_modules/`, and `.git/`. ```toml [sources] schema = ["schema/**/*.surql", "migrations/**/*.surql"] # default: **/*.surql, **/*.surrealql queries = ["queries/**/*.surql"] # default: **/*.surql, **/*.surrealql ignore = ["target/**", "node_modules/**", ".git/**"] # default: same [analysis] strict = false # tighten otherwise-advisory checks surrealdb_version = "2" # target version for version-gated behavior [diagnostics] warnings_as_errors = false # promote every warning to an error require_suppression_reasons = false # require a reason on every inline suppression [lints] # levels are allow | warn | deny; keys can be a named lint, a specific code # (`E1002` or the bare number `7002`), or a whole-family wildcard (`"7xxx"`). # A specific code always overrides a family wildcard that also covers it. select_star = "warn" # named lint dynamic_query = "warn" # named lint permission_gated_field = "warn" "7xxx" = "warn" # every 7xxx lint at warn... E1002 = "allow" # ...but silence this specific code ``` In-source suppression is separate from config policy - it is source-authored intent applied at analysis time, rustc-style. The directive covers the next line (or its own line when trailing a statement); a suppressed finding never leaves the pipeline. Blanket suppressions are rejected; a directive that violates its own contract is `7013`. ```surql -- surrealguard: allow(E1001) reason="table created by a migration tool at runtime" SELECT * FROM external_table; ``` --- ## 5. Diagnostics - the eight code families Codes are `<3 digits>`; the `E`/`W`/`I` prefix comes from resolved severity at render time, and parse-level findings keep an `S` prefix. Severity classes: **error** (provably fails or misbehaves), **warning** (provably suspicious but executable), **hint** (analyzer limitation or style note). Severity is intrinsic data; consumers apply policy at their edge. Errors are never demotable; lints are fully configurable. 90 contracts span the eight families below (0xxx syntax is emitted by the parser). The listing below is representative, not exhaustive; the full catalog is in `crates/diagnostics/src/catalog.rs`. Families: 0xxx syntax - 1xxx schema references - 2xxx types - 3xxx graph - 4xxx statement misuse - 5xxx functions - 6xxx parameters - 7xxx lints - 8xxx version compatibility. Some codes fire from cross-cutting passes rather than a single statement kind. Control-flow narrowing (occurrence typing on `option` and `record` unions via `= NONE` guards and `type::table()` discriminants) feeds the comparison and field-access checks. PERMISSIONS predicates are lowered and walked in a row-scoped child env, so `1002` (undefined field), `5001` (undefined `fn::`), `2004`/`7005` (always-false), and `2005` (non-boolean) all fire inside a `PERMISSIONS FOR ...` clause just as they do in a normal expression. ### 1xxx - Schema references Contract: every name a query uses must resolve against the schema. - `1001` (E) - a table reference names a known table (FROM/targets, RELATE endpoints, `record`, `DEFINE ... ON`). - `1002` (E) - a field reference names a declared field of its row's table (schemafull only; FLEXIBLE subtrees exempt). - `1012` (E) - a schema-object reference names a known object of that kind (REMOVE/REBUILD INDEX, REMOVE EVENT, analyzers). - `1025` (E) - a subfield is declared under an object-shaped parent (not a scalar). - `1032` (E) - DEFINE ANALYZER components name known tokenizers/filters/languages. - `1021` (W) - REMOVE removes something that exists. - `1022` (W) - a definition does not silently redefine (use OVERWRITE). - `1023` (E) - FETCH names something that can hold records. - `1024` (E) - SPLIT names a collection field. - `1027` (E) - an index-backed operator has its supporting index (`@@`/search needs a SEARCH index; `<|k|>` needs MTREE/HNSW). - `1029` (W) - each index covers a distinct field set. ### 2xxx - Types and nullability - `2001` (E) - a value written to a field inhabits the field's declared type (SET, CONTENT/MERGE/REPLACE, INSERT, DEFAULT/VALUE, record links, NONE into non-optional). - `2004` (E) - the operands make sense together for the operator (binary/unary, arithmetic/comparison, compound assignment). - `2005` (W) - a condition position expects a boolean (IF, ASSERT, bare WHERE). - `2007` (E) - a cast names a known type (` x`). - `2008` (E) - a conversion can succeed (` 'abc'`, `type::int('x')`). - `2012` (E) - a body returns what it declares (DEFINE FUNCTION / closure return type). - `2015` (W) - a value-requiring position gets a value that is always present. - `2017` (E) - ORDER BY keys name fields available on the result rows (or RAND()). - `2018` (E) - LIMIT/START take a non-negative integer. - `2019` (E) - TIMEOUT takes a duration. - `2020` (E) - KILL takes a live-query uuid. - `2021` (E) - SHOW SINCE takes a versionstamp or datetime. - `2022` (E) - FOR iterates something iterable. - `2025` (E) - READONLY fields are written only at creation. - `2026` (W) - computed (VALUE-clause) fields are not hand-assigned. - `2030` (E) - index/filter/splat apply to collections (`age[0]`, `name[WHERE ...]`). - `2031` (E) - a regex literal compiles. - `2032` (E) - literal content is valid for its kind (`d'2024-13-45'`, `u'not-a-uuid'`). - `2033` (E) - PATCH operations are well-formed. - `2034` (E) - required fields are provided at creation. - `2035` (E) - DEFINE ANALYZER filter arguments are valid. - `2036` (E) - GeoJSON literals have their declared shape. - `2037` (E) - a field's DEFAULT satisfies its own ASSERT (a DEFAULT that folds to a value the ASSERT rejects fails every default-applied CREATE). ### 3xxx - Graph and relations Contract: a traversal or RELATE must use relations as declared (`in`->edge->`out`). - `3001` (E) - a step traverses a relation table (not a plain table). - `3002` (E) - the usage matches the relation's declared shape (wrong direction, hop landing off the far side, RELATE writing endpoints on the wrong sides). - `3004` (E) - a FROM-position chain is complete (edge->target pairs). - `3009` (E) - a traversal starts from records. - `3011` (W) - graph recursion is bounded (`@{..}` with no upper bound). ### 4xxx - Statement and clause misuse - `4001` (E) - clause valid on this statement (`SELECT ... RETURN NONE`, `CREATE ... WHERE`). - `4003` (E) - ONLY on a table-wide target needs LIMIT 1 (`SingleOnlyOutput`). - `4004` (E) - INSERT tuple column/value count matches. - `4005` (E) - BREAK/CONTINUE only inside a loop. - `4006` (W) - no unreachable statements after RETURN/BREAK/THROW, or after an IF/ELSE (or nested block) whose every branch diverges. - `4007` (E) - transaction pairing: BEGIN opens exactly one transaction that COMMIT/CANCEL closes. - `4009` (E) - LIVE SELECT with an unsupported clause. - `4010` (W) - no duplicate SET target in one statement. - `4011` (W) - no duplicate projection key/alias. - `4012` (W) - OMIT without a wildcard projection. - `4013` (W) - GROUP BY field not in the projections. - `4016` (I) - empty block. - `4017` (W) - a block ending with LET has value NONE. - `4018` (W) - no side-effecting subquery in read position. - `4019` (W) - CREATE/INSERT on a relation table sets `in`/`out`. - `4020` (W) - RETURN mode meaningless for the statement. - `4021` (E) - SHOW CHANGES only on a table with CHANGEFEED. - `4022` (W) - SELECT from a DROP table (rows are never retained). - `4023` (W) - a bare `count()` without GROUP yields 1 per row, not a total. ### 5xxx - Functions and closures - `5001` (E) - a call resolves to a function that exists (unknown builtins, undefined `fn::`, methods unavailable on the receiver's kind). - `5002` (E) - a call matches the function's signature (arity, per-argument kinds, `fn::` declared params, closure arity). - `5005` (E) - a const argument satisfies the function's value contract (`type::field('aeg')` naming no field, `type::thing('ghost', ...)`). - `5009` (W) - `fn::` definitions terminate (no recursion cycles). - `5010` (W) - events do not trigger themselves, directly or in a cycle. ### 6xxx - Parameters - `6001` (E) - constraints on one param unify (`WHERE $x > 3 AND $x = 'abc'` has no satisfying value). - `6002` (W) - a param does not shadow a DEFINE PARAM with a different kind. - `6004` (W) - a param is not used before its LET in source order. - `6005` (E) - a context param is used inside its context (`$before` outside an event, `$parent` outside a subquery). - `6006` (E, host-static) - a host-declared type does not contradict a query constraint (host binds `$age: string`, query needs int) - enforced by adapters. - `6007` (E) - no assignment to a protected parameter (`$auth`, `$session`, `$token`, `$this`, ...). ### 7xxx - Lints (levels configurable) - `7001` (W) - unused LET binding. - `7002` (I) - LET shadowing. - `7003` (I) - mixed-kind array literal (`[1, 'a']`). - `7004` (W) - control flow decided by a constant (`IF true`, `WHERE 1 = 1`, `FOR $x IN []`). - `7005` (W) - an `=`/`!=` that is provably constant: a comparison against a closed literal set (`WHEN $event = 'CRATE'`), a `NONE`/`NULL` sentinel against a kind that excludes it (`option = NULL`), or two record links with disjoint table sets (`record = record`). - `7006` (W) - a membership test (`IN`/`CONTAINS`/`INSIDE`) that can never match: an empty collection, or an element whose kind is incompatible with the collection's element kind (`array CONTAINS 5`). - `7007` (I) - `SELECT *` alongside explicit fields. - `7008` (I) - schemaless table in a typed workspace. - `7009` (W) - whole-table UPDATE/DELETE without WHERE. - `7011` (W) - assignment to `id` in SET. - `7012` (W) - blocking or side-effecting call in a computed context (`http::get(...)` / `sleep()` in a field VALUE or event body). - `7013` (W) - a suppression directive names a catalog code (with a reason when required). ### 8xxx - Version compatibility Gated on `[analysis] surrealdb_version`. - `8001` (E) - every function used exists in the configured target version (unavailable or renamed - the message suggests the new name). - `8003` (E) - syntax requires a newer version (closures, `??` on old targets). --- ## 6. Parameter constraints and context parameters An unbound `$param` used in a checkable position produces a **constraint** the host adapters discharge, not a finding. Constraints from multiple uses intersect (unify); an empty intersection is finding `6001`. Examples of the constraint each use produces: - `SET age = $age` -> `$age: int` (the field's kind) - `WHERE age > $min` -> `$min: numeric` - `string::len($s)` -> `$s: string` - `fn::greet($n)` -> `$n: string` (from the DEFINE) - `type::field($f)` -> `$f: string` and value in the table's field paths - `FROM $tbl` -> `$tbl: table | record` (domain: known tables) - `KILL $id` -> `$id: uuid` - `LIMIT $n` -> `$n: int` and `n >= 0` The exported `ParamInference` carries: `name`, unified `kind`, optional value `domain` (`OneOf(values)` or `Range { min, max }`), `required` (no DEFINE PARAM default), and every use-site span. Host adapters (the Rust macro, the TypeScript codegen) enforce these at the call site - at the host's compile time where possible, or as a runtime guard for values the host cannot prove statically. Context parameters are injected by SurrealQL; the analyzer models them as implicitly bound with kinds where the context defines them, so their bodies type-check and using one outside its context is `6005`: - `$this` (any row scope) - current row (table's object type) - `$parent` (subquery) - enclosing row - `$event` (DEFINE EVENT) - `'CREATE' | 'UPDATE' | 'DELETE'` (literal union) - `$before`, `$after` (DEFINE EVENT) - the table's row type - `$value`, `$input` (EVENT / FIELD VALUE-ASSERT) - event row / field's declared type - `$auth`, `$session`, `$token`, `$scope` - session-shaped objects (open) --- ## 7. Rust - `surrealguard-rs` Compile-time-checked, typed SurrealQL. Add it via git until the crates.io release: `surrealguard-rs = { git = "https://github.com/DrewRidley/surrealguard" }`. The crate re-exports two proc-macros; a crate using them only needs `surrealguard-rs` in scope (it re-exports every dependency the expansion references: `serde`, `chrono`, `uuid`, `serde_json`). ### Macros - `query!("...")` - check and return a typed `Query` whose `T` is the inferred, nameless result type. - `surql!("...")` - check and expand to the query text as a `&'static str` (the lighter form when you only want validation). ```rust use surrealguard_rs::{query, surql}; let users = query!("SELECT name, age FROM user"); // users: Query> <- nameless struct, inferred let sql = surql!("UPDATE user SET age = 30"); // sql: &'static str - validated, unchanged text ``` ### The compiler is the checker The macro runs the real analyzer at compile time and turns each error-severity finding into a spanned `compile_error!`. On stable Rust the span covers the whole literal; the message carries every finding's code and text. ```rust let bad = query!("SELECT name, ssn FROM user"); // error: SurrealGuard rejected this query: // [E1002] unknown field `ssn` on table `user` ``` With no schema configured, queries are still checked for everything that doesn't depend on one (syntax, function arity, operators). ### Schema resolution Resolved at compile time from, in order: 1. the `SURREALGUARD_SCHEMA` environment variable - a `.surql` file or a directory, taken relative to `CARGO_MANIFEST_DIR` unless absolute; 2. otherwise a convention path under the crate root, first match wins: `schema/`, `migrations/`, then `schema.surql`. A directory contributes every `.surql`/`.surrealql` file it contains, **sorted by path**, so zero-padded migrations (`0001_*.surql`, `0002_*.surql`) apply in order. The macro emits an `include_bytes!` per schema file, so editing a schema forces the query's crate to recompile. ```bash SURREALGUARD_SCHEMA=db/schema.surql cargo check ``` ### Nameless typed results `query!` expands to a `Query`. Object shapes become `struct`s defined inside the macro's own block scope - the type escapes as a value but its name never does, so you get nested field access without ever writing a type (like sqlx's `query!`). Generated types derive `serde::Deserialize`. A statement with no response (e.g. a bare `LET`) yields `()`. `Query` carries the validated text (`.text: &'static str`) and can decode a JSON encoding of the rows (`.from_json(json) -> serde_json::Result`). Execution: the shipped crate **checks and types** queries; it does not itself run them. Running a `Query` against a live database uses the official `surrealdb` Rust SDK - the execution layer is the next layer on top. ### Kind -> Rust type mapping - `bool` -> `bool` - `int` -> `i64` - `float` / `number` / `decimal` -> `f64` - `string` / `regex` -> `String` - `datetime` -> `chrono::DateTime` - `uuid` -> `uuid::Uuid` - `duration` -> `String` (serialized as `1h30m`) - `bytes` -> `Vec` - `none` / `null` -> `()` - `array` / `set` -> `Vec` - `option` -> `Option` - `record` -> `String` (the id decodes as a string; also surfaced as a typed `RecordLink` in the runtime) - closed object -> a nested block-local `struct` - open object / geometry / range / file / `any` -> `serde_json::Value` Literal kinds follow their base: a literal string is `String`, a literal integer is `i64`, a literal array becomes a tuple, a literal object becomes a struct. --- ## 8. TypeScript Four packages: `@surrealguard/client`, `@surrealguard/query`, `@surrealguard/next`, `@surrealguard/svelte`. ### `@surrealguard/client` `SurrealGuardClient` extends the official `surrealdb` SDK's `Surreal` class, so `new SurrealGuardClient()` has every SDK method plus typed queries. Pass a string literal to `query` and its result and params resolve from the generated registry; a dynamic string falls back to the SDK's `unknown[]` and still runs. `surrealdb` is a peer dependency. ```ts // One import: the generated file re-exports a ready client and loads the registry. import { SurrealGuardClient } from "./surrealguard.generated"; const db = new SurrealGuardClient(); await db.connect("ws://localhost:8000/rpc"); await db.use({ namespace: "app", database: "app" }); // SurrealDB returns one result per statement, so destructure the first result. const [users] = await db.query("SELECT name FROM user WHERE team = $team", { team: "red" }); // ^ Array<{ name: string }> - result & params inferred from the query text await db.query("SELECT name FROM user WHERE team = $team"); // x Expected 2 arguments, but got 1. (params required when the query reads them) ``` Because the client IS a real `Surreal`, `connect()`, `use()`, live queries, and every other SDK method behave exactly as the SDK documents. Signatures and exports: ```ts class SurrealGuardClient extends Surreal { // The SDK's `query` builder is nominal and unexported, so the override // intersects it with `Promise` - narrowing what `await` yields while // staying assignable to the base method (no runtime change, no lie about it). query(query: Q, ...args: ArgsOf): QueryBuilder & Promise>; } // Required exactly when the query reads params, forbidden otherwise - one // conditional generic, proven against tsc. There is deliberately NO permissive // string overload for text; a second overload would defeat the inference. type ParamsArg

= P extends Record ? [] : [params: P]; type ArgsOf = Q extends keyof SurqlRegistry ? ParamsArg : [bindings?: Record]; type QueryResultOf = Q extends keyof SurqlRegistry ? [SurqlRegistry[Q]["result"]] : unknown[]; type RecordId = string & { readonly __table?: T }; ``` Also exports `GeoJSON`, `SurqlRegistry`, `SurqlQueryShape`, `ArgsOf`, `QueryResultOf`. ### `surrealguard generate` Emits one TypeScript module that re-exports a ready `SurrealGuardClient` and augments `@surrealguard/client`'s registry - one entry per analyzed query, keyed by its exact text. The base `SurqlRegistry` is empty; the generated file only ever adds entries. Import the client from this one file (that also loads the augmentation) and every matching `db.query("...")` is typed, no side-import. ```ts import type { RecordId, GeoJSON } from "@surrealguard/client"; export { SurrealGuardClient } from "@surrealguard/client"; declare module "@surrealguard/client" { interface SurqlRegistry { "SELECT * FROM user": { result: Array<{ id: RecordId<"user">; name: string }>; params: Record; }; } } ``` ### `@surrealguard/query` A framework-agnostic reactive core. A `QueryClient` (constructed from a `SurrealGuardClient`) owns a cache of results keyed by `(sql, params)`. ```ts import { QueryClient } from "@surrealguard/query"; import { SurrealGuardClient } from "./surrealguard.generated"; const db = new SurrealGuardClient(); await db.connect("ws://localhost:8000/rpc"); const qc = new QueryClient(db); const handle = qc.observe("LIVE SELECT * FROM user", { initialData }); const stop = handle.subscribe((state) => render(state.data)); ``` - `observe(sql, options)` - reference-counted subscription. Identical `(sql, params)` calls share one cache entry via a stable `queryKey`; the first subscriber starts it, the last to leave tears it down. Returns an `Observable` with `get()` and `subscribe(listener)`. - **Live reconcile by id.** `LIVE SELECT` (matched by a leading `live` keyword) seeds from `initialData` (SSR), subscribes to the live-query id via the SDK's `liveOf`, then applies notifications keyed by record `id` - `CREATE`/`UPDATE` upsert, `DELETE` removes. A plain query resolves once. - `fetch(sql, params)` - a one-shot query outside the reactive layer. - `dehydrate()` - snapshot successful cached results for SSR transport. - `hydrate(state)` - seed the cache from a server snapshot so the client renders without a refetch, then (for live queries) upgrade in place. State shape: `QueryState = { data: Row[]; status: "loading" | "success" | "error"; error?: unknown }`. ### `@surrealguard/next` Provide the client once via ``, then call `useLiveQuery` in a client component with a callback that returns a `db.live(...)` descriptor. The row type is inferred from the query. ```tsx // app/providers.tsx "use client"; import { SurrealGuardProvider } from "@surrealguard/next"; import { db } from "@/lib/db"; export function Providers({ children }) { return {children}; } // app/users/users-list.tsx "use client"; import { useLiveQuery } from "@surrealguard/next"; function Users({ initialData }) { const { data, status } = useLiveQuery((db) => db.live(`SELECT * FROM user`), { initialData }); if (status === "loading") return ; return data.map((u) =>

  • {u.name}
  • ); } ``` `useLiveQuery(fn, options?)` returns `{ data, status, error }`. Built on `useSyncExternalStore`; seeds from `initialData` (no hydration gap), subscribes on mount, reconciles live notifications, and releases the reference-counted subscription on unmount. `@surrealguard/next/server` is server-safe: a Server Component runs `queryServer(db, db.live(`...`))` once and passes the rows as `initialData`. ### `@surrealguard/svelte` Svelte 5 (runes). Provide the client once with `setClient(db)` in `+layout.svelte`, then call `liveQuery` with a callback returning a `db.live(...)` descriptor. It returns a runes-reactive object read directly as `users.data` (no store `$` prefix); the row type is inferred from the query. ```svelte {#each users.data as user (user.id)}
  • {user.name}
  • {/each} ``` For gap-free SSR, seed from a `load` with `loadLive(db, db.live(`...`))` and pass the rows as `liveQuery(fn, { initial })`; the client subscribes and upgrades to live in place. In both adapters `db.live(...)` takes the query as an argument (the parentheses) so its literal type - and the row type - is preserved. --- ## 9. Editors and the language server (LSP) The SurrealGuard language server speaks LSP over stdio and drives the same analyzer as the CLI, so what an editor shows and what CI reports never diverge. You don't install it by hand - the Zed extension auto-downloads the binary. It handles both `.surql` files and SurrealQL embedded in host files (TypeScript, Svelte, Vue, Astro) - findings land on the exact token inside the inline query. Editor features: - **Diagnostics** with byte-precise ranges, `help:` suggestions, and `note:` related spans. It reads `surrealguard.toml`, so the workspace `[lints]` levels (allow/warn/deny, per-code and per-family) apply live in the editor. - **Inlay type hints** - the inferred kind of each `LET` binding is shown inline in grey, so you see what a query resolves to without hovering. - **Hover** resolves a type by span: context params (`$value`, `$event`, `$before`, `$after`, `$auth`, ...), `LET` variables, and table names (hovering a table lists its declared fields and their kinds). - **Dead code** is greyed out - findings that mark a span unnecessary (e.g. unreachable statements) carry the LSP "unnecessary" tag. Zed: install the `DrewRidley/zed-surreal` extension (https://github.com/DrewRidley/zed-surreal), which auto-downloads the language server and launches it for `.surql` files - no manual LSP install. ## 10. Using SurrealGuard with a coding agent Paste-in setup prompt: > Set up SurrealGuard in this project. Run the CLI with `npx surrealguard init`, > point surrealguard.toml at my .surql schema and query directories, then run > `npx surrealguard check --json` and fix any reported findings. Where I write > queries, use the Rust `query!` macro (surrealguard-rs) or the typed > `@surrealguard/client` db.query so they're checked against my schema. Why it works well for agents: - **Structured findings.** `surrealguard check --json` emits code, severity, source, byte range, message, help, and related spans - parse findings instead of scraping prose. - **Byte-precise spans.** Every finding carries an exact byte range for surgical inline edits, even inside a query embedded in a host file. - **Compile-time errors.** In Rust a bad query fails `cargo check` with a message carrying the code and text (`[E1002] unknown field ...`). In TypeScript a wrong or missing param is a `tsc` error. Agents already read compiler output. - **Deterministic exit code.** `check` exits non-zero exactly when an error-severity finding survives policy, so an agent loop knows when it's done. The check loop: run `check --json`, read the `diagnostics` array, edit at each `range`, repeat until the array is empty and the exit code is `0`. In CI, run `surrealguard check` as a gate - the non-zero exit fails the build on any error-severity finding. Context files: `/llms.txt` (short overview) and `/llms-full.txt` (this file). --- ## 11. Links - Docs: https://surrealguard.dev/docs/ - Short overview for LLMs: https://surrealguard.dev/llms.txt - Full LLM reference: https://surrealguard.dev/llms-full.txt - GitHub: https://github.com/DrewRidley/surrealguard - crates.io: https://crates.io/crates/surrealguard - npm: https://www.npmjs.com/org/surrealguard (`@surrealguard/client`, `@surrealguard/query`, `@surrealguard/next`, `@surrealguard/svelte`) - Rust crate: `surrealguard-rs` (re-exports the `query!` / `surql!` macros) License: MIT OR Apache-2.0.