@omgbase/core 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/ids.d.ts +1 -1
- package/dist/src/core/read/document.d.ts +4 -1
- package/dist/src/core/read/document.js +18 -6
- package/dist/src/core/store/archive.d.ts +196 -0
- package/dist/src/core/store/archive.js +606 -0
- package/dist/src/core/store/gc.d.ts +25 -4
- package/dist/src/core/store/gc.js +43 -28
- package/dist/src/core/store/rebuild.d.ts +2 -0
- package/dist/src/core/store/rebuild.js +8 -2
- package/dist/src/core/store/retention.d.ts +138 -0
- package/dist/src/core/store/retention.js +409 -0
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +30 -4
- package/dist/src/core/store/size.d.ts +21 -2
- package/dist/src/core/store/size.js +24 -3
- package/dist/src/core/store/store.d.ts +1 -0
- package/dist/src/core/store/store.js +53 -1
- package/dist/src/graph/history.d.ts +2 -0
- package/dist/src/graph/history.js +8 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +4 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +24 -6
- package/dist/src/mcp/server.js +2 -2
- package/dist/src/oqx-js/context.js +21 -5
- package/dist/src/oqx-js/dates.d.ts +105 -0
- package/dist/src/oqx-js/dates.js +427 -0
- package/dist/src/oqx-js/names.js +14 -3
- package/dist/src/oqx-js/planner.js +17 -2
- package/dist/src/oqx-js/run.d.ts +1 -0
- package/dist/src/oqx-js/run.js +66 -1
- package/dist/src/spec-versions.d.ts +4 -4
- package/dist/src/spec-versions.js +3 -3
- package/package.json +2 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
+
import { archivedSummary } from "./archive.js";
|
|
3
|
+
import { evaluateRetention, retentionPolicy } from "./retention.js";
|
|
2
4
|
// The size report (spec/store §7 "Size", 14.1): what the database is made of,
|
|
3
5
|
// table by table through SQLite's `dbstat` virtual table, grouped into the four
|
|
4
6
|
// categories a reader reasons about — the live state (`content`), the
|
|
@@ -30,6 +32,7 @@ export const TABLE_CATEGORY = {
|
|
|
30
32
|
block_changes: "history",
|
|
31
33
|
resurrection_pool: "history",
|
|
32
34
|
pending_materializations: "history",
|
|
35
|
+
archives: "history",
|
|
33
36
|
blocks_fts: "search",
|
|
34
37
|
nodes_fts: "search",
|
|
35
38
|
embeddings: "search",
|
|
@@ -53,6 +56,8 @@ export const VACUUM_ADVICE_MIN_SHARE = 10;
|
|
|
53
56
|
export const HISTORY_ADVICE_MIN_SHARE = 50;
|
|
54
57
|
export const SAME_ADVICE_MIN_SHARE = 25;
|
|
55
58
|
export const EMBEDDINGS_ADVICE_MIN_SHARE = 25;
|
|
59
|
+
/** The retention advice fires when the plan's bytes exceed this share of the `history` category (15.0). */
|
|
60
|
+
export const RETENTION_ADVICE_MIN_SHARE = 10;
|
|
56
61
|
/**
|
|
57
62
|
* `<n> B` under 1024, else one decimal of KB/MB/GB/TB (1024-based), the tenths
|
|
58
63
|
* rounded half up — `4096` → `4.0 KB`, `13734297` → `13.1 MB`. Both engines
|
|
@@ -83,7 +88,7 @@ export function shareOf(bytes, total) {
|
|
|
83
88
|
return Math.round((bytes * 1000) / total) / 10;
|
|
84
89
|
}
|
|
85
90
|
/** spec/store §7: the advice lines, in this order; `nothing to reclaim` when none applies. */
|
|
86
|
-
export function sizeAdvice(file, categories, sameDispositions = 0) {
|
|
91
|
+
export function sizeAdvice(file, categories, sameDispositions = 0, retention = { revisions: 0, bytes: 0 }) {
|
|
87
92
|
const advice = [];
|
|
88
93
|
const logical = file.page_count * file.page_size;
|
|
89
94
|
if (file.reclaimable > VACUUM_ADVICE_MIN_BYTES && file.reclaimable * 100 > logical * VACUUM_ADVICE_MIN_SHARE) {
|
|
@@ -100,6 +105,12 @@ export function sizeAdvice(file, categories, sameDispositions = 0) {
|
|
|
100
105
|
if (sameDispositions > 0 && sameDispositions * 100 > dispositionRows * SAME_ADVICE_MIN_SHARE) {
|
|
101
106
|
advice.push(`${sameDispositions} same dispositions are history nothing reads (store 14.2): \`omg gc\` removes them, \`--vacuum\` reclaims the pages`);
|
|
102
107
|
}
|
|
108
|
+
// 15.0: what the retention policy would move to a segment, when it is worth
|
|
109
|
+
// a run — more than a tenth of the history category's pages (dbstat only:
|
|
110
|
+
// the comparison needs the bytes).
|
|
111
|
+
if (retention.revisions > 0 && history && history.bytes !== null && retention.bytes * 100 > history.bytes * RETENTION_ADVICE_MIN_SHARE) {
|
|
112
|
+
advice.push(`${retention.revisions} revisions older than the policy would archive ${humanBytes(retention.bytes)}: \`omg history export --prune\``);
|
|
113
|
+
}
|
|
103
114
|
const search = categories.find((c) => c.name === "search");
|
|
104
115
|
if (search && search.bytes !== null) {
|
|
105
116
|
const total = categories.reduce((n, c) => n + (c.bytes ?? 0), 0);
|
|
@@ -124,7 +135,7 @@ function dbstatBytes(store) {
|
|
|
124
135
|
}
|
|
125
136
|
}
|
|
126
137
|
/** The size report of an open store (spec/store §7 "Size"). */
|
|
127
|
-
export function sizeReport(store) {
|
|
138
|
+
export function sizeReport(store, opts = {}) {
|
|
128
139
|
const db = store.db;
|
|
129
140
|
const path = db.name === "" ? ":memory:" : db.name;
|
|
130
141
|
const page_size = db.pragma("page_size", { simple: true });
|
|
@@ -208,7 +219,17 @@ export function sizeReport(store) {
|
|
|
208
219
|
return { name, bytes, share: bytes === null ? null : shareOf(bytes, total), rows: tables.reduce((n, t) => n + t.rows, 0), tables };
|
|
209
220
|
});
|
|
210
221
|
const same_dispositions = db.prepare("SELECT count(*) AS c FROM dispositions WHERE kind = 'same'").get().c;
|
|
211
|
-
const
|
|
222
|
+
const archived = archivedSummary(store);
|
|
223
|
+
// The retention plan of every repository under its effective policy (the
|
|
224
|
+
// report is store-wide; the policy is a repository's) at one clock.
|
|
225
|
+
const now = opts.now ?? new Date().toISOString();
|
|
226
|
+
const retention = { revisions: 0, bytes: 0 };
|
|
227
|
+
for (const r of db.prepare("SELECT repo_id FROM repos ORDER BY repo_id").all()) {
|
|
228
|
+
const plan = evaluateRetention(store, r.repo_id, { now, policy: retentionPolicy(store, r.repo_id) });
|
|
229
|
+
retention.revisions += plan.totals.archive;
|
|
230
|
+
retention.bytes += plan.totals.bytes;
|
|
231
|
+
}
|
|
232
|
+
const report = { file, categories, same_dispositions, archived, advice: sizeAdvice(file, categories, same_dispositions, retention) };
|
|
212
233
|
if (!stat)
|
|
213
234
|
report.detail = "dbstat unavailable";
|
|
214
235
|
return report;
|
|
@@ -18,6 +18,7 @@ export declare class Store {
|
|
|
18
18
|
private migrateV9;
|
|
19
19
|
private migrateV11;
|
|
20
20
|
private migrateV13;
|
|
21
|
+
private migrateV15;
|
|
21
22
|
/**
|
|
22
23
|
* Run fn inside a single write transaction (02 §2: one transaction per
|
|
23
24
|
* commit). better-sqlite3 transactions are synchronous and serialized.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
-
import { DDL, SCHEMA_VERSION, MIGRATIONS, SYNC_DDL } from "./schema.js";
|
|
4
|
+
import { DDL, SCHEMA_VERSION, MIGRATIONS, SYNC_DDL, ARCHIVES_DDL } from "./schema.js";
|
|
5
5
|
import { cosineBytes } from "../vec.js";
|
|
6
6
|
import { mintId, registerIdOracle } from "../ids.js";
|
|
7
7
|
// spec/store §2.1 "Uniqueness at mint": the table(s) and id column a minted
|
|
@@ -18,6 +18,7 @@ const ID_TABLES = {
|
|
|
18
18
|
cp: [["checkpoints", "id"]],
|
|
19
19
|
rp: [["repos", "repo_id"]],
|
|
20
20
|
src: [["sources", "source_id"]],
|
|
21
|
+
s: [["archives", "segment_id"]],
|
|
21
22
|
};
|
|
22
23
|
export class Store {
|
|
23
24
|
db;
|
|
@@ -97,6 +98,10 @@ export class Store {
|
|
|
97
98
|
this.migrateV13();
|
|
98
99
|
continue;
|
|
99
100
|
}
|
|
101
|
+
if (v === 15) {
|
|
102
|
+
this.migrateV15();
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
100
105
|
const ddl = MIGRATIONS[v];
|
|
101
106
|
if (!ddl)
|
|
102
107
|
throw new Error(`no migration to schema v${v}`);
|
|
@@ -184,6 +189,53 @@ export class Store {
|
|
|
184
189
|
this.db.exec("ALTER TABLE repos DROP COLUMN root_path");
|
|
185
190
|
})();
|
|
186
191
|
}
|
|
192
|
+
// v15 (spec/store §3.4 step 15, §7 "Retention"): `revisions.root_tree` and
|
|
193
|
+
// `frontmatter_blob` become nullable and lose their foreign keys (an archived
|
|
194
|
+
// revision keeps its row and loses its bodies), `revisions` gains `archive`,
|
|
195
|
+
// and `archives` is created. SQLite cannot relax a NOT NULL or drop a FK in
|
|
196
|
+
// place, so the table is rebuilt the documented way (sqlite.org/lang_altertable,
|
|
197
|
+
// "making other kinds of table schema changes"): foreign keys off for the
|
|
198
|
+
// duration — `pending_materializations.rev_id` references `revisions`, and the
|
|
199
|
+
// DROP would otherwise be an implicit DELETE under the FK — create the new
|
|
200
|
+
// table, copy every row with `archive = NULL`, drop the old, rename, check.
|
|
201
|
+
// Idempotent: a `revisions` table that already carries `archive` is left alone
|
|
202
|
+
// (a fresh database never reaches here); `archives` is `CREATE … IF NOT EXISTS`.
|
|
203
|
+
migrateV15() {
|
|
204
|
+
const cols = this.db.pragma("table_info(revisions)");
|
|
205
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "archive")) {
|
|
206
|
+
// PRAGMA foreign_keys is a no-op inside a transaction, so it brackets one.
|
|
207
|
+
this.db.pragma("foreign_keys = OFF");
|
|
208
|
+
try {
|
|
209
|
+
this.db.transaction(() => {
|
|
210
|
+
this.db.exec(`
|
|
211
|
+
CREATE TABLE revisions_v15 (
|
|
212
|
+
rev_id TEXT PRIMARY KEY,
|
|
213
|
+
doc_id TEXT NOT NULL REFERENCES docs(doc_id),
|
|
214
|
+
seq INTEGER NOT NULL,
|
|
215
|
+
root_tree BLOB,
|
|
216
|
+
frontmatter_blob BLOB,
|
|
217
|
+
rendered_hash BLOB NOT NULL,
|
|
218
|
+
path TEXT NOT NULL,
|
|
219
|
+
commit_id TEXT NOT NULL REFERENCES commits(commit_id),
|
|
220
|
+
archive TEXT,
|
|
221
|
+
UNIQUE (doc_id, seq)
|
|
222
|
+
);
|
|
223
|
+
INSERT INTO revisions_v15 (rev_id, doc_id, seq, root_tree, frontmatter_blob, rendered_hash, path, commit_id, archive)
|
|
224
|
+
SELECT rev_id, doc_id, seq, root_tree, frontmatter_blob, rendered_hash, path, commit_id, NULL FROM revisions;
|
|
225
|
+
DROP TABLE revisions;
|
|
226
|
+
ALTER TABLE revisions_v15 RENAME TO revisions;
|
|
227
|
+
`);
|
|
228
|
+
const violations = this.db.pragma("foreign_key_check");
|
|
229
|
+
if (violations.length > 0)
|
|
230
|
+
throw new Error(`migration v15: ${violations.length} foreign key violation(s) after rebuilding revisions`);
|
|
231
|
+
})();
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.db.pragma("foreign_keys = ON");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
this.db.exec(ARCHIVES_DDL);
|
|
238
|
+
}
|
|
187
239
|
/**
|
|
188
240
|
* Run fn inside a single write transaction (02 §2: one transaction per
|
|
189
241
|
* commit). better-sqlite3 transactions are synchronous and serialized.
|
|
@@ -137,6 +137,8 @@ export interface DocVersion {
|
|
|
137
137
|
hash: string;
|
|
138
138
|
/** true when this rev == the doc's current_rev. */
|
|
139
139
|
isCurrent: boolean;
|
|
140
|
+
/** true when the revision's bodies live in an archive segment (spec/store §7 "Retention", 15.0; `docs_read { at }` reads through it when attached). */
|
|
141
|
+
archived: boolean;
|
|
140
142
|
}
|
|
141
143
|
/** A matching document with its ordered version history. */
|
|
142
144
|
export interface DocVersions {
|
|
@@ -3,6 +3,7 @@ import { readDocumentAtRevision } from "../core/read/document.js";
|
|
|
3
3
|
import { isValidId } from "../core/ids.js";
|
|
4
4
|
import { EngineError } from "../mcp/errors.js";
|
|
5
5
|
import { referencePath, storagePath } from "../core/paths.js";
|
|
6
|
+
import { revisionBody } from "../core/store/archive.js";
|
|
6
7
|
/** blocks_history: a block's biography from block_changes + commits (06 §3). */
|
|
7
8
|
export function historyNode(store, blockId, opts = {}) {
|
|
8
9
|
const limit = opts.limit ?? 100;
|
|
@@ -56,12 +57,14 @@ export function diffBlocks(store, docId, fromRev, toRev) {
|
|
|
56
57
|
// of the document: a container's raw already holds its children's text, so
|
|
57
58
|
// joining the values would repeat every nested block (spec/surface §9).
|
|
58
59
|
function blocksAtRevision(store, docId, revId) {
|
|
59
|
-
|
|
60
|
+
// An archived revision's bodies come from its segment (spec/store §7, 15.0;
|
|
61
|
+
// `prefix` is the attached alias — RevisionArchived when unavailable).
|
|
62
|
+
const rev = revisionBody(store, revId, docId);
|
|
60
63
|
if (!rev)
|
|
61
64
|
throw new RevisionNotFound(docId, revId);
|
|
62
65
|
const out = new Map();
|
|
63
|
-
const blob = store.db.prepare(
|
|
64
|
-
const treeNode = store.db.prepare(
|
|
66
|
+
const blob = store.db.prepare(`SELECT bytes FROM ${rev.prefix}blobs WHERE hash = ?`);
|
|
67
|
+
const treeNode = store.db.prepare(`SELECT entries FROM ${rev.prefix}tree_nodes WHERE hash = ?`);
|
|
65
68
|
const walk = (treeHashHex) => {
|
|
66
69
|
const node = treeNode.get(Buffer.from(treeHashHex, "hex"));
|
|
67
70
|
if (!node)
|
|
@@ -346,7 +349,7 @@ export function docHistory(store, repoId, opts = {}) {
|
|
|
346
349
|
const truncated = docRows.length > limit;
|
|
347
350
|
const page = docRows.slice(0, limit);
|
|
348
351
|
const revStmt = store.db.prepare(`SELECT r.rev_id AS rev, r.seq AS seq, r.commit_id AS commitId, r.rendered_hash AS rendered_hash,
|
|
349
|
-
c.ts AS ts, c.origin AS origin, c.actor AS actor
|
|
352
|
+
c.ts AS ts, c.origin AS origin, c.actor AS actor, r.archive AS archive
|
|
350
353
|
FROM revisions r JOIN commits c ON c.commit_id = r.commit_id
|
|
351
354
|
WHERE r.doc_id = ? ORDER BY r.seq ASC`);
|
|
352
355
|
const docs = page.map((d) => {
|
|
@@ -360,6 +363,7 @@ export function docHistory(store, repoId, opts = {}) {
|
|
|
360
363
|
actor: r.actor,
|
|
361
364
|
hash: r.rendered_hash.toString("hex"),
|
|
362
365
|
isCurrent: r.rev === d.current_rev,
|
|
366
|
+
archived: r.archive !== null,
|
|
363
367
|
}));
|
|
364
368
|
return { id: d.doc_id, path: referencePath(d.path), deleted: d.deleted_commit !== null, currentRev: d.current_rev, versions };
|
|
365
369
|
});
|
package/dist/src/index.d.ts
CHANGED
|
@@ -74,5 +74,7 @@ export { planUpdate, type PlanUpdateOptions } from "./mutate/plan-update.js";
|
|
|
74
74
|
export { opsetKernelOps, renderOpsetPlan, renderIdentityPlan, type IdentityPreview, serializeOpset, parseOpset, type Opset, type PlanOp, type OpsetSummary, type OpsetPrecondition, type PlanDisposition, } from "./mutate/opset.js";
|
|
75
75
|
export { rebuildIndex, type RebuildTarget } from "./core/store/rebuild.js";
|
|
76
76
|
export { runGc, type GcResult } from "./core/store/gc.js";
|
|
77
|
-
export {
|
|
77
|
+
export { evaluateRetention, retentionPolicy, mergeRetentionPolicy, policyFor, globToRegExp, ageDays, periodKey, DEFAULT_RETENTION, LADDER_KEEPS, RetentionPolicyError, type RetentionPolicy, type RetentionOverride, type LadderRung, type LadderKeep, type RetentionPlan, type RetentionDocPlan, type RetentionTotals, type EvaluateOptions, } from "./core/store/retention.js";
|
|
78
|
+
export { exportHistory, attachArchive, detachArchive, listArchives, unrecordedSegments, readManifest, revisionBody, archivePrefix, archivedSummary, segmentFileName, parseSegmentFileName, RevisionArchived, NotASegment, StaleSegment, ARCHIVE_DDL, ARCHIVE_SUFFIX, ARCHIVE_DIR, type ExportOptions, type ExportResult, type SegmentInfo, type SquashResult, type AttachResult, type AttachedSegment, type ArchiveListing, type ArchiveState, type UnrecordedSegment, type Manifest, type RevisionBody, } from "./core/store/archive.js";
|
|
79
|
+
export { sizeReport, sizeAdvice, vacuum, reclaimableBytes, humanBytes, formatShare, shareOf, categoryOf, TABLE_CATEGORY, SIZE_CATEGORIES, type SizeReport, type SizeCategory, type SizeCategoryReport, type SizeTable, type SizeFile, type SizeArchived, type SizeRetention, type VacuumResult, } from "./core/store/size.js";
|
|
78
80
|
export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
|
package/dist/src/index.js
CHANGED
|
@@ -88,6 +88,9 @@ export { planUpdate } from "./mutate/plan-update.js";
|
|
|
88
88
|
export { opsetKernelOps, renderOpsetPlan, renderIdentityPlan, serializeOpset, parseOpset, } from "./mutate/opset.js";
|
|
89
89
|
export { rebuildIndex } from "./core/store/rebuild.js";
|
|
90
90
|
export { runGc } from "./core/store/gc.js";
|
|
91
|
+
// History retention (spec/store §7 "Retention", 15.0): the policy and plan, the archive segments.
|
|
92
|
+
export { evaluateRetention, retentionPolicy, mergeRetentionPolicy, policyFor, globToRegExp, ageDays, periodKey, DEFAULT_RETENTION, LADDER_KEEPS, RetentionPolicyError, } from "./core/store/retention.js";
|
|
93
|
+
export { exportHistory, attachArchive, detachArchive, listArchives, unrecordedSegments, readManifest, revisionBody, archivePrefix, archivedSummary, segmentFileName, parseSegmentFileName, RevisionArchived, NotASegment, StaleSegment, ARCHIVE_DDL, ARCHIVE_SUFFIX, ARCHIVE_DIR, } from "./core/store/archive.js";
|
|
91
94
|
export { sizeReport, sizeAdvice, vacuum, reclaimableBytes, humanBytes, formatShare, shareOf, categoryOf, TABLE_CATEGORY, SIZE_CATEGORIES, } from "./core/store/size.js";
|
|
92
95
|
export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
|
|
93
96
|
//# sourceMappingURL=index.js.map
|
package/dist/src/mcp/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ErrorCode = "stale_expectation" | "parent_missing" | "target_missing" | "block_missing" | "doc_missing" | "cycle_move" | "opaque_block" | "not_contiguous" | "type_mismatch" | "conflicted_document" | "path_taken" | "create_conflict" | "ambiguous_locator" | "ambiguous_heading" | "filter_invalid" | "budget_exceeded" | "provider_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved" | "block_not_container" | "stale_plan" | "plan_not_convergent" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "ambiguous_target" | "selection_too_large";
|
|
1
|
+
export type ErrorCode = "stale_expectation" | "parent_missing" | "target_missing" | "block_missing" | "doc_missing" | "cycle_move" | "opaque_block" | "not_contiguous" | "type_mismatch" | "conflicted_document" | "path_taken" | "create_conflict" | "ambiguous_locator" | "ambiguous_heading" | "filter_invalid" | "budget_exceeded" | "provider_unavailable" | "embedder_failed" | "sync_conflict" | "repo_not_found" | "node_not_editable" | "seed_unresolved" | "block_not_container" | "stale_plan" | "plan_not_convergent" | "conflicting_effects" | "unsupported_plan_version" | "plan_mismatch" | "ambiguous_target" | "selection_too_large" | "revision_archived";
|
|
2
2
|
export interface EngineErrorBody {
|
|
3
3
|
error: ErrorCode;
|
|
4
4
|
message: string;
|
package/dist/src/mcp/errors.js
CHANGED
|
@@ -2,6 +2,7 @@ import { FilterInvalid } from "../search/cel/parser.js";
|
|
|
2
2
|
import { CursorInvalid } from "../core/cursor.js";
|
|
3
3
|
import { MutationError } from "../mutate/tree.js";
|
|
4
4
|
import { RevisionNotFound } from "../graph/history.js";
|
|
5
|
+
import { RevisionArchived } from "../core/store/archive.js";
|
|
5
6
|
import { WriterLockTimeout } from "../sync/writer-lock.js";
|
|
6
7
|
export class EngineError extends Error {
|
|
7
8
|
code;
|
|
@@ -28,6 +29,9 @@ export function errorBody(err) {
|
|
|
28
29
|
return { error: err.code, message: err.message, data: err.data, retriable: Boolean(err.data.retriable) };
|
|
29
30
|
if (err instanceof RevisionNotFound)
|
|
30
31
|
return { error: "target_missing", message: err.message, data: { doc: err.docId, rev: err.rev }, retriable: false };
|
|
32
|
+
// spec/surface 4.9: `{ rev, archive, path? }` — `path` only when the segment is recorded (then it is the file that is not on disk).
|
|
33
|
+
if (err instanceof RevisionArchived)
|
|
34
|
+
return { error: "revision_archived", message: err.message, data: { rev: err.rev, archive: err.archive, ...(err.path !== null ? { path: err.path } : {}) }, retriable: false };
|
|
31
35
|
if (err instanceof WriterLockTimeout)
|
|
32
36
|
return { error: "repo_not_found", message: err.message, retriable: false };
|
|
33
37
|
return { error: "repo_not_found", message: String(err), retriable: false };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const QUERY_SYNTAX = "# query \u2014 OQX syntax reference\n\nThe `query` tool takes ONE plain OQX string (+ optional `limit`/`cursor`, and\n`bare: true` to drop the automatic `id`/`path` envelope \u2014 needs a `select`).\nThere is no {from, filter, order} envelope: every concern is a clause of the\nstring. Two rules tell the two syntaxes apart: DOT navigation belongs to the\nhost object model (`doc.layer`, `section.blocks`, `dst.title`); WHITESPACE\nquery directives belong to OQX \u2014 a nested query is `<receiver> collect|exists|\ncount|first|single { <block> }` (a postfix directive + a `{ \u2026 }` block), never a\nmethod. Its distinctive power is the receiver-constrained nested query that\ncorrelates to the current row: `from docs where nodes exists { where kind ==\n\"md:task\" }` returns only the docs that themselves contain a matching node.\n\n [select <items>] from <target> [where <pred>] [frontier <pred>] [follow <dest> [{ \u2026 }], \u2026]\n [order by <expr> [asc|desc], \u2026] [limit N] [offset N]\n <target> count|exists|none|first|single { <block> } (scalar/one-row form: a bare target at the root scope)\n\nCLAUSE ORDER IS FIXED: select, from, where, frontier, follow, order by, limit, offset \u2014\neach at most once; an out-of-order clause is a parse error naming the order.\nOnly `select` may drop its keyword, and only as the first clause\n(`path, era from docs where \u2026`). A top-level predicate needs `where`, because\n`where` follows `from` (`era > 1600 from docs` is the clause-order error); a\nblock may LEAD with a predicate (see Sugar), but a predicate after a projection\nstill needs the keyword (`{ name where kind == \"md:task\" }`, not\n`{ name, kind == \"md:task\" }`); `from docs count` is an error (write\n`docs count { \u2026 }`). `where` and later `select` items may use the same body's\n`select` aliases (`select path, old: era < 1000 from docs where old`; an alias\nshadows a same-named field there). `and` / `or` / `not` are the connectives;\nthe symbols `&&` / `||` / `!` are accepted synonyms (the words are canonical and\nwhat `print` writes).\n\n## Sugar (OQX 0.17)\n\nEvery form here is shorthand for an explicit directive (the AST is the explicit form):\n nodes { kind == \"md:task\" } \u2261 nodes collect { where kind == \"md:task\" } \u2014 a receiver block with\n no consumer is collect; a block whose LEADING expression is a predicate\n (comparison, call, !/is/not, in, literal, (\u2026), a consumer test \u2014 anything\n but a bare name, a dotted path or a ^lift) is where-first. Bare names still\n project: nodes { name } \u2261 nodes collect { name }; filter a bare field with\n nodes { is checked }. Wherever a value stands (0.20), call arguments included:\n size(children { is checked }); x distinct { \u2026 } \u2261 x collect distinct { \u2026 }. After a\n follow destination too: follow children { depth 2 } is that destination's block.\n (a, b) a tuple (0.20): two or more expressions, concatenated one level flat \u2014 a value\n (size((tags, aliases)), \"x\" in (tags, aliases)) and a receiver ((a, b) { \u2026 },\n follow (before, after) { depth 2 }); (a) alone is grouping\n refs(x)[0] \u2261 refs(x) first { offset 0 } positional (integer literal or ${binding});\n out of range \u21D2 absent; refs(x)[0].path navigates the row\n ^docs[path == ^company] \u2261 ^docs first { where path == ^company } first match or absent\n ^docs[path == ^company]! \u2261 \u2026 single { \u2026 } required: exactly one, else filter_invalid\n title! required: title, or an error naming the expression (and the row's $key \u2014 its id)\n \u2014 never a filter, never a coercion (0!, \"\"! are values); tightest: a!.b vs a.b!\n is x / not x truthiness of x, and its negation (prefix; `not` \u2261 `!`)\n x is y / x is not y identity ($key: a store row's id, else structural) \u2014 compares rows, which == does not;\n x is null = absent; for scalars is \u2261 ==; comparison precedence, no chaining\n a and b / a or b the connectives (\u2261 && / ||: same precedence, short-circuit, value: data.title or path coalesces)\n boss: ^docs[path == ^manager], bossName: boss.title a select item may use the items to its LEFT\n Reserved words (never a bare field name): from where select is not and or, true false null.\n Contextual words (limit order offset follow collect count first distinct desc by in frontier depth) parse as\n fields in expression position (select o: order from docs).\n\nResults are LEAN hits \u2014 { id, path } + whatever `select` projects (or `values`,\n`count`, `exists`, `none`). The envelope keys ARE the attributes: copy a hit's\n`id` straight into `where id == \"\u2026\"` (no translation); `select id, path` is simply\nthe envelope (no duplicate columns); any OTHER projection whose output key would\nbe `id` or `path` must be aliased \u2014 `select data.path` is refused (\"projection\n`path` collides with the hit envelope \u2014 alias it (`p: data.path`)\"); a row\nsurfacing as a value (an empty `collect { }`) is the same `{ id, path }`. Every\nother tool spells a row the same way (`docs_read` \u2192 `{ id, path, rev, properties,\nraw }`, `blocks_read` \u2192 `{ id, type, text, raw, hash, \u2026 }`, `docs_list` \u2192 `{ id,\npath, updated_at, blocks }`, a revision \u2192 `{ id, path, hash }`): hydrate by id via\nblocks_read/docs_read.\n\n## Paths\n\nEvery path the surface returns is `/`-rooted \u2014 the form a reference is\nwritten in (`[x](/projects/oqx.md)`, `before: [/timeline/kickoff.md]`): a\nhit's `path`, an edge's `dst_path`, every tool's paths. Every path it\naccepts tolerates both forms (`/a.md` or `a.md`): tool arguments, `refs(x)`,\n`within(...)`, and a string LITERAL compared with `path`/`dst_path` (bare, or\nthrough a row handle: `doc.path`) by `==`/`!=` or passed to their\n`.startsWith(...)` (it is rooted first, so `path == \"a.md\"` and `path == \"/a.md\"`\nboth match). A property holding a reference compares directly \u2014 and is never\nrooted: `where ^path in list(after)`, `where customer == ^path`,\n`where data.path == \"/x.md\"` (the author's `path:` key, as written) \u2014 no\n`\"/\" + ...` glue.\n\n## Targets & field namespaces\n\nTHE NAME RULE: `$` belongs to OQX \u2014 $it, $key, $depth, $stop, $leaf, $frontier,\n$ordinal are values the LANGUAGE injects into a scope; omgbase adds NO $ names\n(a `$id` in a query is an unknown name, absent). Every engine attribute is a BARE\nsnake_case name that WINS on collision \u2014 the convention the relations (children,\nblocks, out, in, doc, frontmatter, \u2026) always followed: a bare name that is an\nengine attribute reads the engine's value, never yours. Your data is bare too;\nwhere a key of yours collides with an engine name, reach it through a source\nhandle: `data.<k>` (the merged authored view \u2014 frontmatter + inline, exactly the\nread a bare non-colliding key performs), `frontmatter.<k>`, `inline.<k>`. So\n`path` is the engine's /-rooted location and `data.path` your `path:` key;\n`tags` is your tags \u222A the body #hashtags and `data.tags` the authored list alone;\n`title` the computed title (the first H1) and `data.title` your `title:`;\n`entries(data)` lists every authored top-level key. Same value, same name\neverywhere: a hit's `id` is docs_read's `id` and the `id` argument of blocks_read.\n\n Reserved bare names per kind (engine attributes; handles; self; relations):\n docs id path raw hash updated_at title tags format | data frontmatter inline | doc | blocks nodes out_links in_links revisions\n blocks id path doc_id raw hash updated_at type text attrs depth ordinal | block doc | children nodes out_links section\n nodes id path doc_id block_id kind name value attrs | section doc block | blocks subsections children\n links id path doc_id block_id via_id dst_id dst_path uri field dst_kind predicate provenance anchor | doc\n revisions id path rev seq doc_id commit ts origin actor reason hash is_current | doc\n A key of yours with any of these names is `data.<k>` (or frontmatter.<k> /\n inline.<k> by source; attrs.<k> on a block or node).\n UNKNOWN NAMES: a name that is none of the above AND that no object of the\n kind in this repository carries is reported before any row is read \u2014 on\n links as a filter_invalid error naming the fields; on docs/blocks/nodes the\n result gains `hints` (one line per name, nearest match when there is one)\n and the rows still come back; in a plan/apply/execute selector it is refused.\n A property some objects carry is known (partial presence is normal).\n\ndocs:\n - bare identifier = a document PROPERTY (frontmatter + inline `key:: value`,\n unioned; nested via dots: meta.owner). YAML/JSON docs\n expose the parsed object's keys the same way.\n - source handles = data.<k> (merged authored view) / frontmatter.<k> /\n inline.<k>; the whole bag as a collection via entries(data)\n / entries(frontmatter) / entries(inline) (see entries()).\n - attributes = id, path (/-rooted, see Paths), updated_at (ISO-8601,\n sorts chronologically), raw (the whole file, frontmatter\n included), hash (its sha256, 64 hex), title (the computed\n title: the first H1; data.title is the authored key), tags\n (authored tags \u222A body #hashtags, deduped, authored first;\n data.tags is the authored list), format (the doc's format \u2014\n \"markdown\"/\"yaml\"/\"json\"; present on EVERY doc, so\n has(format) is always true and `format == \"yaml\"` is the\n filter).\n - relations = nodes, blocks, doc.out_links / doc.in_links (a doc's\n links as rows; the documents at their other end are\n out_links collect distinct { from dst } and\n in_links collect distinct { from doc } \u2014 there is no\n out / in relation), revisions (the document's stored\n revisions, oldest first \u2014 see revisions below).\n\nblocks:\n - bare fields = doc_id (the owning doc), type, text (normalized), ordinal\n (0-based among siblings), depth (0 at the top level), and\n every attrs key FLATTENED onto the row (bare `checked`\n reads attrs.checked; `attrs.<k>` still works; the\n structural fields win on collision; absent \u21D2 false).\n - attributes = id, path (the owning doc's path), raw (the block's raw\n source \u2014 a container's raw holds its children), hash\n (its sha256 \u2014 the value update/split expect as\n expect.content_hash), updated_at.\n - reach-through = doc.<key> / doc.path / doc.id / doc.format = the\n CONTAINING doc (doc.id and doc.path cost nothing \u2014 they\n read the row's own doc_id / joined path, no lookup).\n - relations = block.children, block.nodes, block.out_links, section.\n NOTE: inside a `follow`, $depth/$ordinal are ALWAYS the walk's metadata; a\n block's own nesting is `depth` / `ordinal` (no $) everywhere.\n\nnodes:\n - bare fields = doc_id, block_id (null for a block-less node), kind\n (md:task / md:link / md:section / yaml:\u2026), name, value,\n + attrs keys flattened (checked, level).\n - attributes = id, path.\n - reach-through = doc.<key> / doc.id / doc.path, block.type / block.text\n / block.id / block.path.\n - relations = section.blocks, section.children, section.subsections.\n\nlinks: (the authored link graph as rows \u2014 one per open link: the relation a link\n establishes; the md:link NODE is its occurrence in the text and via_id\n points at it \u2014 a frontmatter link has a row and no node)\n - bare fields = doc_id (the SOURCE doc), block_id (the source block; null\n for a frontmatter edge), via_id, dst_id (the destination\n node id: a doc id, an external node id or phantom:<path>),\n dst_path (target doc path, /-rooted; NULL when\n dangling/external), uri (external URL; NULL otherwise),\n predicate (\"references\"/\"embeds\"/freeform), provenance\n (\"link\"/\"frontmatter\"/\"inline_field\"/\u2026), dst_kind\n (\"document\"/\"external\"/\"collection\"), anchor, field (the\n frontmatter key / inline field it was authored in).\n - attributes = id, path (the SOURCE doc's path); doc.<key> reaches the\n source document. There is no from_commit: a query sees\n open edges only (history is blocks_history/changes_since).\n - text()/semantic() are N/A on links.\n\nrevisions: (history as rows \u2014 one per stored revision of every document of the\n repository, deleted documents included: what docs_history lists, queryable)\n - bare fields = rev (= id), seq (1, 2, \u2026 per document), doc_id, commit,\n ts (the commit's time, sorts chronologically), origin\n (\"observed\"/\"api\"/\"import\"/\"projection\"), actor, reason,\n hash (the rendered file's sha256 \u2014 docs_history's hash),\n is_current (true for the document's current revision).\n - attributes = id (the revision id, r_\u2026), path (the path the revision\n was committed under \u2014 a move re-paths only the current\n revision, so earlier ones keep the old path). doc.<key>\n reaches the document's CURRENT row; absent for a deleted\n document, so where doc == null is the revisions of deleted documents.\n - on docs = revisions is a relation (oldest first): a document with\n more than one version is docs { revisions count { } > 1 }.\n - text()/semantic() are N/A on revisions. The body as of a revision is\n docs_read { path, at: \"<rev>\" } (docs_read_at is the same call).\n\n## Scoping (ADR-015; the root row since surface 2.0): bare names are LOCAL\n\n name the CURRENT row only \u2014 never falls through to an enclosing row\n or the repository; an absent field is simply absent.\n ^name one scope OUT (the enclosing row's fields/intrinsics/lifts);\n ^^name two scopes out. `^path` = the enclosing row's path.\n ^docs ^nodes ^blocks ^links ^revisions\n THE ROOT ROW (surface 2.0, oqx 0.18): the repository is the root\n scope's row, one scope out from a top-level row, so ^docs there is\n the whole documents scan (uncorrelated until you add a ^ predicate);\n one caret per enclosing block (^^docs from depth two), or the\n absolute 0^docs from ANY depth. ^id / 0^id = the repository id.\n ^$it = the root object: ^$it.docs \u2261 ^docs, entries(^$it) names the\n five collections. At the root scope itself a bare target is the scan\n (docs count { \u2026 }, from docs, entries(docs)).\n ERRORS: a bare `docs` INSIDE a block reads a property of the current\n row, which has none \u2014 refused (\"did you mean ^docs\"); `^docs` AT THE\n TOP LEVEL reaches past the root \u2014 refused (write the bare `docs`);\n so is `$repo` (surface < 2.0; the message names the replacement).\n nodes/blocks on a doc row are that doc's RELATIONS, not the root.\n $it the current item itself (a row, or each ELEMENT when the\n receiver is a list property); ^$it = the enclosing row.\n $value is NOT an intrinsic (since oqx 0.15): it reads a\n property literally named $value, absent when there is none.\n $key the row's identity (0.20): a store row's id (doc/block/node/edge), an entries(x)\n entry's key, a follow occurrence's walk identity; project $key: <expr> to rekey the\n rows a block yields (a destination block's $key: is not a column).\n\n## Scalar semantics (@omgbase/oqx)\n\n comparisons == != < <= > >= strict & typed (5 == \"5\" is false); either\n side may be a field (field-vs-field OK).\n absence null and a missing field are the same \"absent\" value; two absents\n are EQUAL, so `absent != \"x\"` is TRUE and `absent == null` is TRUE.\n Relational (< <= > >=) on an absent operand \u2192 FALSE. Bare `f` in\n boolean position uses JS truthiness (absent/false/0/\"\" \u21D2 false;\n NOTE: \"false\", [] and {} are truthy). has(f) = explicit presence.\n boolean and or not grouping ( \u2026 ) \u2014 && || ! are accepted synonyms\n arithmetic + - * / % (+ concatenates if either side is a string)\n membership x in y array \u2192 typed membership; string \u2192 substring;\n object \u2192 key exists; range \u2192 coverage. Any list\n property works directly: \"pricing\" in tags. list(f)\n coerces scalar-or-list (absent \u2192 []) \u2014 use it when a\n key is sometimes scalar, sometimes a list.\n ranges lo..hi (inclusive) lo...hi (exclusive hi) ..hi lo..\n era in 800..1680; works over ISO dates. range(prop) reads a\n range-shaped STRING property as an interval: \"2026-01-15\" in range(window).\n strings x.contains(\"s\") x.startsWith(\"p\") x.endsWith(\"s\") x.matches(\"^re$\")\n (real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE \u2014\n fold with .lower(): title.lower().contains(\"aurora\").\n Infix regex: x ~ \"re\" is x.matches(\"re\"); x ~* \"re\" is the\n case-insensitive match; x !~ \"re\" and x !~* \"re\" negate them.\n Unanchored unless the pattern anchors; an absent x is false\n under ~ and TRUE under !~ (not SQL's NULL).\n functions has(f) size(x) list(x) entries(x) range(s)\n literals \"str\" 'str' 123 1.5 true false null\n determinism no now()/clock/random. Lists/structs are not literals; ternary is\n not supported. Errors surface as filter_invalid.\n scalars vs lists a list-authored key never == a scalar (tags == \"x\" is false\n when tags is a list) \u2014 use `\"x\" in tags` / `in list(tags)`.\n\n## Domain predicates (omgbase)\n\n text(\"terms\") FTS5 PRUNING predicate (docs match via their blocks, nodes\n via the node index); composes anywhere, incl. nested scopes.\n semantic(\"phrase\") embedding cosine SCORE (docs/blocks): threshold it\n (semantic(\"x\") > 0.6), project it, or `order by semantic(\"x\")\n desc` for top-K. Needs a provider that has embedded the repository; else provider_unavailable.\n blocks target only:\n under(id_or_heading) block in the containment subtree / section range\n under_heading(\"Launch\") an ancestor section heading contains the text (ci)\n under_kind(type[, name]) an ancestor block has that type (name: its text\n contains / its key equals name)\n within(\"path\" | \"d_..\") containing doc = id, exact path, or path glob (*);\n either path form\n yaml_path(\"a.b\") YAML docs: the block at that key path\n json_pointer(\"#/a/b\") JSON docs: the block at that pointer\n has_edge(\"pred\"[, target]) an open authored edge with that predicate leaves here\n has_anchor() block carries an authored ^ref\n parent_type() == \"x\" parent block's type (must be compared)\n child_count() > 0 number of direct children (must be compared)\n refs(x) the live documents a property's document references name:\n x is a string, a list or absent; each \"/a/b.md\", \"a/b.md\"\n or \"d_\u2026\" element resolves to that doc's row; dangling and\n non-string elements are dropped. Yields docs rows, so it is\n a source, a receiver or a follow destination:\n follow refs(before), refs(after) walks a timeline both ways.\n The reverse direction needs no function: a document's\n path is already the reference form, so\n ^docs { where ^path in list(after) } is\n \"the documents whose `after` names me\".\n A HIT IS A STORE ROW: a top-level row that is not a doc/block/node/edge fails\n (filter_invalid). `follow before` over a list of paths\n reaches the STRINGS \u2014 write `follow refs(before)`.\n\n## Nested queries & consumers\n\n <receiver> collect|exists|none|count|first|single { <block> }\n receiver = a relation (nodes, blocks, doc.out_links, section.blocks, \u2026), a\n reach-through row (dst, doc: one row \u2014 dst { title }), a\n list property (tags), entries(x), or a root collection (^docs; 0^docs at depth \u2265 2).\n block = [select \u2026] [from E] [where \u2026] [frontier \u2026] [follow \u2026] [order by \u2026] [limit N] [offset N]\n \u2014 the same fixed order as the top level (`from` optional; the\n receiver supplies the rows). A leading name/alias list is the\n projection (`select` dropped); a predicate always needs `where`.\n A `from E` inside a block RE-PROJECTS the rows through a relation\n (a typed flatMap): `docs collect { from nodes where kind == \"md:task\" }`\n starts at docs, projects each through `nodes`, and queries the\n resulting nodes; a scalar relation (`from doc`) contributes one\n row; a missing or non-navigable relation is a loud error, never a\n global scan.\n exists \u22651 row nodes exists { where kind == \"md:task\" and not checked }\n none 0 rows nodes none { where kind == \"md:task\" and not checked } (\"every task done\")\n count a number nodes count { where kind == \"md:task\" } >= 2\n collect rows (default) \u2014 in select for nested results; in where with a lift\n first / single zero-or-one (single errors on >1) \u2014 lookups in select\n distinct on any consumer: dedup by projected value \u2014 nodes count distinct { select kind }\n limit/offset bound the block's rows AFTER where/order/distinct, BEFORE the\n consumer reduces: nodes exists { offset 1 } = at least two;\n first { \u2026 offset 1 } = the second.\n lift `^name:` in a where-collect binds values outward:\n select path, open from docs where nodes collect { ^open: value where kind == \"md:task\" and not checked }\n joins ^<t> exists { where slug == ^ref } semi-join; not \u2026 exists anti-join;\n ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } lookup;\n x in ^keys membership over a lifted set.\n\n## select \u2014 projection\n\n select a, alias: expr, nested: nodes collect { \u2026 }\n bare key \u2192 the attribute when it is one (id, path, title, \u2026), else the property\n (scalar-authored \u2192 scalar; list \u2192 array); data.<k> forces the property;\n a call/arithmetic needs an alias (n: size(tags)) unless in values mode;\n an output key of id/path other than the attribute itself needs an alias.\n raw whole file bytes (docs) \u2014 docs_read is cheaper for one doc.\n distinct select distinct type (dedup hits by projected value)\n values select <ONE item> values \u2192 bare values instead of records:\n select distinct type values from docs \u2192 values: [\"hub\",\"lab-note\",\u2026]\n select path values from docs \u2192 values: [\"/index.md\", \u2026]\n tags: tags collect { $it values } \u2192 a plain array\n h: nodes first { name values where kind == \"md:section\" order by first_ordinal }\n entries(x) a record as a collection ($key / $it per entry), key order:\n select fm: entries(data) collect { k: $key, v: $it }\n where entries(frontmatter) exists { where $key == \"era\" and $it > 1600 }\n entries(inline), entries(attrs), a nested map, or a list (index keys)\n $semantic_score is not a field \u2014 project semantic(\"\u2026\") under an alias instead.\n\n## order by, limit/offset, pagination\n\n order by era desc, path asc absent values sort LAST (asc); ties \u2192 (path, id)\n limit N / offset N define the result SET (integer literals);\n the tool's limit/cursor then page WITHIN it\n cursor opaque keyset on (path,id); a custom order by\n disables it (you get the top page + truncated)\n\n## Graph traversal (follow) and links\n\n The citation graph is the LINK ROWS walked through a destination block:\n dst is a link's destination document (absent for an external or dangling\n link \u2014 it then contributes no successor), doc its source.\n from docs where <seed> follow out_links collect { from dst } docs the seed links TO\n from docs where <seed> follow in_links collect { from doc } backlinks\n follow out_links collect { from dst depth 3 } bound (1..8, default 8) \u2014 a clause of the block\n follow out_links collect { from dst where layer == \"canon\" } keep only matching successors\n follow in_links collect { from doc where ^path in list(before) } ^ = the row being expanded\n (^^ = the walk's enclosing scope)\n follow out_links collect { from dst where predicate == \"depends_on\" } one predicate only\n follow out_links collect { from dst }, in_links collect { from doc }\n several destinations: their union\n (one step dedups by identity)\n follow ^docs { where out_links exists { where dst_path == ^^path } }\n a destination block over the root's\n documents, re-read per frontier row\n (^ = that row) = the backlinks walk\n follow out_links collect { from dst frontier type == \"practitioner\" } cut, keeping the frontier row\n from docs where type == \"process\" frontier !verified follow out_links collect { from dst }\n the BODY's frontier applies to the seeds:\n an unverified seed is kept, not expanded\n follow distinct out_links collect { from dst } / \u2026 { $key: group from dst } identity control ($key)\n follow refs(before), ^docs { where ^path in list(after) } forward over `before`, backward\n over every doc whose `after` names me\n Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|\n depth|cycle), $leaf/$frontier, $ordinal (a deterministic 1..N rank over the\n walk, ordered by depth then path \u2014 `order by $ordinal` for walk order,\n `where $ordinal <= N` for a deterministic budget cut), $key \u2014 usable in\n select/order by and the top-level post-walk where (a where conjunct that\n reads one filters the walk's RESULT; the other conjuncts remain the seed\n predicate), NOT metadata of the candidates a destination block's where reads,\n nor valid inside a consumer directive. Cyclic graphs are safe: a revisited\n node is admitted once as a `$stop == \"cycle\"` occurrence and never\n re-expanded. The default keeps one occurrence per distinct walk path;\n `follow distinct` dedups reached rows by identity.\n Destination blocks (OQX 0.20): a brace after ANY destination opens that\n destination's block \u2014 children { depth 2 }, refs(x) { depth 2 }, out_links\n collect { from dst depth 2 }, ^docs { where ^path in list(after) } \u2014 with the\n 0.17 body rules (where-first, projections) and two clauses of its own after\n `where`: frontier <pred> (occurrences reached through this destination that\n match are frontiers; ^ there is the walk's enclosing scope) and depth <n>\n (this destination expands a frontier row only while $depth < n). The body\n that carries the follow may write frontier <pred> too, after its where and\n before follow: it applies to the SEEDS (read like the body's where) \u2014 a\n matching seed stays in the result with $stop == \"frontier\" and is not\n expanded; each destination's frontier is its own. There is no options block;\n the old `by` is $key. A bare follow ^docs (no brace, no consumer) is an\n error naming both block spellings.\n Type-preserving relations: block.children, section.children, section.subsections;\n the citation graph is out_links collect { from dst } / in_links collect { from doc }.\n from links where dst_path == \"/notes/x.md\" inbound links to a doc, as rows\n select d: dst.title, k: dst_kind from links the destination document's title\n from docs where out_links exists { where dst.layer == \"draft\" } links into drafts\n select b: in_links collect distinct { from doc } from docs backlinks as { id, path }\n select doc_id, dst_path from links where predicate == \"depends_on\"\n from docs where not doc.in_links exists { } orphans (nothing links in)\n (The `graph` tool is a convenience wrapper that compiles to a follow query.)\n\n## semantic grain\n\nfrom docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.\ntext()/where prune the candidate set; only order by reweights.\n\n## Examples\n\n from docs where layer == \"working\"\n from docs where path.startsWith(\"/guides/\") and updated_at >= \"2026-08-01\"\n select layer, tags from docs where \"pricing\" in list(tags)\n from docs where inline.owner == \"alice\"\n from docs where title.lower().contains(\"q3 plan\")\n from docs where format == \"yaml\"\n from docs where type == \"practitioner\" order by era desc limit 2\n from docs where nodes none { where kind == \"md:task\" and not checked }\n from docs where tags exists { where $it == \"tria-prima\" }\n select fm: entries(data) collect { k: $key, v: $it } from docs where path == \"/x.md\"\n select owner_id, owner: ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } from docs\n from blocks where type == \"task\" and not checked and under_heading(\"Launch\") and doc.layer == \"working\"\n from blocks where type == \"paragraph\" and has_edge(\"references\", \"d_92aaaaa\")\n select path, $depth from docs where path == \"/timeline/review.md\" follow refs(before), refs(after)\n select path, next: ^docs collect { path where ^path in list(before) } from docs where type == \"milestone\"\n select path, n: size(^docs), repo: ^id from docs limit 1\n select p: data.path, t: data.title from docs where has(data.path)\n docs { type == \"substance\" and not verified }\n select subject, process: ^docs[slug == ^subject]!.path from docs where type == \"lab-note\"\n select path, back: nodes first { select p: 0^docs collect { path where ^^path in list(after) } values } from docs where type == \"milestone\"\n select path, prior: refs(before) collect { path, when } from docs where type == \"milestone\"\n from blocks order by semantic(\"identity preservation across edits\") desc limit 10\n select uri from links where dst_kind == \"external\"\n docs count { where layer == \"canon\" }\n select seq, origin, ts, hash from revisions where path == \"/processes/calcination.md\" order by seq desc\n from docs where revisions count { where origin == \"api\" } > 0\n select p: path, d: doc.path from revisions where doc == null\n\nCommon mistake: `$path`/`$id` (the 2.x spelling) \u2014 omgbase has no $ names; write\n`path`/`id`. Another: `where tags == \"x\"` on a list-valued key is false \u2014 write\n`\"x\" in tags`. Another: `select data.path` \u2014 alias it (`p: data.path`).\n";
|
|
1
|
+
export declare const QUERY_SYNTAX = "# query \u2014 OQX syntax reference\n\nThe `query` tool takes ONE plain OQX string (+ optional `limit`/`cursor`, and\n`bare: true` to drop the automatic `id`/`path` envelope \u2014 needs a `select`).\nThere is no {from, filter, order} envelope: every concern is a clause of the\nstring. Two rules tell the two syntaxes apart: DOT navigation belongs to the\nhost object model (`doc.layer`, `section.blocks`, `dst.title`); WHITESPACE\nquery directives belong to OQX \u2014 a nested query is `<receiver> collect|exists|\ncount|first|single { <block> }` (a postfix directive + a `{ \u2026 }` block), never a\nmethod. Its distinctive power is the receiver-constrained nested query that\ncorrelates to the current row: `from docs where nodes exists { where kind ==\n\"md:task\" }` returns only the docs that themselves contain a matching node.\n\n [select <items>] from <target> [where <pred>] [frontier <pred>] [follow <dest> [{ \u2026 }], \u2026]\n [group by <key>, \u2026] [having <pred>] [order by <expr> [asc|desc], \u2026] [limit N] [offset N]\n <target> count|exists|none|first|single { <block> } (scalar/one-row form: a bare target at the root scope)\n\nCLAUSE ORDER IS FIXED: select, from, where, frontier, follow, group by, having, order by, limit, offset \u2014\neach at most once; an out-of-order clause is a parse error naming the order.\nOnly `select` may drop its keyword, and only as the first clause\n(`path, era from docs where \u2026`). A top-level predicate needs `where`, because\n`where` follows `from` (`era > 1600 from docs` is the clause-order error); a\nblock may LEAD with a predicate (see Sugar), but a predicate after a projection\nstill needs the keyword (`{ name where kind == \"md:task\" }`, not\n`{ name, kind == \"md:task\" }`); `from docs count` is an error (write\n`docs count { \u2026 }`). `where` and later `select` items may use the same body's\n`select` aliases (`select path, old: era < 1000 from docs where old`; an alias\nshadows a same-named field there). `and` / `or` / `not` are the connectives;\nthe symbols `&&` / `||` / `!` are accepted synonyms (the words are canonical and\nwhat `print` writes).\n\n## Sugar (OQX 0.17)\n\nEvery form here is shorthand for an explicit directive (the AST is the explicit form):\n nodes { kind == \"md:task\" } \u2261 nodes collect { where kind == \"md:task\" } \u2014 a receiver block with\n no consumer is collect; a block whose LEADING expression is a predicate\n (comparison, call, !/is/not, in, literal, (\u2026), a consumer test \u2014 anything\n but a bare name, a dotted path or a ^lift) is where-first. Bare names still\n project: nodes { name } \u2261 nodes collect { name }; filter a bare field with\n nodes { is checked }. Wherever a value stands (0.20), call arguments included:\n size(children { is checked }); x distinct { \u2026 } \u2261 x collect distinct { \u2026 }. After a\n follow destination too: follow children { depth 2 } is that destination's block.\n (a, b) a tuple (0.20): two or more expressions, concatenated one level flat \u2014 a value\n (size((tags, aliases)), \"x\" in (tags, aliases)) and a receiver ((a, b) { \u2026 },\n follow (before, after) { depth 2 }); (a) alone is grouping\n refs(x)[0] \u2261 refs(x) first { offset 0 } positional (integer literal or ${binding});\n out of range \u21D2 absent; refs(x)[0].path navigates the row\n ^docs[path == ^company] \u2261 ^docs first { where path == ^company } first match or absent\n ^docs[path == ^company]! \u2261 \u2026 single { \u2026 } required: exactly one, else filter_invalid\n title! required: title, or an error naming the expression (and the row's $key \u2014 its id)\n \u2014 never a filter, never a coercion (0!, \"\"! are values); tightest: a!.b vs a.b!\n is x / not x truthiness of x, and its negation (prefix; `not` \u2261 `!`)\n x is y / x is not y identity ($key: a store row's id, else structural) \u2014 compares rows, which == does not;\n x is null = absent; for scalars is \u2261 ==; comparison precedence, no chaining\n a and b / a or b the connectives (\u2261 && / ||: same precedence, short-circuit, value: data.title or path coalesces)\n boss: ^docs[path == ^manager], bossName: boss.title a select item may use the items to its LEFT\n Reserved words (never a bare field name): from where select is not and or, true false null.\n Contextual words (limit order offset follow collect count first distinct desc by in frontier depth) parse as\n fields in expression position (select o: order from docs).\n\nResults are LEAN hits \u2014 { id, path } + whatever `select` projects (or `values`,\n`count`, `exists`, `none`). The envelope keys ARE the attributes: copy a hit's\n`id` straight into `where id == \"\u2026\"` (no translation); `select id, path` is simply\nthe envelope (no duplicate columns); any OTHER projection whose output key would\nbe `id` or `path` must be aliased \u2014 `select data.path` is refused (\"projection\n`path` collides with the hit envelope \u2014 alias it (`p: data.path`)\"); a row\nsurfacing as a value (an empty `collect { }`) is the same `{ id, path }`. Every\nother tool spells a row the same way (`docs_read` \u2192 `{ id, path, rev, properties,\nraw }`, `blocks_read` \u2192 `{ id, type, text, raw, hash, \u2026 }`, `docs_list` \u2192 `{ id,\npath, updated_at, blocks }`, a revision \u2192 `{ id, path, hash }`): hydrate by id via\nblocks_read/docs_read.\n\n## Paths\n\nEvery path the surface returns is `/`-rooted \u2014 the form a reference is\nwritten in (`[x](/projects/oqx.md)`, `before: [/timeline/kickoff.md]`): a\nhit's `path`, an edge's `dst_path`, every tool's paths. Every path it\naccepts tolerates both forms (`/a.md` or `a.md`): tool arguments, `refs(x)`,\n`within(...)`, and a string LITERAL compared with `path`/`dst_path` (bare, or\nthrough a row handle: `doc.path`) by `==`/`!=` or passed to their\n`.startsWith(...)` (it is rooted first, so `path == \"a.md\"` and `path == \"/a.md\"`\nboth match). A property holding a reference compares directly \u2014 and is never\nrooted: `where ^path in list(after)`, `where customer == ^path`,\n`where data.path == \"/x.md\"` (the author's `path:` key, as written) \u2014 no\n`\"/\" + ...` glue.\n\n## Targets & field namespaces\n\nTHE NAME RULE: `$` belongs to OQX \u2014 $it, $key, $depth, $stop, $leaf, $frontier,\n$ordinal are values the LANGUAGE injects into a scope; omgbase adds NO $ names\n(a `$id` in a query is an unknown name, absent). Every engine attribute is a BARE\nsnake_case name that WINS on collision \u2014 the convention the relations (children,\nblocks, out, in, doc, frontmatter, \u2026) always followed: a bare name that is an\nengine attribute reads the engine's value, never yours. Your data is bare too;\nwhere a key of yours collides with an engine name, reach it through a source\nhandle: `data.<k>` (the merged authored view \u2014 frontmatter + inline, exactly the\nread a bare non-colliding key performs), `frontmatter.<k>`, `inline.<k>`. So\n`path` is the engine's /-rooted location and `data.path` your `path:` key;\n`tags` is your tags \u222A the body #hashtags and `data.tags` the authored list alone;\n`title` the computed title (the first H1) and `data.title` your `title:`;\n`entries(data)` lists every authored top-level key. Same value, same name\neverywhere: a hit's `id` is docs_read's `id` and the `id` argument of blocks_read.\n\n Reserved bare names per kind (engine attributes; handles; self; relations):\n docs id path raw hash updated_at title tags format | data frontmatter inline | doc | blocks nodes out_links in_links revisions\n blocks id path doc_id raw hash updated_at type text attrs depth ordinal | block doc | children nodes out_links section\n nodes id path doc_id block_id kind name value attrs | section doc block | blocks subsections children\n links id path doc_id block_id via_id dst_id dst_path uri field dst_kind predicate provenance anchor | doc\n revisions id path rev seq doc_id commit ts origin actor reason hash is_current archived | doc\n A key of yours with any of these names is `data.<k>` (or frontmatter.<k> /\n inline.<k> by source; attrs.<k> on a block or node).\n UNKNOWN NAMES: a name that is none of the above AND that no object of the\n kind in this repository carries is reported before any row is read \u2014 on\n links as a filter_invalid error naming the fields; on docs/blocks/nodes the\n result gains `hints` (one line per name, nearest match when there is one)\n and the rows still come back; in a plan/apply/execute selector it is refused.\n A property some objects carry is known (partial presence is normal).\n\ndocs:\n - bare identifier = a document PROPERTY (frontmatter + inline `key:: value`,\n unioned; nested via dots: meta.owner). YAML/JSON docs\n expose the parsed object's keys the same way.\n - source handles = data.<k> (merged authored view) / frontmatter.<k> /\n inline.<k>; the whole bag as a collection via entries(data)\n / entries(frontmatter) / entries(inline) (see entries()).\n - attributes = id, path (/-rooted, see Paths), updated_at (ISO-8601,\n sorts chronologically), raw (the whole file, frontmatter\n included), hash (its sha256, 64 hex), title (the computed\n title: the first H1; data.title is the authored key), tags\n (authored tags \u222A body #hashtags, deduped, authored first;\n data.tags is the authored list), format (the doc's format \u2014\n \"markdown\"/\"yaml\"/\"json\"; present on EVERY doc, so\n has(format) is always true and `format == \"yaml\"` is the\n filter).\n - relations = nodes, blocks, doc.out_links / doc.in_links (a doc's\n links as rows; the documents at their other end are\n out_links collect distinct { from dst } and\n in_links collect distinct { from doc } \u2014 there is no\n out / in relation), revisions (the document's stored\n revisions, oldest first \u2014 see revisions below).\n\nblocks:\n - bare fields = doc_id (the owning doc), type, text (normalized), ordinal\n (0-based among siblings), depth (0 at the top level), and\n every attrs key FLATTENED onto the row (bare `checked`\n reads attrs.checked; `attrs.<k>` still works; the\n structural fields win on collision; absent \u21D2 false).\n - attributes = id, path (the owning doc's path), raw (the block's raw\n source \u2014 a container's raw holds its children), hash\n (its sha256 \u2014 the value update/split expect as\n expect.content_hash), updated_at.\n - reach-through = doc.<key> / doc.path / doc.id / doc.format = the\n CONTAINING doc (doc.id and doc.path cost nothing \u2014 they\n read the row's own doc_id / joined path, no lookup).\n - relations = block.children, block.nodes, block.out_links, section.\n NOTE: inside a `follow`, $depth/$ordinal are ALWAYS the walk's metadata; a\n block's own nesting is `depth` / `ordinal` (no $) everywhere.\n\nnodes:\n - bare fields = doc_id, block_id (null for a block-less node), kind\n (md:task / md:link / md:section / yaml:\u2026), name, value,\n + attrs keys flattened (checked, level).\n - attributes = id, path.\n - reach-through = doc.<key> / doc.id / doc.path, block.type / block.text\n / block.id / block.path.\n - relations = section.blocks, section.children, section.subsections.\n\nlinks: (the authored link graph as rows \u2014 one per open link: the relation a link\n establishes; the md:link NODE is its occurrence in the text and via_id\n points at it \u2014 a frontmatter link has a row and no node)\n - bare fields = doc_id (the SOURCE doc), block_id (the source block; null\n for a frontmatter edge), via_id, dst_id (the destination\n node id: a doc id, an external node id or phantom:<path>),\n dst_path (target doc path, /-rooted; NULL when\n dangling/external), uri (external URL; NULL otherwise),\n predicate (\"references\"/\"embeds\"/freeform), provenance\n (\"link\"/\"frontmatter\"/\"inline_field\"/\u2026), dst_kind\n (\"document\"/\"external\"/\"collection\"), anchor, field (the\n frontmatter key / inline field it was authored in).\n - attributes = id, path (the SOURCE doc's path); doc.<key> reaches the\n source document. There is no from_commit: a query sees\n open edges only (history is blocks_history/changes_since).\n - text()/semantic() are N/A on links.\n\nrevisions: (history as rows \u2014 one per stored revision of every document of the\n repository, deleted documents included: what docs_history lists, queryable)\n - bare fields = rev (= id), seq (1, 2, \u2026 per document), doc_id, commit,\n ts (the commit's time, sorts chronologically), origin\n (\"observed\"/\"api\"/\"import\"/\"projection\"), actor, reason,\n hash (the rendered file's sha256 \u2014 docs_history's hash),\n is_current (true for the document's current revision),\n archived (true when the bodies moved to an archive segment:\n the row stays, docs_read { at } reads through the segment).\n - attributes = id (the revision id, r_\u2026), path (the path the revision\n was committed under \u2014 a move re-paths only the current\n revision, so earlier ones keep the old path). doc.<key>\n reaches the document's CURRENT row; absent for a deleted\n document, so where doc == null is the revisions of deleted documents.\n - on docs = revisions is a relation (oldest first): a document with\n more than one version is docs { revisions count { } > 1 }.\n - text()/semantic() are N/A on revisions. The body as of a revision is\n docs_read { path, at: \"<rev>\" } (docs_read_at is the same call).\n\n## Scoping (ADR-015; the root row since surface 2.0): bare names are LOCAL\n\n name the CURRENT row only \u2014 never falls through to an enclosing row\n or the repository; an absent field is simply absent.\n ^name one scope OUT (the enclosing row's fields/intrinsics/lifts);\n ^^name two scopes out. `^path` = the enclosing row's path.\n ^docs ^nodes ^blocks ^links ^revisions\n THE ROOT ROW (surface 2.0, oqx 0.18): the repository is the root\n scope's row, one scope out from a top-level row, so ^docs there is\n the whole documents scan (uncorrelated until you add a ^ predicate);\n one caret per enclosing block (^^docs from depth two), or the\n absolute 0^docs from ANY depth. ^id / 0^id = the repository id.\n ^$it = the root object: ^$it.docs \u2261 ^docs, entries(^$it) names the\n five collections. At the root scope itself a bare target is the scan\n (docs count { \u2026 }, from docs, entries(docs)).\n ERRORS: a bare `docs` INSIDE a block reads a property of the current\n row, which has none \u2014 refused (\"did you mean ^docs\"); `^docs` AT THE\n TOP LEVEL reaches past the root \u2014 refused (write the bare `docs`);\n so is `$repo` (surface < 2.0; the message names the replacement).\n nodes/blocks on a doc row are that doc's RELATIONS, not the root.\n $it the current item itself (a row, or each ELEMENT when the\n receiver is a list property); ^$it = the enclosing row.\n $value is NOT an intrinsic (since oqx 0.15): it reads a\n property literally named $value, absent when there is none.\n $key the row's identity (0.20): a store row's id (doc/block/node/edge), an entries(x)\n entry's key, a follow occurrence's walk identity; project $key: <expr> to rekey the\n rows a block yields (a destination block's $key: is not a column).\n\n## Scalar semantics (@omgbase/oqx)\n\n comparisons == != < <= > >= strict & typed (5 == \"5\" is false); either\n side may be a field (field-vs-field OK).\n absence null and a missing field are the same \"absent\" value; two absents\n are EQUAL, so `absent != \"x\"` is TRUE and `absent == null` is TRUE.\n Relational (< <= > >=) on an absent operand \u2192 FALSE. Bare `f` in\n boolean position uses JS truthiness (absent/false/0/\"\" \u21D2 false;\n NOTE: \"false\", [] and {} are truthy). has(f) = explicit presence.\n boolean and or not grouping ( \u2026 ) \u2014 && || ! are accepted synonyms\n arithmetic + - * / % (+ concatenates if either side is a string)\n membership x in y array \u2192 typed membership; string \u2192 substring;\n object \u2192 key exists; range \u2192 coverage. Any list\n property works directly: \"pricing\" in tags. list(f)\n coerces scalar-or-list (absent \u2192 []) \u2014 use it when a\n key is sometimes scalar, sometimes a list.\n ranges lo..hi (inclusive) lo...hi (exclusive hi) ..hi lo..\n era in 800..1680; works over ISO dates. range(prop) reads a\n range-shaped STRING property as an interval: \"2026-01-15\" in range(window).\n strings x.contains(\"s\") x.startsWith(\"p\") x.endsWith(\"s\") x.matches(\"^re$\")\n (real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE \u2014\n fold with .lower(): title.lower().contains(\"aurora\").\n Infix regex: x ~ \"re\" is x.matches(\"re\"); x ~* \"re\" is the\n case-insensitive match; x !~ \"re\" and x !~* \"re\" negate them.\n Unanchored unless the pattern anchors; an absent x is false\n under ~ and TRUE under !~ (not SQL's NULL).\n functions has(f) size(x) list(x) entries(x) range(s)\n dates a date is an ISO string, a duration a NUMBER OF SECONDS; arithmetic\n is METHODS, never + or - (+ concatenates strings).\n date(\"today\"|\"now\"|\"yesterday\"|\"tomorrow\"|\"sow\"|\"eow\"|\"som\"|\"eom\"|\n \"soy\"|\"eoy\") \u2014 the host clock, UTC; date(\"2026-10-15\") canonical ISO\n (absent if unparseable). dur(\"1h30m\") dur(\"2 days, 3 hours\")\n dur(\"P1DT2H\") dur(90) \u2192 seconds (year = 365 d, month = 30 d).\n d.plus(dur(\"30 days\")) d.minus(86400) d.plus(\"1 month\") (calendar:\n Jan 31 \u2192 Feb 28) a.until(b) \u2192 signed seconds d.year() d.month()\n d.day() d.weekday() (1 = Mon) d.hour() d.minute() d.second()\n d.date() d.format(\"EEE dd MMM yyyy\") n.dur_text() \u2192 \"1h 30m\"\n n.dur_iso() \u2192 \"PT1H30M\". Total: a non-date is absent, so\n `where date(due) < date(\"today\").minus(dur(\"30 days\"))` just\n filters. A datetime is never < its own date: compare\n updated_at.date() == date(\"today\"), or < date(\"tomorrow\").\n literals \"str\" 'str' 123 1.5 true false null\n determinism no random; the only clock is date(\"today\"/\"now\"/\u2026), read once per\n query. Lists/structs are not literals; ternary is not supported.\n Errors surface as filter_invalid.\n scalars vs lists a list-authored key never == a scalar (tags == \"x\" is false\n when tags is a list) \u2014 use `\"x\" in tags` / `in list(tags)`.\n\n## Domain predicates (omgbase)\n\n text(\"terms\") FTS5 PRUNING predicate (docs match via their blocks, nodes\n via the node index); composes anywhere, incl. nested scopes.\n semantic(\"phrase\") embedding cosine SCORE (docs/blocks): threshold it\n (semantic(\"x\") > 0.6), project it, or `order by semantic(\"x\")\n desc` for top-K. Needs a provider that has embedded the repository; else provider_unavailable.\n blocks target only:\n under(id_or_heading) block in the containment subtree / section range\n under_heading(\"Launch\") an ancestor section heading contains the text (ci)\n under_kind(type[, name]) an ancestor block has that type (name: its text\n contains / its key equals name)\n within(\"path\" | \"d_..\") containing doc = id, exact path, or path glob (*);\n either path form\n yaml_path(\"a.b\") YAML docs: the block at that key path\n json_pointer(\"#/a/b\") JSON docs: the block at that pointer\n has_edge(\"pred\"[, target]) an open authored edge with that predicate leaves here\n has_anchor() block carries an authored ^ref\n parent_type() == \"x\" parent block's type (must be compared)\n child_count() > 0 number of direct children (must be compared)\n refs(x) the live documents a property's document references name:\n x is a string, a list or absent; each \"/a/b.md\", \"a/b.md\"\n or \"d_\u2026\" element resolves to that doc's row; dangling and\n non-string elements are dropped. Yields docs rows, so it is\n a source, a receiver or a follow destination:\n follow refs(before), refs(after) walks a timeline both ways.\n The reverse direction needs no function: a document's\n path is already the reference form, so\n ^docs { where ^path in list(after) } is\n \"the documents whose `after` names me\".\n A HIT IS A STORE ROW: a top-level row that is not a doc/block/node/edge fails\n (filter_invalid). `follow before` over a list of paths\n reaches the STRINGS \u2014 write `follow refs(before)`.\n\n## Nested queries & consumers\n\n <receiver> collect|exists|none|count|first|single { <block> }\n receiver = a relation (nodes, blocks, doc.out_links, section.blocks, \u2026), a\n reach-through row (dst, doc: one row \u2014 dst { title }), a\n list property (tags), entries(x), or a root collection (^docs; 0^docs at depth \u2265 2).\n block = [select \u2026] [from E] [where \u2026] [frontier \u2026] [follow \u2026] [order by \u2026] [limit N] [offset N]\n \u2014 the same fixed order as the top level (`from` optional; the\n receiver supplies the rows). A leading name/alias list is the\n projection (`select` dropped); a predicate always needs `where`.\n A `from E` inside a block RE-PROJECTS the rows through a relation\n (a typed flatMap): `docs collect { from nodes where kind == \"md:task\" }`\n starts at docs, projects each through `nodes`, and queries the\n resulting nodes; a scalar relation (`from doc`) contributes one\n row; a missing or non-navigable relation is a loud error, never a\n global scan.\n exists \u22651 row nodes exists { where kind == \"md:task\" and not checked }\n none 0 rows nodes none { where kind == \"md:task\" and not checked } (\"every task done\")\n count a number nodes count { where kind == \"md:task\" } >= 2\n collect rows (default) \u2014 in select for nested results; in where with a lift\n first / single zero-or-one (single errors on >1) \u2014 lookups in select\n distinct on any consumer: dedup by projected value \u2014 nodes count distinct { select kind }\n limit/offset bound the block's rows AFTER where/order/distinct, BEFORE the\n consumer reduces: nodes exists { offset 1 } = at least two;\n first { \u2026 offset 1 } = the second.\n lift `^name:` in a where-collect binds values outward:\n select path, open from docs where nodes collect { ^open: value where kind == \"md:task\" and not checked }\n joins ^<t> exists { where slug == ^ref } semi-join; not \u2026 exists anti-join;\n ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } lookup;\n x in ^keys membership over a lifted set.\n\n## select \u2014 projection\n\n select a, alias: expr, nested: nodes collect { \u2026 }\n bare key \u2192 the attribute when it is one (id, path, title, \u2026), else the property\n (scalar-authored \u2192 scalar; list \u2192 array); data.<k> forces the property;\n a call/arithmetic needs an alias (n: size(tags)) unless in values mode;\n an output key of id/path other than the attribute itself needs an alias.\n raw whole file bytes (docs) \u2014 docs_read is cheaper for one doc.\n distinct select distinct type (dedup hits by projected value)\n values select <ONE item> values \u2192 bare values instead of records:\n select distinct type values from docs \u2192 values: [\"hub\",\"lab-note\",\u2026]\n select path values from docs \u2192 values: [\"/index.md\", \u2026]\n tags: tags collect { $it values } \u2192 a plain array\n h: nodes first { name values where kind == \"md:section\" order by first_ordinal }\n entries(x) a record as a collection ($key / $it per entry), key order:\n select fm: entries(data) collect { k: $key, v: $it }\n where entries(frontmatter) exists { where $key == \"era\" and $it > 1600 }\n entries(inline), entries(attrs), a nested map, or a list (index keys)\n $semantic_score is not a field \u2014 project semantic(\"\u2026\") under an alias instead.\n\n## order by, limit/offset, pagination\n\n order by era desc, path asc absent values sort LAST (asc); ties \u2192 (path, id)\n limit N / offset N define the result SET (integer literals);\n the tool's limit/cursor then page WITHIN it\n cursor opaque keyset on (path,id); a custom order by\n disables it (you get the top page + truncated)\n\n## Graph traversal (follow) and links\n\n The citation graph is the LINK ROWS walked through a destination block:\n dst is a link's destination document (absent for an external or dangling\n link \u2014 it then contributes no successor), doc its source.\n from docs where <seed> follow out_links collect { from dst } docs the seed links TO\n from docs where <seed> follow in_links collect { from doc } backlinks\n follow out_links collect { from dst depth 3 } bound (1..8, default 8) \u2014 a clause of the block\n follow out_links collect { from dst where layer == \"canon\" } keep only matching successors\n follow in_links collect { from doc where ^path in list(before) } ^ = the row being expanded\n (^^ = the walk's enclosing scope)\n follow out_links collect { from dst where predicate == \"depends_on\" } one predicate only\n follow out_links collect { from dst }, in_links collect { from doc }\n several destinations: their union\n (one step dedups by identity)\n follow ^docs { where out_links exists { where dst_path == ^^path } }\n a destination block over the root's\n documents, re-read per frontier row\n (^ = that row) = the backlinks walk\n follow out_links collect { from dst frontier type == \"practitioner\" } cut, keeping the frontier row\n from docs where type == \"process\" frontier !verified follow out_links collect { from dst }\n the BODY's frontier applies to the seeds:\n an unverified seed is kept, not expanded\n follow distinct out_links collect { from dst } / \u2026 { $key: group from dst } identity control ($key)\n follow refs(before), ^docs { where ^path in list(after) } forward over `before`, backward\n over every doc whose `after` names me\n Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|\n depth|cycle), $leaf/$frontier, $ordinal (a deterministic 1..N rank over the\n walk, ordered by depth then path \u2014 `order by $ordinal` for walk order,\n `where $ordinal <= N` for a deterministic budget cut), $key \u2014 usable in\n select/order by and the top-level post-walk where (a where conjunct that\n reads one filters the walk's RESULT; the other conjuncts remain the seed\n predicate), NOT metadata of the candidates a destination block's where reads,\n nor valid inside a consumer directive. Cyclic graphs are safe: a revisited\n node is admitted once as a `$stop == \"cycle\"` occurrence and never\n re-expanded. The default keeps one occurrence per distinct walk path;\n `follow distinct` dedups reached rows by identity.\n Destination blocks (OQX 0.20): a brace after ANY destination opens that\n destination's block \u2014 children { depth 2 }, refs(x) { depth 2 }, out_links\n collect { from dst depth 2 }, ^docs { where ^path in list(after) } \u2014 with the\n 0.17 body rules (where-first, projections) and two clauses of its own after\n `where`: frontier <pred> (occurrences reached through this destination that\n match are frontiers; ^ there is the walk's enclosing scope) and depth <n>\n (this destination expands a frontier row only while $depth < n). The body\n that carries the follow may write frontier <pred> too, after its where and\n before follow: it applies to the SEEDS (read like the body's where) \u2014 a\n matching seed stays in the result with $stop == \"frontier\" and is not\n expanded; each destination's frontier is its own. There is no options block;\n the old `by` is $key. A bare follow ^docs (no brace, no consumer) is an\n error naming both block spellings.\n Type-preserving relations: block.children, section.children, section.subsections;\n the citation graph is out_links collect { from dst } / in_links collect { from doc }.\n from links where dst_path == \"/notes/x.md\" inbound links to a doc, as rows\n select d: dst.title, k: dst_kind from links the destination document's title\n from docs where out_links exists { where dst.layer == \"draft\" } links into drafts\n select b: in_links collect distinct { from doc } from docs backlinks as { id, path }\n select doc_id, dst_path from links where predicate == \"depends_on\"\n from docs where not doc.in_links exists { } orphans (nothing links in)\n (The `graph` tool is a convenience wrapper that compiles to a follow query.)\n\n## semantic grain\n\nfrom docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.\ntext()/where prune the candidate set; only order by reweights.\n\n## Examples\n\n from docs where layer == \"working\"\n from docs where path.startsWith(\"/guides/\") and updated_at >= \"2026-08-01\"\n select layer, tags from docs where \"pricing\" in list(tags)\n from docs where inline.owner == \"alice\"\n from docs where title.lower().contains(\"q3 plan\")\n from docs where format == \"yaml\"\n from docs where type == \"practitioner\" order by era desc limit 2\n from docs where nodes none { where kind == \"md:task\" and not checked }\n from docs where tags exists { where $it == \"tria-prima\" }\n select fm: entries(data) collect { k: $key, v: $it } from docs where path == \"/x.md\"\n select owner_id, owner: ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } from docs\n from blocks where type == \"task\" and not checked and under_heading(\"Launch\") and doc.layer == \"working\"\n from blocks where type == \"paragraph\" and has_edge(\"references\", \"d_92aaaaa\")\n select path, $depth from docs where path == \"/timeline/review.md\" follow refs(before), refs(after)\n select path, next: ^docs collect { path where ^path in list(before) } from docs where type == \"milestone\"\n select path, n: size(^docs), repo: ^id from docs limit 1\n select p: data.path, t: data.title from docs where has(data.path)\n docs { type == \"substance\" and not verified }\n select subject, process: ^docs[slug == ^subject]!.path from docs where type == \"lab-note\"\n select path, back: nodes first { select p: 0^docs collect { path where ^^path in list(after) } values } from docs where type == \"milestone\"\n select path, prior: refs(before) collect { path, when } from docs where type == \"milestone\"\n from blocks order by semantic(\"identity preservation across edits\") desc limit 10\n select uri from links where dst_kind == \"external\"\n docs count { where layer == \"canon\" }\n select seq, origin, ts, hash from revisions where path == \"/processes/calcination.md\" order by seq desc\n from docs where revisions count { where origin == \"api\" } > 0\n select p: path, d: doc.path from revisions where doc == null\n 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\n\nCommon mistake: `$path`/`$id` (the 2.x spelling) \u2014 omgbase has no $ names; write\n`path`/`id`. Another: `where tags == \"x\"` on a list-valued key is false \u2014 write\n`\"x\" in tags`. Another: `select data.path` \u2014 alias it (`p: data.path`).\n";
|
|
@@ -19,10 +19,10 @@ correlates to the current row: \`from docs where nodes exists { where kind ==
|
|
|
19
19
|
"md:task" }\` returns only the docs that themselves contain a matching node.
|
|
20
20
|
|
|
21
21
|
[select <items>] from <target> [where <pred>] [frontier <pred>] [follow <dest> [{ … }], …]
|
|
22
|
-
[order by <expr> [asc|desc], …] [limit N] [offset N]
|
|
22
|
+
[group by <key>, …] [having <pred>] [order by <expr> [asc|desc], …] [limit N] [offset N]
|
|
23
23
|
<target> count|exists|none|first|single { <block> } (scalar/one-row form: a bare target at the root scope)
|
|
24
24
|
|
|
25
|
-
CLAUSE ORDER IS FIXED: select, from, where, frontier, follow, order by, limit, offset —
|
|
25
|
+
CLAUSE ORDER IS FIXED: select, from, where, frontier, follow, group by, having, order by, limit, offset —
|
|
26
26
|
each at most once; an out-of-order clause is a parse error naming the order.
|
|
27
27
|
Only \`select\` may drop its keyword, and only as the first clause
|
|
28
28
|
(\`path, era from docs where …\`). A top-level predicate needs \`where\`, because
|
|
@@ -113,7 +113,7 @@ everywhere: a hit's \`id\` is docs_read's \`id\` and the \`id\` argument of bloc
|
|
|
113
113
|
blocks id path doc_id raw hash updated_at type text attrs depth ordinal | block doc | children nodes out_links section
|
|
114
114
|
nodes id path doc_id block_id kind name value attrs | section doc block | blocks subsections children
|
|
115
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
|
|
116
|
+
revisions id path rev seq doc_id commit ts origin actor reason hash is_current archived | doc
|
|
117
117
|
A key of yours with any of these names is \`data.<k>\` (or frontmatter.<k> /
|
|
118
118
|
inline.<k> by source; attrs.<k> on a block or node).
|
|
119
119
|
UNKNOWN NAMES: a name that is none of the above AND that no object of the
|
|
@@ -195,7 +195,9 @@ revisions: (history as rows — one per stored revision of every document of th
|
|
|
195
195
|
ts (the commit's time, sorts chronologically), origin
|
|
196
196
|
("observed"/"api"/"import"/"projection"), actor, reason,
|
|
197
197
|
hash (the rendered file's sha256 — docs_history's hash),
|
|
198
|
-
is_current (true for the document's current revision)
|
|
198
|
+
is_current (true for the document's current revision),
|
|
199
|
+
archived (true when the bodies moved to an archive segment:
|
|
200
|
+
the row stays, docs_read { at } reads through the segment).
|
|
199
201
|
- attributes = id (the revision id, r_…), path (the path the revision
|
|
200
202
|
was committed under — a move re-paths only the current
|
|
201
203
|
revision, so earlier ones keep the old path). doc.<key>
|
|
@@ -261,9 +263,24 @@ revisions: (history as rows — one per stored revision of every document of th
|
|
|
261
263
|
Unanchored unless the pattern anchors; an absent x is false
|
|
262
264
|
under ~ and TRUE under !~ (not SQL's NULL).
|
|
263
265
|
functions has(f) size(x) list(x) entries(x) range(s)
|
|
266
|
+
dates a date is an ISO string, a duration a NUMBER OF SECONDS; arithmetic
|
|
267
|
+
is METHODS, never + or - (+ concatenates strings).
|
|
268
|
+
date("today"|"now"|"yesterday"|"tomorrow"|"sow"|"eow"|"som"|"eom"|
|
|
269
|
+
"soy"|"eoy") — the host clock, UTC; date("2026-10-15") canonical ISO
|
|
270
|
+
(absent if unparseable). dur("1h30m") dur("2 days, 3 hours")
|
|
271
|
+
dur("P1DT2H") dur(90) → seconds (year = 365 d, month = 30 d).
|
|
272
|
+
d.plus(dur("30 days")) d.minus(86400) d.plus("1 month") (calendar:
|
|
273
|
+
Jan 31 → Feb 28) a.until(b) → signed seconds d.year() d.month()
|
|
274
|
+
d.day() d.weekday() (1 = Mon) d.hour() d.minute() d.second()
|
|
275
|
+
d.date() d.format("EEE dd MMM yyyy") n.dur_text() → "1h 30m"
|
|
276
|
+
n.dur_iso() → "PT1H30M". Total: a non-date is absent, so
|
|
277
|
+
\`where date(due) < date("today").minus(dur("30 days"))\` just
|
|
278
|
+
filters. A datetime is never < its own date: compare
|
|
279
|
+
updated_at.date() == date("today"), or < date("tomorrow").
|
|
264
280
|
literals "str" 'str' 123 1.5 true false null
|
|
265
|
-
determinism no
|
|
266
|
-
not
|
|
281
|
+
determinism no random; the only clock is date("today"/"now"/…), read once per
|
|
282
|
+
query. Lists/structs are not literals; ternary is not supported.
|
|
283
|
+
Errors surface as filter_invalid.
|
|
267
284
|
scalars vs lists a list-authored key never == a scalar (tags == "x" is false
|
|
268
285
|
when tags is a list) — use \`"x" in tags\` / \`in list(tags)\`.
|
|
269
286
|
|
|
@@ -454,6 +471,7 @@ text()/where prune the candidate set; only order by reweights.
|
|
|
454
471
|
select seq, origin, ts, hash from revisions where path == "/processes/calcination.md" order by seq desc
|
|
455
472
|
from docs where revisions count { where origin == "api" } > 0
|
|
456
473
|
select p: path, d: doc.path from revisions where doc == null
|
|
474
|
+
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
|
|
457
475
|
|
|
458
476
|
Common mistake: \`$path\`/\`$id\` (the 2.x spelling) — omgbase has no $ names; write
|
|
459
477
|
\`path\`/\`id\`. Another: \`where tags == "x"\` on a list-valued key is false — write
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -1202,7 +1202,7 @@ export function buildServer(ctx) {
|
|
|
1202
1202
|
}
|
|
1203
1203
|
});
|
|
1204
1204
|
tool("docs_read_at", {
|
|
1205
|
-
description: "TIME-TRAVEL read: the whole file bytes of a document AS OF a past revision — `raw` is the reconstructed source at that `rev` (fences/tables/list markers preserved), plus `id`/`path` (the path as of that revision)/`rev`. Sugar for `docs_read { id | path, at: rev }` (surface 4.7) — the same result, byte for byte. Use this to audit or answer 'what did this doc say at revision N', or to fetch the exact prior text before reverting. Get a `rev` from `query` over the `revisions` target (`from revisions where path == … order by seq`), docs_diff, blocks_history, docs_history or changes_since. Contrast: docs_read = current bytes; docs_diff = block-grain changes BETWEEN two revisions. `renderedHashMatch` is true when the reconstruction is byte-for-byte the file at that revision; it can be false for an old revision ONLY when the document's leading/frontmatter trivia (separator bytes, not block content) changed since — block content always reconstructs faithfully. No `properties`: the property table is current-state only (through 4.6 the current bag rode along flagged propertiesAreCurrent). Address the document by `id` or `path` plus a `rev` id.",
|
|
1205
|
+
description: "TIME-TRAVEL read: the whole file bytes of a document AS OF a past revision — `raw` is the reconstructed source at that `rev` (fences/tables/list markers preserved), plus `id`/`path` (the path as of that revision)/`rev`. Sugar for `docs_read { id | path, at: rev }` (surface 4.7) — the same result, byte for byte. Use this to audit or answer 'what did this doc say at revision N', or to fetch the exact prior text before reverting. Get a `rev` from `query` over the `revisions` target (`from revisions where path == … order by seq`), docs_diff, blocks_history, docs_history or changes_since. Contrast: docs_read = current bytes; docs_diff = block-grain changes BETWEEN two revisions. `renderedHashMatch` is true when the reconstruction is byte-for-byte the file at that revision; it can be false for an old revision ONLY when the document's leading/frontmatter trivia (separator bytes, not block content) changed since — block content always reconstructs faithfully. No `properties`: the property table is current-state only (through 4.6 the current bag rode along flagged propertiesAreCurrent). Address the document by `id` or `path` plus a `rev` id. A revision whose bodies were moved to an archive segment (`archived` in docs_history / the revisions target) reads through the segment when it is attached; otherwise the call fails `revision_archived` with `data: { rev, archive, path? }` — an operator attaches the file with `omg history attach`.",
|
|
1206
1206
|
inputSchema: { ...DOC_ARGS, rev: z.string(), ...REPO_ARG },
|
|
1207
1207
|
}, async (args) => {
|
|
1208
1208
|
try {
|
|
@@ -1215,7 +1215,7 @@ export function buildServer(ctx) {
|
|
|
1215
1215
|
}
|
|
1216
1216
|
});
|
|
1217
1217
|
tool("docs_history", {
|
|
1218
|
-
description: "VERSION HISTORY of matching documents, grouped BY DOCUMENT: for each doc (`id`, `path`, `deleted`, `currentRev`), its ordered revision list (oldest→newest) with `rev`/`seq`/`commit`/`updated_at`/`origin`/`actor`/`hash`/`isCurrent
|
|
1218
|
+
description: "VERSION HISTORY of matching documents, grouped BY DOCUMENT: for each doc (`id`, `path`, `deleted`, `currentRev`), its ordered revision list (oldest→newest) with `rev`/`seq`/`commit`/`updated_at`/`origin`/`actor`/`hash`/`isCurrent`/`archived` (true when the revision's bodies live in an archive segment — the row stays, docs_read_at reads through the segment while it is attached). Give `path_glob` (e.g. \"journal/*\" — `*` matches ACROSS `/`, so journal/* and journal/** are equivalent) OR a single document by `id` or `path`; at least one is required. `hash` (hex of the revision's rendered file hash) lets you spot no-op vs real changes. Feed a returned `rev` into `docs_read_at` (whole doc AT that rev) or `docs_diff` (block-grain changes BETWEEN two revs). By default only live docs; `include_deleted:true` also returns tombstoned docs (their history is intact for audit). `limit` caps DOCUMENTS returned (default 50) with a `truncated` flag. Contrast `changes_since`: a repo-wide COMMIT feed, not this per-document view.",
|
|
1219
1219
|
inputSchema: {
|
|
1220
1220
|
path_glob: z.string().optional(),
|
|
1221
1221
|
...DOC_ARGS,
|
|
@@ -23,6 +23,7 @@ import { sanitizeFtsQuery } from "../search/fts-query.js";
|
|
|
23
23
|
import { cosineBytes } from "../core/vec.js";
|
|
24
24
|
import { ctxHashHex } from "../search/embeddings.js";
|
|
25
25
|
import { makeBlockContextResolver } from "../search/tasks.js";
|
|
26
|
+
import { dateFunction, dateMethod } from "./dates.js";
|
|
26
27
|
const TARGET = Symbol("oqx.target");
|
|
27
28
|
const ROOT_ROW = Symbol("oqx.rootRow");
|
|
28
29
|
const PROP_SOURCE = Symbol("oqx.propSource");
|
|
@@ -65,8 +66,10 @@ const NODE_ATTRIBUTES = new Set(["id", "path", "doc_id", "block_id", "kind", "na
|
|
|
65
66
|
const EDGE_ATTRIBUTES = new Set(["id", "path", "doc_id", "block_id", "via_id", "dst_id", "dst_path", "uri", "field", "dst_kind", "predicate", "provenance", "anchor"]);
|
|
66
67
|
// The history target (spec/surface §1.2, 4.7): a revision row's own columns
|
|
67
68
|
// under the names `docs_history` already uses (`rev`, `seq`, `commit`, `origin`,
|
|
68
|
-
// `actor`, `hash`), the commit's `ts` and `reason`, and
|
|
69
|
-
|
|
69
|
+
// `actor`, `hash`), the commit's `ts` and `reason`, `is_current`, and (4.9)
|
|
70
|
+
// `archived` — the revision's bodies live in an archive segment (spec/store §7
|
|
71
|
+
// "Retention"; `docs_read { at }` reads through it when attached).
|
|
72
|
+
const REVISION_ATTRIBUTES = new Set(["id", "path", "rev", "seq", "doc_id", "commit", "ts", "origin", "actor", "reason", "hash", "is_current", "archived"]);
|
|
70
73
|
/** The engine attributes per kind (spec/surface §1.2 table). Exported so the
|
|
71
74
|
* translator's tests pin the two against each other. */
|
|
72
75
|
export const ATTRIBUTES = {
|
|
@@ -78,7 +81,7 @@ export const ATTRIBUTES = {
|
|
|
78
81
|
// `revisions` table carries no `repo_id`, so the repository guard joins `docs`.
|
|
79
82
|
// `is_current` is `docs_history`'s `isCurrent`: the revision is the document's
|
|
80
83
|
// `current_rev`.
|
|
81
|
-
const REVISION_COLS = "r.rev_id, r.doc_id, r.seq, r.rendered_hash, r.path, r.commit_id, c.ts, c.origin, c.actor, c.reason, (d.current_rev = r.rev_id) AS is_current";
|
|
84
|
+
const REVISION_COLS = "r.rev_id, r.doc_id, r.seq, r.rendered_hash, r.path, r.commit_id, c.ts, c.origin, c.actor, c.reason, (d.current_rev = r.rev_id) AS is_current, (r.archive IS NOT NULL) AS archived";
|
|
82
85
|
const REVISION_FROM = "revisions r JOIN docs d ON d.doc_id = r.doc_id JOIN commits c ON c.commit_id = r.commit_id";
|
|
83
86
|
/** The source handles of a document (§1.2): `data` is the merged authored view
|
|
84
87
|
* (frontmatter + inline), the other two one source each. */
|
|
@@ -537,6 +540,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
537
540
|
case "reason": return row.reason ?? null;
|
|
538
541
|
case "hash": return hex(row.rendered_hash);
|
|
539
542
|
case "is_current": return row.is_current === 1 || row.is_current === true;
|
|
543
|
+
case "archived": return row.archived === 1 || row.archived === true;
|
|
540
544
|
}
|
|
541
545
|
return undefined;
|
|
542
546
|
};
|
|
@@ -888,6 +892,13 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
888
892
|
}
|
|
889
893
|
if (name === "refs")
|
|
890
894
|
return { handled: true, value: refs(args[0]) };
|
|
895
|
+
// `date(x)` / `dur(x)` (spec/surface §1.3 "Dates and durations", 4.8):
|
|
896
|
+
// pure over their argument except the named points, which read the host
|
|
897
|
+
// clock (pinned by the spec clock seam). run.ts folds a literal argument
|
|
898
|
+
// before planning, so the arm here serves the non-literal ones.
|
|
899
|
+
const dated = dateFunction(name, args);
|
|
900
|
+
if (dated.handled)
|
|
901
|
+
return dated;
|
|
891
902
|
if (name === "range") {
|
|
892
903
|
const s = args[0];
|
|
893
904
|
if (semantics.isRange(s))
|
|
@@ -898,12 +909,17 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
898
909
|
const fn = semantics.BUILTIN_FUNCTIONS[name];
|
|
899
910
|
return fn ? { handled: true, value: fn(args) } : { handled: false };
|
|
900
911
|
},
|
|
901
|
-
// Domain methods first
|
|
902
|
-
//
|
|
912
|
+
// Domain methods first, then the date methods (`plus`/`minus`/`until`/the
|
|
913
|
+
// fields/`format` on a date string, `dur_text`/`dur_iso` on a number —
|
|
914
|
+
// §1.3 4.8; total: any other receiver is absent); fall back to oqx-js
|
|
915
|
+
// builtin methods (contains/startsWith/endsWith/matches/size/lower/upper).
|
|
903
916
|
callMethod(name, recv, args) {
|
|
904
917
|
const r = method(name, recv, args);
|
|
905
918
|
if (r.handled)
|
|
906
919
|
return r;
|
|
920
|
+
const dated = dateMethod(name, recv, args);
|
|
921
|
+
if (dated.handled)
|
|
922
|
+
return dated;
|
|
907
923
|
const fn = semantics.BUILTIN_METHODS[name];
|
|
908
924
|
return fn ? { handled: true, value: fn(recv, args) } : { handled: false };
|
|
909
925
|
},
|