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