Skip to main content

omgbase_surface/
reference.rs

1//! The `query_syntax` text (`spec/surface/README.md` §4: reference prose,
2//! unpinned). A condensed distillation of `docs/query-language.md`. Byte for
3//! byte the reference's `packages/core/src/mcp/reference.ts` — a test below
4//! compares the two when the monorepo is present, and another parses every
5//! example the text shows.
6
7/// The agent-facing OQX syntax reference the `query_syntax` tool returns.
8pub const QUERY_SYNTAX: &str = r##"# query — OQX syntax reference
9
10The `query` tool takes ONE plain OQX string (+ optional `limit`/`cursor`). There
11is no {from, filter, order} envelope: every concern is a clause of the string.
12
13  [select <items>] from <target> [where <pred>] [frontier <pred>] [follow <dest> [{ … }], …]
14                   [order by <expr> [asc|desc], …] [limit N] [offset N]
15  <target> count|exists|none|first|single { <block> }     (scalar/one-row form: a bare target at the root scope)
16
17CLAUSE ORDER IS FIXED: select, from, where, frontier, follow, order by, limit, offset —
18each at most once; an out-of-order clause is a parse error naming the order.
19Only `select` may drop its keyword, and only as the first clause
20(`path, era from docs where …`). A top-level predicate needs `where`, because
21`where` follows `from` (`era > 1600 from docs` is the clause-order error); a
22block may LEAD with a predicate (see Sugar), but a predicate after a projection
23still needs the keyword (`{ name where kind == "md:task" }`, not
24`{ name, kind == "md:task" }`); `from docs count` is an error (write
25`docs count { … }`). `where` and later `select` items may use the same body's
26`select` aliases (`select path, old: era < 1000 from docs where old`; an alias
27shadows a same-named field there). `and` / `or` / `not` are the connectives;
28the symbols `&&` / `||` / `!` are accepted synonyms (the words are canonical and
29what `print` writes).
30
31## Sugar (OQX 0.17)
32
33Every form here is shorthand for an explicit directive (the AST is the explicit form):
34  nodes { kind == "md:task" }        ≡ nodes collect { where kind == "md:task" } — a receiver block with
35                                       no consumer is collect; a block whose LEADING expression is a predicate
36                                       (comparison, call, !/is/not, in, literal, (…), a consumer test — anything
37                                       but a bare name, a dotted path or a ^lift) is where-first. Bare names still
38                                       project: nodes { name } ≡ nodes collect { name }; filter a bare field with
39                                       nodes { is checked }. Wherever a value stands (0.20), call arguments included:
40                                       size(children { is checked }); x distinct { … } ≡ x collect distinct { … }. After a
41                                       follow destination too: follow children { depth 2 } is that destination's block.
42  (a, b)                              a tuple (0.20): two or more expressions, concatenated one level flat — a value
43                                       (size((tags, aliases)), "x" in (tags, aliases)) and a receiver ((a, b) { … },
44                                       follow (before, after) { depth 2 }); (a) alone is grouping
45  refs(x)[0]                          ≡ refs(x) first { offset 0 }   positional (integer literal or ${binding});
46                                       out of range ⇒ absent; refs(x)[0].path navigates the row
47  ^docs[path == ^company]             ≡ ^docs first { where path == ^company }   first match or absent
48  ^docs[path == ^company]!            ≡ … single { … } required: exactly one, else filter_invalid
49  title!                              required: title, or an error naming the expression (and the row's $key — its id)
50                                       — never a filter, never a coercion (0!, ""! are values); tightest: a!.b vs a.b!
51  is x / not x                        truthiness of x, and its negation (prefix; `not` ≡ `!`)
52  x is y / x is not y                 identity ($key: a store row's id, else structural) — compares rows, which == does not;
53                                       x is null = absent; for scalars is ≡ ==; comparison precedence, no chaining
54  a and b / a or b                    the connectives (≡ && / ||: same precedence, short-circuit, value: data.title or path coalesces)
55  boss: ^docs[path == ^manager], bossName: boss.title   a select item may use the items to its LEFT
56  Reserved words (never a bare field name): from where select is not and or, true false null.
57  Contextual words (limit order offset follow collect count first distinct desc by in frontier depth) parse as
58  fields in expression position (select o: order from docs).
59
60Results are LEAN hits — { id, path } + whatever `select` projects (or `values`,
61`count`, `exists`, `none`). The envelope keys ARE the attributes: copy a hit's
62`id` straight into `where id == "…"` (no translation); `select id, path` is simply
63the envelope (no duplicate columns); any OTHER projection whose output key would
64be `id` or `path` must be aliased — `select data.path` is refused ("projection
65`path` collides with the hit envelope — alias it (`p: data.path`)"); a row
66surfacing as a value (an empty `collect { }`) is the same `{ id, path }`. Every
67other tool spells a row the same way (`docs_read` → `{ id, path, rev, properties,
68raw }`, `blocks_read` → `{ id, type, text, raw, hash, … }`, `docs_list` → `{ id,
69path, updated_at, blocks }`, a revision → `{ id, path, hash }`): hydrate by id via
70blocks_read/docs_read.
71
72## Paths
73
74Every path the surface returns is `/`-rooted — the form a reference is
75written in (`[x](/projects/oqx.md)`, `before: [/timeline/kickoff.md]`): a
76hit's `path`, an edge's `dst_path`, every tool's paths. Every path it
77accepts tolerates both forms (`/a.md` or `a.md`): tool arguments, `refs(x)`,
78`within(...)`, and a string LITERAL compared with `path`/`dst_path` (bare, or
79through a row handle: `doc.path`) by `==`/`!=` or passed to their
80`.startsWith(...)` (it is rooted first, so `path == "a.md"` and `path == "/a.md"`
81both match). A property holding a reference compares directly — and is never
82rooted: `where ^path in list(after)`, `where customer == ^path`,
83`where data.path == "/x.md"` (the author's `path:` key, as written) — no
84`"/" + ...` glue.
85
86## Targets & field namespaces
87
88THE NAME RULE: `$` belongs to OQX — $it, $key, $depth, $stop, $leaf, $frontier,
89$ordinal are values the LANGUAGE injects into a scope; omgbase adds NO $ names
90(a `$id` in a query is an unknown name, absent). Every engine attribute is a BARE
91snake_case name that WINS on collision — the convention the relations (children,
92blocks, out, in, doc, frontmatter, …) always followed: a bare name that is an
93engine attribute reads the engine's value, never yours. Your data is bare too;
94where a key of yours collides with an engine name, reach it through a source
95handle: `data.<k>` (the merged authored view — frontmatter + inline, exactly the
96read a bare non-colliding key performs), `frontmatter.<k>`, `inline.<k>`. So
97`path` is the engine's /-rooted location and `data.path` your `path:` key;
98`tags` is your tags ∪ the body #hashtags and `data.tags` the authored list alone;
99`title` the computed title (the first H1) and `data.title` your `title:`;
100`entries(data)` lists every authored top-level key. Same value, same name
101everywhere: a hit's `id` is docs_read's `id` and the `id` argument of blocks_read.
102
103  Reserved bare names per kind (engine attributes; handles; self; relations):
104    docs    id path raw hash updated_at title tags format | data frontmatter inline | doc | blocks nodes out in out_links in_links
105    blocks  id path doc_id raw hash updated_at type text attrs depth ordinal | block doc | children nodes out_links section
106    nodes   id path doc_id block_id kind name value attrs | section doc block | blocks subsections children
107    links   id path doc_id block_id via_id dst_id dst_path uri field dst_kind predicate provenance anchor | doc
108  A key of yours with any of these names is `data.<k>` (or frontmatter.<k> /
109  inline.<k> by source; attrs.<k> on a block or node).
110
111docs:
112  - bare identifier  = a document PROPERTY (frontmatter + inline `key:: value`,
113                       unioned; nested via dots: meta.owner). YAML/JSON docs
114                       expose the parsed object's keys the same way.
115  - source handles   = data.<k> (merged authored view) / frontmatter.<k> /
116                       inline.<k>; the whole bag as a collection via entries(data)
117                       / entries(frontmatter) / entries(inline) (see entries()).
118  - attributes       = id, path (/-rooted, see Paths), updated_at (ISO-8601,
119                       sorts chronologically), raw (the whole file, frontmatter
120                       included), hash (its sha256, 64 hex), title (the computed
121                       title: the first H1; data.title is the authored key), tags
122                       (authored tags ∪ body #hashtags, deduped, authored first;
123                       data.tags is the authored list), format (the doc's format —
124                       "markdown"/"yaml"/"json"; present on EVERY doc, so
125                       has(format) is always true and `format == "yaml"` is the
126                       filter).
127  - relations        = nodes, blocks, doc.out_links / doc.in_links (a doc's
128                       links as rows; the documents at their other end are
129                       out_links collect distinct { from dst } and
130                       in_links collect distinct { from doc } — there is no
131                       out / in relation).
132
133blocks:
134  - bare fields      = doc_id (the owning doc), type, text (normalized), ordinal
135                       (0-based among siblings), depth (0 at the top level), and
136                       every attrs key FLATTENED onto the row (bare `checked`
137                       reads attrs.checked; `attrs.<k>` still works; the
138                       structural fields win on collision; absent ⇒ false).
139  - attributes       = id, path (the owning doc's path), raw (the block's raw
140                       source — a container's raw holds its children), hash
141                       (its sha256 — the value update/split expect as
142                       expect.content_hash), updated_at.
143  - reach-through    = doc.<key> / doc.path / doc.id / doc.format = the
144                       CONTAINING doc (doc.id and doc.path cost nothing — they
145                       read the row's own doc_id / joined path, no lookup).
146  - relations        = block.children, block.nodes, block.out_links, section.
147  NOTE: inside a `follow`, $depth/$ordinal are ALWAYS the walk's metadata; a
148  block's own nesting is `depth` / `ordinal` (no $) everywhere.
149
150nodes:
151  - bare fields      = doc_id, block_id (null for a block-less node), kind
152                       (md:task / md:link / md:section / yaml:…), name, value,
153                       + attrs keys flattened (checked, level).
154  - attributes       = id, path.
155  - reach-through    = doc.<key> / doc.id / doc.path, block.type / block.text
156                       / block.id / block.path.
157  - relations        = section.blocks, section.children, section.subsections.
158
159links:  (the authored link graph as rows — one per open link: the relation a link
160        establishes; the md:link NODE is its occurrence in the text and via_id
161        points at it — a frontmatter link has a row and no node)
162  - bare fields      = doc_id (the SOURCE doc), block_id (the source block; null
163                       for a frontmatter edge), via_id, dst_id (the destination
164                       node id: a doc id, an external node id or phantom:<path>),
165                       dst_path (target doc path, /-rooted; NULL when
166                       dangling/external), uri (external URL; NULL otherwise),
167                       predicate ("references"/"embeds"/freeform), provenance
168                       ("link"/"frontmatter"/"inline_field"/…), dst_kind
169                       ("document"/"external"/"collection"), anchor, field (the
170                       frontmatter key / inline field it was authored in).
171  - attributes       = id, path (the SOURCE doc's path); doc.<key> reaches the
172                       source document. There is no from_commit: a query sees
173                       open edges only (history is blocks_history/changes_since).
174  - text()/semantic() are N/A on links.
175
176## Scoping (ADR-015; the root row since surface 2.0): bare names are LOCAL
177
178  name          the CURRENT row only — never falls through to an enclosing row
179                or the repository; an absent field is simply absent.
180  ^name         one scope OUT (the enclosing row's fields/intrinsics/lifts);
181                ^^name two scopes out. `^path` = the enclosing row's path.
182  ^docs ^nodes ^blocks ^links
183                THE ROOT ROW (surface 2.0, oqx 0.18): the repository is the root
184                scope's row, one scope out from a top-level row, so ^docs there is
185                the whole documents scan (uncorrelated until you add a ^ predicate);
186                one caret per enclosing block (^^docs from depth two), or the
187                absolute 0^docs from ANY depth. ^id / 0^id = the repository id.
188                ^$it = the root object: ^$it.docs ≡ ^docs, entries(^$it) names the
189                four collections. At the root scope itself a bare target is the scan
190                (docs count { … }, from docs, entries(docs)).
191                ERRORS: a bare `docs` INSIDE a block reads a property of the current
192                row, which has none — refused ("did you mean ^docs"); `^docs` AT THE
193                TOP LEVEL reaches past the root — refused (write the bare `docs`);
194                so is `$repo` (surface < 2.0; the message names the replacement).
195                nodes/blocks on a doc row are that doc's RELATIONS, not the root.
196  $it           the current item itself (a row, or each ELEMENT when the
197                receiver is a list property); ^$it = the enclosing row.
198                $value is NOT an intrinsic (since oqx 0.15): it reads a
199                property literally named $value, absent when there is none.
200  $key          the row's identity (0.20): a store row's id (doc/block/node/edge), an entries(x)
201                entry's key, a follow occurrence's walk identity; project $key: <expr> to rekey the
202                rows a block yields (a destination block's $key: is not a column).
203
204## Scalar semantics (@omgbase/oqx)
205
206  comparisons  ==  !=  <  <=  >  >=    strict & typed (5 == "5" is false); either
207                                       side may be a field (field-vs-field OK).
208  absence      null and a missing field are the same "absent" value; two absents
209               are EQUAL, so `absent != "x"` is TRUE and `absent == null` is TRUE.
210               Relational (< <= > >=) on an absent operand → FALSE. Bare `f` in
211               boolean position uses JS truthiness (absent/false/0/"" ⇒ false;
212               NOTE: "false", [] and {} are truthy). has(f) = explicit presence.
213  boolean      and  or  not    grouping ( … )   — && || ! are accepted synonyms
214  arithmetic   + - * / %    (+ concatenates if either side is a string)
215  membership   x in y       array → typed membership; string → substring;
216                            object → key exists; range → coverage. Any list
217                            property works directly: "pricing" in tags. list(f)
218                            coerces scalar-or-list (absent → []) — use it when a
219                            key is sometimes scalar, sometimes a list.
220  ranges       lo..hi (inclusive)  lo...hi (exclusive hi)  ..hi  lo..
221               era in 800..1680; works over ISO dates. range(prop) reads a
222               range-shaped STRING property as an interval: "2026-01-15" in range(window).
223  strings      x.contains("s") x.startsWith("p") x.endsWith("s") x.matches("^re$")
224               (real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE —
225               fold with .lower(): title.lower().contains("aurora").
226  functions    has(f)  size(x)  list(x)  entries(x)  range(s)
227  literals     "str" 'str' 123 1.5 true false null
228  determinism  no now()/clock/random. Lists/structs are not literals; ternary is
229               not supported. Errors surface as filter_invalid.
230  scalars vs lists  a list-authored key never == a scalar (tags == "x" is false
231               when tags is a list) — use `"x" in tags` / `in list(tags)`.
232
233## Domain predicates (omgbase)
234
235  text("terms")        FTS5 PRUNING predicate (docs match via their blocks, nodes
236                       via the node index); composes anywhere, incl. nested scopes.
237  semantic("phrase")   embedding cosine SCORE (docs/blocks): threshold it
238                       (semantic("x") > 0.6), project it, or `order by semantic("x")
239                       desc` for top-K. Needs a provider that has embedded the repository; else provider_unavailable.
240  blocks target only:
241    under(id_or_heading)      block in the containment subtree / section range
242    under_heading("Launch")   an ancestor section heading contains the text (ci)
243    under_kind(type[, name])  an ancestor block has that type (name: its text
244                              contains / its key equals name)
245    within("path" | "d_..")   containing doc = id, exact path, or path glob (*);
246                              either path form
247    yaml_path("a.b")          YAML docs: the block at that key path
248    json_pointer("#/a/b")     JSON docs: the block at that pointer
249    has_edge("pred"[, target])  an open authored edge with that predicate leaves here
250    has_anchor()              block carries an authored ^ref
251    parent_type() == "x"      parent block's type (must be compared)
252    child_count() > 0         number of direct children (must be compared)
253  refs(x)              the live documents a property's document references name:
254                       x is a string, a list or absent; each "/a/b.md", "a/b.md"
255                       or "d_…" element resolves to that doc's row; dangling and
256                       non-string elements are dropped. Yields docs rows, so it is
257                       a source, a receiver or a follow destination:
258                       follow refs(before), refs(after) walks a timeline both ways.
259                       The reverse direction needs no function: a document's
260                       path is already the reference form, so
261                       ^docs { where ^path in list(after) } is
262                       "the documents whose `after` names me".
263  A HIT IS A STORE ROW: a top-level row that is not a doc/block/node/edge fails
264                       (filter_invalid). `follow before` over a list of paths
265                       reaches the STRINGS — write `follow refs(before)`.
266
267## Nested queries & consumers
268
269  <receiver> collect|exists|none|count|first|single { <block> }
270    receiver = a relation (nodes, blocks, doc.out_links, section.blocks, …), a
271               reach-through row (dst, doc: one row — dst { title }), a
272    list property (tags), entries(x), or a root collection (^docs; 0^docs at depth ≥ 2).
273    block    = [select …] [where …] [frontier …] [follow …] [order by …] [limit N] [offset N]
274               — the same fixed order as the top level (`from` optional; the
275               receiver supplies the rows). A leading name/alias list is the
276               projection (`select` dropped); a predicate always needs `where`.
277  exists    ≥1 row      nodes exists { where kind == "md:task" and not checked }
278  none      0 rows      nodes none { where kind == "md:task" and not checked }  ("every task done")
279  count     a number    nodes count { where kind == "md:task" } >= 2
280  collect   rows (default) — in select for nested results; in where with a lift
281  first / single   zero-or-one (single errors on >1) — lookups in select
282  distinct  on any consumer: dedup by projected value — nodes count distinct { select kind }
283  limit/offset  bound the block's rows AFTER where/order/distinct, BEFORE the
284            consumer reduces: nodes exists { offset 1 } = at least two;
285            first { … offset 1 } = the second.
286  lift      `^name:` in a where-collect binds values outward:
287            select path, open from docs where nodes collect { ^open: value where kind == "md:task" and not checked }
288  joins     ^<t> exists { where slug == ^ref }  semi-join; not … exists  anti-join;
289            ^nodes single { where kind == "person" and attrs.id == ^owner_id }  lookup;
290            x in ^keys  membership over a lifted set.
291
292## select — projection
293
294  select a, alias: expr, nested: nodes collect { … }
295  bare key → the attribute when it is one (id, path, title, …), else the property
296  (scalar-authored → scalar; list → array); data.<k> forces the property;
297  a call/arithmetic needs an alias (n: size(tags)) unless in values mode;
298  an output key of id/path other than the attribute itself needs an alias.
299  raw         whole file bytes (docs) — docs_read is cheaper for one doc.
300  distinct    select distinct type   (dedup hits by projected value)
301  values      select <ONE item> values → bare values instead of records:
302                select distinct type values from docs          → values: ["hub","lab-note",…]
303                select path values from docs                   → values: ["/index.md", …]
304                tags: tags collect { $it values }             → a plain array
305                h: nodes first { name values where kind == "md:section" order by first_ordinal }
306  entries(x)  a record as a collection ($key / $it per entry), key order:
307                select fm: entries(data) collect { k: $key, v: $it }
308                where entries(frontmatter) exists { where $key == "era" and $it > 1600 }
309                entries(inline), entries(attrs), a nested map, or a list (index keys)
310  $semantic_score is not a field — project semantic("…") under an alias instead.
311
312## order by, limit/offset, pagination
313
314  order by era desc, path asc      absent values sort LAST (asc); ties → (path, id)
315  limit N / offset N               define the result SET (integer literals);
316                                   the tool's limit/cursor then page WITHIN it
317  cursor                           opaque keyset on (path,id); a custom order by
318                                   disables it (you get the top page + truncated)
319
320## Graph traversal (follow) and links
321
322  The citation graph is the LINK ROWS walked through a destination block:
323  dst is a link's destination document (absent for an external or dangling
324  link — it then contributes no successor), doc its source.
325  from docs where <seed> follow out_links collect { from dst }     docs the seed links TO
326  from docs where <seed> follow in_links collect { from doc }      backlinks
327    follow out_links collect { from dst depth 3 }                  bound (1..8, default 8) — a clause of the block
328    follow out_links collect { from dst where layer == "canon" }   keep only matching successors
329    follow in_links collect { from doc where ^path in list(before) }   ^ = the row being expanded
330                                                   (^^ = the walk's enclosing scope)
331    follow out_links collect { from dst where predicate == "depends_on" }   one predicate only
332    follow out_links collect { from dst }, in_links collect { from doc }
333                                                   several destinations: their union
334                                                   (one step dedups by identity)
335    follow ^docs { where out_links exists { where dst_path == ^^path } }
336                                                   a destination block over the root's
337                                                   documents, re-read per frontier row
338                                                   (^ = that row) = the backlinks walk
339    follow out_links collect { from dst frontier type == "practitioner" }   cut, keeping the frontier row
340    from docs where type == "process" frontier !verified follow out_links collect { from dst }
341                                                   the BODY's frontier applies to the seeds:
342                                                   an unverified seed is kept, not expanded
343    follow distinct out_links collect { from dst } / … { $key: group from dst }   identity control ($key)
344    follow refs(before), ^docs { where ^path in list(after) }    forward over `before`, backward
345                                                   over every doc whose `after` names me
346  Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|
347  depth|cycle), $leaf/$frontier, $ordinal, $key — usable in select/order by and
348  the top-level post-walk where, NOT metadata of the candidates a destination
349  block's where reads.
350  Destination blocks (OQX 0.20): a brace after ANY destination opens that
351  destination's block — children { depth 2 }, refs(x) { depth 2 }, out_links
352  collect { from dst depth 2 }, ^docs { where ^path in list(after) } — with the
353  0.17 body rules (where-first, projections) and two clauses of its own after
354  `where`: frontier <pred> (occurrences reached through this destination that
355  match are frontiers; ^ there is the walk's enclosing scope) and depth <n>
356  (this destination expands a frontier row only while $depth < n). The body
357  that carries the follow may write frontier <pred> too, after its where and
358  before follow: it applies to the SEEDS (read like the body's where) — a
359  matching seed stays in the result with $stop == "frontier" and is not
360  expanded; each destination's frontier is its own. There is no options block;
361  the old `by` is $key. A bare follow ^docs (no brace, no consumer) is an
362  error naming both block spellings.
363  Type-preserving relations: block.children, section.children, section.subsections;
364  the citation graph is out_links collect { from dst } / in_links collect { from doc }.
365  from links where dst_path == "/notes/x.md"       inbound links to a doc, as rows
366  select d: dst.title, k: dst_kind from links       the destination document's title
367  from docs where out_links exists { where dst.layer == "draft" }   links into drafts
368  select b: in_links collect distinct { from doc } from docs     backlinks as { id, path }
369  select doc_id, dst_path from links where predicate == "depends_on"
370  from docs where not doc.in_links exists { }      orphans (nothing links in)
371  (The `graph` tool is a convenience wrapper that compiles to a follow query.)
372
373## semantic grain
374
375from docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.
376text()/where prune the candidate set; only order by reweights.
377
378## Examples
379
380  from docs where layer == "working"
381  from docs where path.startsWith("/guides/") and updated_at >= "2026-08-01"
382  select layer, tags from docs where "pricing" in list(tags)
383  from docs where inline.owner == "alice"
384  from docs where title.lower().contains("q3 plan")
385  from docs where format == "yaml"
386  from docs where type == "practitioner" order by era desc limit 2
387  from docs where nodes none { where kind == "md:task" and not checked }
388  from docs where tags exists { where $it == "tria-prima" }
389  select fm: entries(data) collect { k: $key, v: $it } from docs where path == "/x.md"
390  select owner_id, owner: ^nodes single { where kind == "person" and attrs.id == ^owner_id } from docs
391  from blocks where type == "task" and not checked and under_heading("Launch") and doc.layer == "working"
392  from blocks where type == "paragraph" and has_edge("references", "d_92aaaaa")
393  select path, $depth from docs where path == "/timeline/review.md" follow refs(before), refs(after)
394  select path, next: ^docs collect { path where ^path in list(before) } from docs where type == "milestone"
395  select path, n: size(^docs), repo: ^id from docs limit 1
396  select p: data.path, t: data.title from docs where has(data.path)
397  docs { type == "substance" and not verified }
398  select subject, process: ^docs[slug == ^subject]!.path from docs where type == "lab-note"
399  select path, back: nodes first { select p: 0^docs collect { path where ^^path in list(after) } values } from docs where type == "milestone"
400  select path, prior: refs(before) collect { path, when } from docs where type == "milestone"
401  from blocks order by semantic("identity preservation across edits") desc limit 10
402  select uri from links where dst_kind == "external"
403  docs count { where layer == "canon" }
404
405Common mistake: `$path`/`$id` (the 2.x spelling) — omgbase has no $ names; write
406`path`/`id`. Another: `where tags == "x"` on a list-valued key is false — write
407`"x" in tags`. Another: `select data.path` — alias it (`p: data.path`).
408"##;
409
410#[cfg(test)]
411mod tests {
412    //! Every OQX example the text shows must parse — extracted MECHANICALLY by
413    //! the rule the reference's `packages/core/src/mcp/reference.test.ts` states
414    //! (and mirrors here): (1) only indented lines are candidates — an error
415    //! example belongs in prose, a code line is a valid query; (2) in
416    //! `## Examples` every indented line is a whole query; (3) elsewhere the
417    //! query is the text before the first run of three or more spaces, skipped
418    //! when it holds a grammar placeholder (`<pred>`), an ellipsis (`…`) or
419    //! sentence punctuation (wrapped commentary); (4) a candidate is a query
420    //! when it begins `select `, `from `, or a root target followed by a
421    //! consumer or `{` (a `^`-led line is an expression fragment); (5) a
422    //! `select`-led candidate without a depth-0 ` from ` is a fragment.
423    use super::QUERY_SYNTAX;
424
425    const TARGETS: [&str; 4] = ["docs", "blocks", "nodes", "links"];
426    const CONSUMERS: [&str; 7] = [
427        "collect", "exists", "none", "count", "first", "single", "distinct",
428    ];
429
430    fn has_top_level_from(q: &str) -> bool {
431        let chars: Vec<char> = q.chars().collect();
432        let (mut depth, mut quote): (i32, Option<char>) = (0, None);
433        for (i, &c) in chars.iter().enumerate() {
434            if let Some(open) = quote {
435                if c == open {
436                    quote = None;
437                }
438                continue;
439            }
440            match c {
441                '"' | '\'' => quote = Some(c),
442                '{' => depth += 1,
443                '}' => depth -= 1,
444                _ if depth == 0 && chars[i..].starts_with(&[' ', 'f', 'r', 'o', 'm', ' ']) => {
445                    return true;
446                }
447                _ => {}
448            }
449        }
450        false
451    }
452
453    /// `<seed>`, `<pred>`, `<ONE item>`: a `<` + letter … `>` run of letters, `_`, `|`, space or `…`.
454    fn has_placeholder(q: &str) -> bool {
455        let chars: Vec<char> = q.chars().collect();
456        for (i, &c) in chars.iter().enumerate() {
457            if c != '<' || !chars.get(i + 1).is_some_and(|n| n.is_ascii_alphabetic()) {
458                continue;
459            }
460            let mut j = i + 1;
461            while j < chars.len()
462                && (chars[j].is_ascii_alphabetic() || matches!(chars[j], '_' | '|' | ' ' | '…'))
463            {
464                j += 1;
465            }
466            if chars.get(j) == Some(&'>') {
467                return true;
468            }
469        }
470        false
471    }
472
473    fn is_prose(q: &str) -> bool {
474        q.contains(". ")
475            || q.contains("; ")
476            || q.ends_with('.')
477            || q.ends_with(':')
478            || q.ends_with(';')
479    }
480
481    fn root_consumer(q: &str) -> bool {
482        let mut it = q.splitn(3, ' ');
483        let (Some(target), Some(next)) = (it.next(), it.next()) else {
484            return false;
485        };
486        TARGETS.contains(&target) && (CONSUMERS.contains(&next) || next.starts_with('{'))
487    }
488
489    /// `(line, section, query)` for every admitted example, plus the skipped `select` fragments.
490    fn extract(text: &str) -> (Vec<(usize, String, String)>, Vec<String>) {
491        let (mut queries, mut fragments) = (Vec::new(), Vec::new());
492        let mut section = String::new();
493        for (i, raw) in text.lines().enumerate() {
494            if let Some(h) = raw.strip_prefix("## ") {
495                section = h.trim().to_owned();
496                continue;
497            }
498            if !raw.starts_with("  ") || raw.trim().is_empty() {
499                continue;
500            }
501            let trimmed = raw.trim();
502            if section == "Examples" {
503                queries.push((i + 1, section.clone(), trimmed.to_owned()));
504                continue;
505            }
506            let query = trimmed.split("   ").next().unwrap_or("").trim();
507            if has_placeholder(query) || query.contains('…') || is_prose(query) {
508                continue;
509            }
510            if !(query.starts_with("select ") || query.starts_with("from ") || root_consumer(query))
511            {
512                continue;
513            }
514            if query.starts_with("select ") && !has_top_level_from(query) {
515                fragments.push(query.to_owned());
516                continue;
517            }
518            queries.push((i + 1, section.clone(), query.to_owned()));
519        }
520        (queries, fragments)
521    }
522
523    #[test]
524    fn extraction_is_substantial() {
525        let (queries, fragments) = extract(QUERY_SYNTAX);
526        let in_examples = queries.iter().filter(|q| q.1 == "Examples").count();
527        assert!(in_examples >= 20, "{in_examples} examples");
528        assert!(
529            queries.len() - in_examples >= 10,
530            "{} outside Examples",
531            queries.len() - in_examples
532        );
533        for f in &fragments {
534            assert!(
535                !TARGETS.iter().any(|t| f.contains(&format!(" from {t}"))),
536                "a fragment must not carry a top-level from: {f}"
537            );
538        }
539    }
540
541    #[test]
542    fn every_example_parses() {
543        let (queries, _) = extract(QUERY_SYNTAX);
544        let failures: Vec<String> = queries
545            .iter()
546            .filter_map(|(line, section, q)| {
547                oqx::parse_string(q)
548                    .err()
549                    .map(|e| format!("line {line} ({section}): {q}\n    {e}"))
550            })
551            .collect();
552        assert!(
553            failures.is_empty(),
554            "query_syntax examples that do not parse:\n{}",
555            failures.join("\n")
556        );
557    }
558
559    #[test]
560    fn error_examples_live_in_prose() {
561        let (queries, _) = extract(QUERY_SYNTAX);
562        for bad in [
563            "era > 1600 from docs",
564            "from docs count",
565            "from docs where \"pricing\" in list(tags) select layer, tags",
566        ] {
567            assert!(oqx::parse_string(bad).is_err());
568            assert!(
569                !queries.iter().any(|q| q.2 == bad),
570                "{bad} is an error example and must stay in prose"
571            );
572        }
573    }
574
575    #[test]
576    fn clause_order_sentence_matches_the_parser() {
577        let e = oqx::parse_string("from docs where x select y")
578            .unwrap_err()
579            .to_string();
580        let (_, rest) = e
581            .split_once("OQX clause order is ")
582            .expect("the parser names the order");
583        let order = rest
584            .split(" (")
585            .next()
586            .unwrap()
587            .trim_end_matches(['.', ' ']);
588        assert!(
589            QUERY_SYNTAX.contains(&format!("CLAUSE ORDER IS FIXED: {order}")),
590            "the text's order sentence drifted from the parser's ({order})"
591        );
592    }
593
594    /// The twin is byte for byte the reference's `QUERY_SYNTAX` (the TS
595    /// template literal, unescaped). Skipped outside the monorepo.
596    #[test]
597    fn twin_is_byte_identical_to_the_reference() {
598        let path = concat!(
599            env!("CARGO_MANIFEST_DIR"),
600            "/../../packages/core/src/mcp/reference.ts"
601        );
602        let Ok(ts) = std::fs::read_to_string(path) else {
603            return;
604        };
605        let open = "export const QUERY_SYNTAX = `";
606        let start = ts.find(open).expect("the TS export") + open.len();
607        let end = ts.rfind("`;").expect("the TS literal's end");
608        let mut text = String::new();
609        let mut chars = ts[start..end].chars();
610        while let Some(c) = chars.next() {
611            if c == '\\' {
612                if let Some(n) = chars.next() {
613                    text.push(n);
614                }
615            } else {
616                text.push(c);
617            }
618        }
619        assert!(
620            text == QUERY_SYNTAX,
621            "crates/omgbase-surface/src/reference.rs drifted from packages/core/src/mcp/reference.ts"
622        );
623    }
624}