1pub const QUERY_SYNTAX: &str = r##"# query — OQX syntax reference
6
7The `query` tool takes ONE plain OQX string (+ optional `limit`/`cursor`). There
8is no {from, filter, order} envelope: every concern is a clause of the string.
9
10 [select <items>] from <target> [where <pred>] [follow <dest> [{ … }], …]
11 [order by <expr> [asc|desc], …] [limit N] [offset N]
12 <target> count|exists|none|first|single { <block> } (scalar/one-row form: a bare target at the root scope)
13
14CLAUSE ORDER IS FIXED: select, from, where, follow, order by, limit, offset —
15each at most once; an out-of-order clause is a parse error naming the order.
16Only `select` may drop its keyword, and only as the first clause
17(`path, era from docs where …`). A top-level predicate needs `where`, because
18`where` follows `from` (`era > 1600 from docs` is the clause-order error); a
19block may LEAD with a predicate (see Sugar), but a predicate after a projection
20still needs the keyword (`{ name where kind == "md:task" }`, not
21`{ name, kind == "md:task" }`); `from docs count` is an error (write
22`docs count { … }`). `where` and later `select` items may use the same body's
23`select` aliases (`select path, old: era < 1000 from docs where old`; an alias
24shadows a same-named field there). `and` / `or` / `not` are the connectives;
25the symbols `&&` / `||` / `!` are accepted synonyms (the words are canonical and
26what `print` writes).
27
28## Sugar (OQX 0.17)
29
30Every form here is shorthand for an explicit directive (the AST is the explicit form):
31 nodes { kind == "md:task" } ≡ nodes collect { where kind == "md:task" } — a receiver block with
32 no consumer is collect; a block whose LEADING expression is a predicate
33 (comparison, call, !/is/not, in, literal, (…), a consumer test — anything
34 but a bare name, a dotted path or a ^lift) is where-first. Bare names still
35 project: nodes { name } ≡ nodes collect { name }; filter a bare field with
36 nodes { is checked }. Wherever a value stands (0.20), call arguments included:
37 size(children { is checked }); x distinct { … } ≡ x collect distinct { … }. After a
38 follow destination too: follow children { depth 2 } is that destination's block.
39 (a, b) a tuple (0.20): two or more expressions, concatenated one level flat — a value
40 (size((tags, aliases)), "x" in (tags, aliases)) and a receiver ((a, b) { … },
41 follow (before, after) { depth 2 }); (a) alone is grouping
42 refs(x)[0] ≡ refs(x) first { offset 0 } positional (integer literal or ${binding});
43 out of range ⇒ absent; refs(x)[0].path navigates the row
44 ^docs[path == ^company] ≡ ^docs first { where path == ^company } first match or absent
45 ^docs[path == ^company]! ≡ … single { … } required: exactly one, else filter_invalid
46 title! required: title, or an error naming the expression (and the row's $key — its id)
47 — never a filter, never a coercion (0!, ""! are values); tightest: a!.b vs a.b!
48 is x / not x truthiness of x, and its negation (prefix; `not` ≡ `!`)
49 x is y / x is not y identity ($key: a store row's id, else structural) — compares rows, which == does not;
50 x is null = absent; for scalars is ≡ ==; comparison precedence, no chaining
51 a and b / a or b the connectives (≡ && / ||: same precedence, short-circuit, value: data.title or path coalesces)
52 boss: ^docs[path == ^manager], bossName: boss.title a select item may use the items to its LEFT
53 Reserved words (never a bare field name): from where select is not and or, true false null.
54 Contextual words (limit order offset follow collect count first distinct desc by in frontier depth) parse as
55 fields in expression position (select o: order from docs).
56
57Results are LEAN hits — { id, path } + whatever `select` projects (or `values`,
58`count`, `exists`, `none`). The envelope keys ARE the attributes: copy a hit's
59`id` straight into `where id == "…"` (no translation); `select id, path` is simply
60the envelope (no duplicate columns); any OTHER projection whose output key would
61be `id` or `path` must be aliased — `select data.path` is refused ("projection
62`path` collides with the hit envelope — alias it (`p: data.path`)"); a row
63surfacing as a value (an empty `collect { }`) is the same `{ id, path }`. Every
64other tool spells a row the same way (`docs_read` → `{ id, path, rev, properties,
65raw }`, `blocks_read` → `{ id, type, text, raw, hash, … }`, `docs_list` → `{ id,
66path, updated_at, blocks }`, a revision → `{ id, path, hash }`): hydrate by id via
67blocks_read/docs_read.
68
69## Paths
70
71Every path the surface returns is `/`-rooted — the form a reference is
72written in (`[x](/projects/oqx.md)`, `before: [/timeline/kickoff.md]`): a
73hit's `path`, an edge's `dst_path`, every tool's paths. Every path it
74accepts tolerates both forms (`/a.md` or `a.md`): tool arguments, `refs(x)`,
75`within(...)`, and a string LITERAL compared with `path`/`dst_path` (bare, or
76through a row handle: `doc.path`) by `==`/`!=` or passed to their
77`.startsWith(...)` (it is rooted first, so `path == "a.md"` and `path == "/a.md"`
78both match). A property holding a reference compares directly — and is never
79rooted: `where ^path in list(after)`, `where customer == ^path`,
80`where data.path == "/x.md"` (the author's `path:` key, as written) — no
81`"/" + ...` glue.
82
83## Targets & field namespaces
84
85THE NAME RULE: `$` belongs to OQX — $it, $key, $depth, $stop, $leaf, $frontier,
86$ordinal are values the LANGUAGE injects into a scope; omgbase adds NO $ names
87(a `$id` in a query is an unknown name, absent). Every engine attribute is a BARE
88snake_case name that WINS on collision — the convention the relations (children,
89blocks, out, in, doc, frontmatter, …) always followed: a bare name that is an
90engine attribute reads the engine's value, never yours. Your data is bare too;
91where a key of yours collides with an engine name, reach it through a source
92handle: `data.<k>` (the merged authored view — frontmatter + inline, exactly the
93read a bare non-colliding key performs), `frontmatter.<k>`, `inline.<k>`. So
94`path` is the engine's /-rooted location and `data.path` your `path:` key;
95`tags` is your tags ∪ the body #hashtags and `data.tags` the authored list alone;
96`title` the computed title (the first H1) and `data.title` your `title:`;
97`entries(data)` lists every authored top-level key. Same value, same name
98everywhere: a hit's `id` is docs_read's `id` and the `id` argument of blocks_read.
99
100 Reserved bare names per kind (engine attributes; handles; self; relations):
101 docs id path raw hash updated_at title tags format | data frontmatter inline | doc | blocks nodes out in out_links in_links
102 blocks id path doc_id raw hash updated_at type text attrs depth ordinal | block doc | children nodes out_links section
103 nodes id path doc_id block_id kind name value attrs | section doc block | blocks subsections children
104 links id path doc_id block_id via_id dst_id dst_path uri field dst_kind predicate provenance anchor | doc
105 A key of yours with any of these names is `data.<k>` (or frontmatter.<k> /
106 inline.<k> by source; attrs.<k> on a block or node).
107
108docs:
109 - bare identifier = a document PROPERTY (frontmatter + inline `key:: value`,
110 unioned; nested via dots: meta.owner). YAML/JSON docs
111 expose the parsed object's keys the same way.
112 - source handles = data.<k> (merged authored view) / frontmatter.<k> /
113 inline.<k>; the whole bag as a collection via entries(data)
114 / entries(frontmatter) / entries(inline) (see entries()).
115 - attributes = id, path (/-rooted, see Paths), updated_at (ISO-8601,
116 sorts chronologically), raw (the whole file, frontmatter
117 included), hash (its sha256, 64 hex), title (the computed
118 title: the first H1; data.title is the authored key), tags
119 (authored tags ∪ body #hashtags, deduped, authored first;
120 data.tags is the authored list), format (the doc's format —
121 "markdown"/"yaml"/"json"; present on EVERY doc, so
122 has(format) is always true and `format == "yaml"` is the
123 filter).
124 - relations = nodes, blocks, doc.out_links / doc.in_links (a doc's
125 links as rows; the documents at their other end are
126 out_links collect distinct { from dst } and
127 in_links collect distinct { from doc } — there is no
128 out / in relation).
129
130blocks:
131 - bare fields = doc_id (the owning doc), type, text (normalized), ordinal
132 (0-based among siblings), depth (0 at the top level), and
133 every attrs key FLATTENED onto the row (bare `checked`
134 reads attrs.checked; `attrs.<k>` still works; the
135 structural fields win on collision; absent ⇒ false).
136 - attributes = id, path (the owning doc's path), raw (the block's raw
137 source — a container's raw holds its children), hash
138 (its sha256 — the value update/split expect as
139 expect.content_hash), updated_at.
140 - reach-through = doc.<key> / doc.path / doc.id / doc.format = the
141 CONTAINING doc (doc.id and doc.path cost nothing — they
142 read the row's own doc_id / joined path, no lookup).
143 - relations = block.children, block.nodes, block.out_links, section.
144 NOTE: inside a `follow`, $depth/$ordinal are ALWAYS the walk's metadata; a
145 block's own nesting is `depth` / `ordinal` (no $) everywhere.
146
147nodes:
148 - bare fields = doc_id, block_id (null for a block-less node), kind
149 (md:task / md:link / md:section / yaml:…), name, value,
150 + attrs keys flattened (checked, level).
151 - attributes = id, path.
152 - reach-through = doc.<key> / doc.id / doc.path, block.type / block.text
153 / block.id / block.path.
154 - relations = section.blocks, section.children, section.subsections.
155
156links: (the authored link graph as rows — one per open link: the relation a link
157 establishes; the md:link NODE is its occurrence in the text and via_id
158 points at it — a frontmatter link has a row and no node)
159 - bare fields = doc_id (the SOURCE doc), block_id (the source block; null
160 for a frontmatter edge), via_id, dst_id (the destination
161 node id: a doc id, an external node id or phantom:<path>),
162 dst_path (target doc path, /-rooted; NULL when
163 dangling/external), uri (external URL; NULL otherwise),
164 predicate ("references"/"embeds"/freeform), provenance
165 ("link"/"frontmatter"/"inline_field"/…), dst_kind
166 ("document"/"external"/"collection"), anchor, field (the
167 frontmatter key / inline field it was authored in).
168 - attributes = id, path (the SOURCE doc's path); doc.<key> reaches the
169 source document. There is no from_commit: a query sees
170 open edges only (history is blocks_history/changes_since).
171 - text()/semantic() are N/A on links.
172
173## Scoping (ADR-015; the root row since surface 2.0): bare names are LOCAL
174
175 name the CURRENT row only — never falls through to an enclosing row
176 or the repository; an absent field is simply absent.
177 ^name one scope OUT (the enclosing row's fields/intrinsics/lifts);
178 ^^name two scopes out. `^path` = the enclosing row's path.
179 ^docs ^nodes ^blocks ^links
180 THE ROOT ROW (surface 2.0, oqx 0.18): the repository is the root
181 scope's row, one scope out from a top-level row, so ^docs there is
182 the whole documents scan (uncorrelated until you add a ^ predicate);
183 one caret per enclosing block (^^docs from depth two), or the
184 absolute 0^docs from ANY depth. ^id / 0^id = the repository id.
185 ^$it = the root object: ^$it.docs ≡ ^docs, entries(^$it) names the
186 four collections. At the root scope itself a bare target is the scan
187 (docs count { … }, from docs, entries(docs)).
188 ERRORS: a bare `docs` INSIDE a block reads a property of the current
189 row, which has none — refused ("did you mean ^docs"); `^docs` AT THE
190 TOP LEVEL reaches past the root — refused (write the bare `docs`);
191 so is `$repo` (surface < 2.0; the message names the replacement).
192 nodes/blocks on a doc row are that doc's RELATIONS, not the root.
193 $it the current item itself (a row, or each ELEMENT when the
194 receiver is a list property); ^$it = the enclosing row.
195 $value is NOT an intrinsic (since oqx 0.15): it reads a
196 property literally named $value, absent when there is none.
197 $key the row's identity (0.20): a store row's id (doc/block/node/edge), an entries(x)
198 entry's key, a follow occurrence's walk identity; project $key: <expr> to rekey the
199 rows a block yields (a destination block's $key: is not a column).
200
201## Scalar semantics (@omgbase/oqx)
202
203 comparisons == != < <= > >= strict & typed (5 == "5" is false); either
204 side may be a field (field-vs-field OK).
205 absence null and a missing field are the same "absent" value; two absents
206 are EQUAL, so `absent != "x"` is TRUE and `absent == null` is TRUE.
207 Relational (< <= > >=) on an absent operand → FALSE. Bare `f` in
208 boolean position uses JS truthiness (absent/false/0/"" ⇒ false;
209 NOTE: "false", [] and {} are truthy). has(f) = explicit presence.
210 boolean and or not grouping ( … ) — && || ! are accepted synonyms
211 arithmetic + - * / % (+ concatenates if either side is a string)
212 membership x in y array → typed membership; string → substring;
213 object → key exists; range → coverage. Any list
214 property works directly: "pricing" in tags. list(f)
215 coerces scalar-or-list (absent → []) — use it when a
216 key is sometimes scalar, sometimes a list.
217 ranges lo..hi (inclusive) lo...hi (exclusive hi) ..hi lo..
218 era in 800..1680; works over ISO dates. range(prop) reads a
219 range-shaped STRING property as an interval: "2026-01-15" in range(window).
220 strings x.contains("s") x.startsWith("p") x.endsWith("s") x.matches("^re$")
221 (real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE —
222 fold with .lower(): title.lower().contains("aurora").
223 functions has(f) size(x) list(x) entries(x) range(s)
224 literals "str" 'str' 123 1.5 true false null
225 determinism no now()/clock/random. Lists/structs are not literals; ternary is
226 not supported. Errors surface as filter_invalid.
227 scalars vs lists a list-authored key never == a scalar (tags == "x" is false
228 when tags is a list) — use `"x" in tags` / `in list(tags)`.
229
230## Domain predicates (omgbase)
231
232 text("terms") FTS5 PRUNING predicate (docs match via their blocks, nodes
233 via the node index); composes anywhere, incl. nested scopes.
234 semantic("phrase") embedding cosine SCORE (docs/blocks): threshold it
235 (semantic("x") > 0.6), project it, or `order by semantic("x")
236 desc` for top-K. Needs a provider that has embedded the repository; else provider_unavailable.
237 blocks target only:
238 under(id_or_heading) block in the containment subtree / section range
239 under_heading("Launch") an ancestor section heading contains the text (ci)
240 under_kind(type[, name]) an ancestor block has that type (name: its text
241 contains / its key equals name)
242 within("path" | "d_..") containing doc = id, exact path, or path glob (*);
243 either path form
244 yaml_path("a.b") YAML docs: the block at that key path
245 json_pointer("#/a/b") JSON docs: the block at that pointer
246 has_edge("pred"[, target]) an open authored edge with that predicate leaves here
247 has_anchor() block carries an authored ^ref
248 parent_type() == "x" parent block's type (must be compared)
249 child_count() > 0 number of direct children (must be compared)
250 refs(x) the live documents a property's document references name:
251 x is a string, a list or absent; each "/a/b.md", "a/b.md"
252 or "d_…" element resolves to that doc's row; dangling and
253 non-string elements are dropped. Yields docs rows, so it is
254 a source, a receiver or a follow destination:
255 follow refs(before), refs(after) walks a timeline both ways.
256 The reverse direction needs no function: a document's
257 path is already the reference form, so
258 ^docs { where ^path in list(after) } is
259 "the documents whose `after` names me".
260 A HIT IS A STORE ROW: a top-level row that is not a doc/block/node/edge fails
261 (filter_invalid). `follow before` over a list of paths
262 reaches the STRINGS — write `follow refs(before)`.
263
264## Nested queries & consumers
265
266 <receiver> collect|exists|none|count|first|single { <block> }
267 receiver = a relation (nodes, blocks, doc.out_links, section.blocks, …), a
268 reach-through row (dst, doc: one row — dst { title }), a
269 list property (tags), entries(x), or a root collection (^docs; 0^docs at depth ≥ 2).
270 block = [select …] [where …] [follow …] [order by …] [limit N] [offset N]
271 — the same fixed order as the top level (`from` optional; the
272 receiver supplies the rows). A leading name/alias list is the
273 projection (`select` dropped); a predicate always needs `where`.
274 exists ≥1 row nodes exists { where kind == "md:task" and not checked }
275 none 0 rows nodes none { where kind == "md:task" and not checked } ("every task done")
276 count a number nodes count { where kind == "md:task" } >= 2
277 collect rows (default) — in select for nested results; in where with a lift
278 first / single zero-or-one (single errors on >1) — lookups in select
279 distinct on any consumer: dedup by projected value — nodes count distinct { select kind }
280 limit/offset bound the block's rows AFTER where/order/distinct, BEFORE the
281 consumer reduces: nodes exists { offset 1 } = at least two;
282 first { … offset 1 } = the second.
283 lift `^name:` in a where-collect binds values outward:
284 select path, open from docs where nodes collect { ^open: value where kind == "md:task" and not checked }
285 joins ^<t> exists { where slug == ^ref } semi-join; not … exists anti-join;
286 ^nodes single { where kind == "person" and attrs.id == ^owner_id } lookup;
287 x in ^keys membership over a lifted set.
288
289## select — projection
290
291 select a, alias: expr, nested: nodes collect { … }
292 bare key → the attribute when it is one (id, path, title, …), else the property
293 (scalar-authored → scalar; list → array); data.<k> forces the property;
294 a call/arithmetic needs an alias (n: size(tags)) unless in values mode;
295 an output key of id/path other than the attribute itself needs an alias.
296 raw whole file bytes (docs) — docs_read is cheaper for one doc.
297 distinct select distinct type (dedup hits by projected value)
298 values select <ONE item> values → bare values instead of records:
299 from docs select distinct type values → values: ["hub","lab-note",…]
300 select path values from docs → values: ["/index.md", …]
301 tags: tags collect { $it values } → a plain array
302 h: nodes first { name values where kind == "md:section" order by first_ordinal }
303 entries(x) a record as a collection ($key / $it per entry), key order:
304 select fm: entries(data) collect { k: $key, v: $it }
305 where entries(frontmatter) exists { where $key == "era" and $it > 1600 }
306 entries(inline), entries(attrs), a nested map, or a list (index keys)
307 $semantic_score is not a field — project semantic("…") under an alias instead.
308
309## order by, limit/offset, pagination
310
311 order by era desc, path asc absent values sort LAST (asc); ties → (path, id)
312 limit N / offset N define the result SET (integer literals);
313 the tool's limit/cursor then page WITHIN it
314 cursor opaque keyset on (path,id); a custom order by
315 disables it (you get the top page + truncated)
316
317## Graph traversal (follow) and links
318
319 The citation graph is the LINK ROWS walked through a destination block:
320 dst is a link's destination document (absent for an external or dangling
321 link — it then contributes no successor), doc its source.
322 from docs where <seed> follow out_links collect { from dst } docs the seed links TO
323 from docs where <seed> follow in_links collect { from doc } backlinks
324 follow out_links collect { from dst depth 3 } bound (1..8, default 8) — a clause of the block
325 follow out_links collect { from dst where layer == "canon" } keep only matching successors
326 follow in_links collect { from doc where ^path in list(before) } ^ = the row being expanded
327 (^^ = the walk's enclosing scope)
328 follow out_links collect { from dst where predicate == "depends_on" } one predicate only
329 follow out_links collect { from dst }, in_links collect { from doc }
330 several destinations: their union
331 (one step dedups by identity)
332 follow ^docs { where out_links exists { where dst_path == ^^path } }
333 a destination block over the root's
334 documents, re-read per frontier row
335 (^ = that row) = the backlinks walk
336 follow out_links collect { from dst frontier type == "practitioner" } cut, keeping the frontier row
337 from docs where type == "process" frontier !verified follow out_links collect { from dst }
338 the BODY's frontier applies to the seeds:
339 an unverified seed is kept, not expanded
340 follow distinct out_links collect { from dst } / … { $key: group from dst } identity control ($key)
341 follow refs(before), ^docs { where ^path in list(after) } forward over `before`, backward
342 over every doc whose `after` names me
343 Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|
344 depth|cycle), $leaf/$frontier, $ordinal, $key — usable in select/order by and
345 the top-level post-walk where, NOT metadata of the candidates a destination
346 block's where reads.
347 Destination blocks (OQX 0.20): a brace after ANY destination opens that
348 destination's block — children { depth 2 }, refs(x) { depth 2 }, out_links
349 collect { from dst depth 2 }, ^docs { where ^path in list(after) } — with the
350 0.17 body rules (where-first, projections) and two clauses of its own after
351 `where`: frontier <pred> (occurrences reached through this destination that
352 match are frontiers; ^ there is the walk's enclosing scope) and depth <n>
353 (this destination expands a frontier row only while $depth < n). The body
354 that carries the follow may write frontier <pred> too, after its where and
355 before follow: it applies to the SEEDS (read like the body's where) — a
356 matching seed stays in the result with $stop == "frontier" and is not
357 expanded; each destination's frontier is its own. There is no options block;
358 the old `by` is $key. A bare follow ^docs (no brace, no consumer) is an
359 error naming both block spellings.
360 Type-preserving relations: block.children, section.children, section.subsections;
361 the citation graph is out_links collect { from dst } / in_links collect { from doc }.
362 from links where dst_path == "/notes/x.md" inbound links to a doc, as rows
363 select d: dst.title, k: dst_kind from links the destination document's title
364 from docs where out_links exists { where dst.layer == "draft" } links into drafts
365 select b: in_links collect distinct { from doc } from docs backlinks as { id, path }
366 from links where predicate == "depends_on" select doc_id, dst_path
367 from docs where not doc.in_links exists { } orphans (nothing links in)
368 (The `graph` tool is a convenience wrapper that compiles to a follow query.)
369
370## semantic grain
371
372from docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.
373text()/where prune the candidate set; only order by reweights.
374
375## Examples
376
377 from docs where layer == "working"
378 from docs where path.startsWith("/guides/") and updated_at >= "2026-08-01"
379 from docs where "pricing" in list(tags) select layer, tags
380 from docs where inline.owner == "alice"
381 from docs where title.lower().contains("q3 plan")
382 from docs where format == "yaml"
383 from docs where type == "practitioner" order by era desc limit 2
384 from docs where nodes none { where kind == "md:task" and not checked }
385 from docs where tags exists { where $it == "tria-prima" }
386 from docs where path == "/x.md" select fm: entries(data) collect { k: $key, v: $it }
387 from docs select owner_id, owner: ^nodes single { where kind == "person" and attrs.id == ^owner_id }
388 from blocks where type == "task" and not checked and under_heading("Launch") and doc.layer == "working"
389 from blocks where type == "paragraph" and has_edge("references", "d_92aaaaa")
390 select path, $depth from docs where path == "/timeline/review.md" follow refs(before), refs(after)
391 select path, next: ^docs collect { path where ^path in list(before) } from docs where type == "milestone"
392 select path, n: size(^docs), repo: ^id from docs limit 1
393 select p: data.path, t: data.title from docs where has(data.path)
394 docs { type == "substance" and not verified }
395 select subject, process: ^docs[slug == ^subject]!.path from docs where type == "lab-note"
396 select path, back: nodes first { select p: 0^docs collect { path where ^^path in list(after) } values } from docs where type == "milestone"
397 select path, prior: refs(before) collect { path, when } from docs where type == "milestone"
398 from blocks order by semantic("identity preservation across edits") desc limit 10
399 from links where dst_kind == "external" select uri
400 docs count { where layer == "canon" }
401
402Common mistake: `$path`/`$id` (the 2.x spelling) — omgbase has no $ names; write
403`path`/`id`. Another: `where tags == "x"` on a list-valued key is false — write
404`"x" in tags`. Another: `select data.path` — alias it (`p: data.path`).
405"##;