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