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