@omgbase/core 0.19.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/ingest.d.ts +2 -0
- package/dist/src/core/ingest.js +13 -0
- package/dist/src/core/read/document.d.ts +13 -9
- package/dist/src/core/read/document.js +18 -8
- 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 +37 -4
- package/dist/src/core/store/gc.js +62 -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 +24 -2
- package/dist/src/core/store/size.js +33 -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/arguments.d.ts +10 -2
- package/dist/src/mcp/arguments.js +37 -3
- 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 +58 -8
- package/dist/src/mcp/server.js +51 -24
- package/dist/src/mutate/effects.d.ts +23 -5
- package/dist/src/mutate/effects.js +111 -22
- package/dist/src/mutate/known-ids.d.ts +19 -0
- package/dist/src/mutate/known-ids.js +47 -7
- package/dist/src/mutate/ops.d.ts +1 -0
- package/dist/src/mutate/ops.js +20 -1
- package/dist/src/mutate/select.d.ts +0 -11
- package/dist/src/mutate/select.js +33 -9
- package/dist/src/oqx-js/context.d.ts +9 -2
- package/dist/src/oqx-js/context.js +92 -30
- 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.d.ts +40 -0
- package/dist/src/oqx-js/names.js +358 -0
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +30 -11
- package/dist/src/oqx-js/root-row.d.ts +15 -4
- package/dist/src/oqx-js/root-row.js +18 -10
- package/dist/src/oqx-js/run.d.ts +19 -1
- package/dist/src/oqx-js/run.js +97 -13
- package/dist/src/oqx-js/sql/translate.js +1 -1
- package/dist/src/spec-versions.d.ts +5 -5
- package/dist/src/spec-versions.js +4 -4
- package/dist/src/sync/observe.js +9 -3
- package/package.json +2 -2
|
@@ -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
|
|
@@ -109,12 +109,19 @@ read a bare non-colliding key performs), \`frontmatter.<k>\`, \`inline.<k>\`. So
|
|
|
109
109
|
everywhere: a hit's \`id\` is docs_read's \`id\` and the \`id\` argument of blocks_read.
|
|
110
110
|
|
|
111
111
|
Reserved bare names per kind (engine attributes; handles; self; relations):
|
|
112
|
-
docs id path raw hash updated_at title tags format | data frontmatter inline | doc | blocks nodes
|
|
112
|
+
docs id path raw hash updated_at title tags format | data frontmatter inline | doc | blocks nodes out_links in_links revisions
|
|
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 archived | doc
|
|
116
117
|
A key of yours with any of these names is \`data.<k>\` (or frontmatter.<k> /
|
|
117
118
|
inline.<k> by source; attrs.<k> on a block or node).
|
|
119
|
+
UNKNOWN NAMES: a name that is none of the above AND that no object of the
|
|
120
|
+
kind in this repository carries is reported before any row is read — on
|
|
121
|
+
links as a filter_invalid error naming the fields; on docs/blocks/nodes the
|
|
122
|
+
result gains \`hints\` (one line per name, nearest match when there is one)
|
|
123
|
+
and the rows still come back; in a plan/apply/execute selector it is refused.
|
|
124
|
+
A property some objects carry is known (partial presence is normal).
|
|
118
125
|
|
|
119
126
|
docs:
|
|
120
127
|
- bare identifier = a document PROPERTY (frontmatter + inline \`key:: value\`,
|
|
@@ -136,7 +143,8 @@ docs:
|
|
|
136
143
|
links as rows; the documents at their other end are
|
|
137
144
|
out_links collect distinct { from dst } and
|
|
138
145
|
in_links collect distinct { from doc } — there is no
|
|
139
|
-
out / in relation)
|
|
146
|
+
out / in relation), revisions (the document's stored
|
|
147
|
+
revisions, oldest first — see revisions below).
|
|
140
148
|
|
|
141
149
|
blocks:
|
|
142
150
|
- bare fields = doc_id (the owning doc), type, text (normalized), ordinal
|
|
@@ -181,20 +189,39 @@ links: (the authored link graph as rows — one per open link: the relation a l
|
|
|
181
189
|
open edges only (history is blocks_history/changes_since).
|
|
182
190
|
- text()/semantic() are N/A on links.
|
|
183
191
|
|
|
192
|
+
revisions: (history as rows — one per stored revision of every document of the
|
|
193
|
+
repository, deleted documents included: what docs_history lists, queryable)
|
|
194
|
+
- bare fields = rev (= id), seq (1, 2, … per document), doc_id, commit,
|
|
195
|
+
ts (the commit's time, sorts chronologically), origin
|
|
196
|
+
("observed"/"api"/"import"/"projection"), actor, reason,
|
|
197
|
+
hash (the rendered file's sha256 — docs_history's hash),
|
|
198
|
+
is_current (true for the document's current revision),
|
|
199
|
+
archived (true when the bodies moved to an archive segment:
|
|
200
|
+
the row stays, docs_read { at } reads through the segment).
|
|
201
|
+
- attributes = id (the revision id, r_…), path (the path the revision
|
|
202
|
+
was committed under — a move re-paths only the current
|
|
203
|
+
revision, so earlier ones keep the old path). doc.<key>
|
|
204
|
+
reaches the document's CURRENT row; absent for a deleted
|
|
205
|
+
document, so where doc == null is the revisions of deleted documents.
|
|
206
|
+
- on docs = revisions is a relation (oldest first): a document with
|
|
207
|
+
more than one version is docs { revisions count { } > 1 }.
|
|
208
|
+
- text()/semantic() are N/A on revisions. The body as of a revision is
|
|
209
|
+
docs_read { path, at: "<rev>" } (docs_read_at is the same call).
|
|
210
|
+
|
|
184
211
|
## Scoping (ADR-015; the root row since surface 2.0): bare names are LOCAL
|
|
185
212
|
|
|
186
213
|
name the CURRENT row only — never falls through to an enclosing row
|
|
187
214
|
or the repository; an absent field is simply absent.
|
|
188
215
|
^name one scope OUT (the enclosing row's fields/intrinsics/lifts);
|
|
189
216
|
^^name two scopes out. \`^path\` = the enclosing row's path.
|
|
190
|
-
^docs ^nodes ^blocks ^links
|
|
217
|
+
^docs ^nodes ^blocks ^links ^revisions
|
|
191
218
|
THE ROOT ROW (surface 2.0, oqx 0.18): the repository is the root
|
|
192
219
|
scope's row, one scope out from a top-level row, so ^docs there is
|
|
193
220
|
the whole documents scan (uncorrelated until you add a ^ predicate);
|
|
194
221
|
one caret per enclosing block (^^docs from depth two), or the
|
|
195
222
|
absolute 0^docs from ANY depth. ^id / 0^id = the repository id.
|
|
196
223
|
^$it = the root object: ^$it.docs ≡ ^docs, entries(^$it) names the
|
|
197
|
-
|
|
224
|
+
five collections. At the root scope itself a bare target is the scan
|
|
198
225
|
(docs count { … }, from docs, entries(docs)).
|
|
199
226
|
ERRORS: a bare \`docs\` INSIDE a block reads a property of the current
|
|
200
227
|
row, which has none — refused ("did you mean ^docs"); \`^docs\` AT THE
|
|
@@ -231,10 +258,29 @@ links: (the authored link graph as rows — one per open link: the relation a l
|
|
|
231
258
|
strings x.contains("s") x.startsWith("p") x.endsWith("s") x.matches("^re$")
|
|
232
259
|
(real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE —
|
|
233
260
|
fold with .lower(): title.lower().contains("aurora").
|
|
261
|
+
Infix regex: x ~ "re" is x.matches("re"); x ~* "re" is the
|
|
262
|
+
case-insensitive match; x !~ "re" and x !~* "re" negate them.
|
|
263
|
+
Unanchored unless the pattern anchors; an absent x is false
|
|
264
|
+
under ~ and TRUE under !~ (not SQL's NULL).
|
|
234
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").
|
|
235
280
|
literals "str" 'str' 123 1.5 true false null
|
|
236
|
-
determinism no
|
|
237
|
-
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.
|
|
238
284
|
scalars vs lists a list-authored key never == a scalar (tags == "x" is false
|
|
239
285
|
when tags is a list) — use \`"x" in tags\` / \`in list(tags)\`.
|
|
240
286
|
|
|
@@ -422,6 +468,10 @@ text()/where prune the candidate set; only order by reweights.
|
|
|
422
468
|
from blocks order by semantic("identity preservation across edits") desc limit 10
|
|
423
469
|
select uri from links where dst_kind == "external"
|
|
424
470
|
docs count { where layer == "canon" }
|
|
471
|
+
select seq, origin, ts, hash from revisions where path == "/processes/calcination.md" order by seq desc
|
|
472
|
+
from docs where revisions count { where origin == "api" } > 0
|
|
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
|
|
425
475
|
|
|
426
476
|
Common mistake: \`$path\`/\`$id\` (the 2.x spelling) — omgbase has no $ names; write
|
|
427
477
|
\`path\`/\`id\`. Another: \`where tags == "x"\` on a list-valued key is false — write
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -12,7 +12,7 @@ import { oqxRunAsync, collectSemanticPhrases } from "../oqx/run.js";
|
|
|
12
12
|
import { graphNeighborhood } from "./graph.js";
|
|
13
13
|
import { textSearch } from "../search/text.js";
|
|
14
14
|
import { EngineError, errorBody } from "./errors.js";
|
|
15
|
-
import { argumentError, validateArguments } from "./arguments.js";
|
|
15
|
+
import { argumentError, stringList, validateArguments } from "./arguments.js";
|
|
16
16
|
import { linksRetarget, linksRepair } from "../mutate/macros.js";
|
|
17
17
|
import { canonicalDocPath } from "../mutate/docs.js";
|
|
18
18
|
import { applyEffects, executeByDigest, executePlan, planEffects } from "../mutate/plan.js";
|
|
@@ -84,7 +84,7 @@ const selectionExpectSchema = z.object({ max: z.number().int().nonnegative() });
|
|
|
84
84
|
function selectorArgs(args) {
|
|
85
85
|
return { ...(args.select !== undefined ? { select: args.select } : {}), ...(args.expect !== undefined ? { expect: args.expect } : {}) };
|
|
86
86
|
}
|
|
87
|
-
const effectSchema = z.looseObject({ op: z.enum(["create", "insert", "set", "unset", "move", "remove", "split", "merge", "retarget"]) }).describe("one effect: { op: \"create\", path, markdown, frontmatter? } | { op: \"insert\", into: Target, at?, markdown, expect? } | { op: \"set\", target, field, value, expect? } | { op: \"unset\", target, field, expect? } | { op: \"move\", target: document, to, retarget_inbound? } | { op: \"move\", targets: [block…], into, at?, expect? } | { op: \"remove\", target: document } | { op: \"remove\", targets: [block…], expect? } | { op: \"split\", target: block, at: [byte…], expect? } | { op: \"merge\", targets: [block…], separator?, expect? } | { op: \"retarget\", repairs: [{ from, to }], path_glob? }
|
|
87
|
+
const effectSchema = z.looseObject({ op: z.enum(["create", "insert", "set", "unset", "move", "remove", "split", "merge", "retarget"]) }).describe("one effect: { op: \"create\", path, markdown, frontmatter? } | { op: \"insert\", into: Target, at?, markdown, expect? } | { op: \"set\", target, field, value, expect? } | { op: \"unset\", target: document, field: \"frontmatter.<key>\", expect? } | { op: \"move\", target: document, to, retarget_inbound? } | { op: \"move\", targets: [block…], into, at?, expect? } | { op: \"remove\", target: document } | { op: \"remove\", targets: [block…], expect? } | { op: \"split\", target: block, at: [byte…], expect? } | { op: \"merge\", targets: [block…], separator?, expect? } | { op: \"retarget\", repairs: [{ from, to }], path_glob? }. Target: a bare document path, a block/node id, $new.<n> (an identity an earlier effect creates), or { kind: \"document\", id? | path? } | { kind: \"block\", id } | { kind: \"node\", id } | { kind: \"section\", heading_id }. at: \"start\" | \"end\" | { before: <block id> } | { after: <block id> } — a place among the children of `into`, so \"right after block X\" is into: X's parent (its parent block id, or the document path when X is top-level), at: { after: X }. field: a block's is \"markdown\" or \"attrs.checked\"; a document's \"frontmatter.<key>\" or (set only) \"content\"; a node's an editable property (value, name, checked). expect on a single-target op: { content_hash?, parent_children_hash?, revision? }; on remove/merge targets it is keyed by block id: { <block id>: { content_hash?, parent_children_hash? } }. expect: { max } is the request's top-level argument beside select, never inside an effect.");
|
|
88
88
|
// An example ladder (spec/surface §4 "Descriptions", 4.3): the description of
|
|
89
89
|
// each `MINIMAL_TOOLS` tool is a one-line summary, then one example per line —
|
|
90
90
|
// a rung is a line beginning `{` with the call's arguments as JSON, three
|
|
@@ -99,6 +99,10 @@ const effectSchema = z.looseObject({ op: z.enum(["create", "insert", "set", "uns
|
|
|
99
99
|
// thing; the plans they produce equal the 4.3 object-form rungs' (pinned by
|
|
100
100
|
// `mcp/examples.test.ts`).
|
|
101
101
|
const ladder = (...lines) => lines.join("\n");
|
|
102
|
+
// The `docs_read` ladder's time-travel rung (4.7): the revision id is the one
|
|
103
|
+
// the spec minter gives `/processes/calcination.md`'s ingest revision over the
|
|
104
|
+
// alchemy corpus (`examples.json` runs the rung before any write).
|
|
105
|
+
const REVISION_RUNG = "{\"path\":\"/processes/calcination.md\",\"at\":\"r_7\"} as of a revision (rev ids: from revisions where path == \"/processes/calcination.md\")";
|
|
102
106
|
function ok(payload) {
|
|
103
107
|
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
104
108
|
}
|
|
@@ -236,6 +240,15 @@ export function buildServer(ctx) {
|
|
|
236
240
|
}
|
|
237
241
|
return (byId ?? byPath).docId;
|
|
238
242
|
}
|
|
243
|
+
// `docs_read { at }` and its sugar `docs_read_at` (spec/surface §2, 4.7): the
|
|
244
|
+
// document AS OF a revision, `spec/store` §6.2 reconstruct; a revision that is
|
|
245
|
+
// not the document's is `target_missing`.
|
|
246
|
+
function docsReadAt(docId, rev) {
|
|
247
|
+
const res = readDocumentAtRevision(store, docId, rev);
|
|
248
|
+
if (!res)
|
|
249
|
+
throw new EngineError("target_missing", `no revision ${JSON.stringify(rev)} for document ${docId}`, { data: { doc: docId, rev } });
|
|
250
|
+
return res;
|
|
251
|
+
}
|
|
239
252
|
// The owning document of a block id (`blocks_read`): the block must be live.
|
|
240
253
|
function docOfBlock(blockId) {
|
|
241
254
|
const row = store.db.prepare("SELECT doc_id FROM blocks WHERE block_id = ? AND deleted_commit IS NULL").get(blockId);
|
|
@@ -415,16 +428,26 @@ export function buildServer(ctx) {
|
|
|
415
428
|
}
|
|
416
429
|
});
|
|
417
430
|
tool("docs_read", {
|
|
418
|
-
description: ladder("docs_read — One document, whole: raw bytes plus properties { frontmatter, inline, computed } and id/path/rev.", "{\"path\":\"/processes/calcination.md\"} by path (either form)", "{\"path\":\"/processes/calcination.md\",\"include_ids\":true} plus ids (pre-order), hashes (the CAS tokens expect.content_hash takes), parents (null = top level)", "{\"id\":\"d_14\"} by id (a query hit's id)", "Structure only: query md:section nodes. Many documents: a query with select raw. Grammar: query_syntax."),
|
|
431
|
+
description: ladder("docs_read — One document, whole: raw bytes plus properties { frontmatter, inline, computed } and id/path/rev.", "{\"path\":\"/processes/calcination.md\"} by path (either form)", "{\"path\":\"/processes/calcination.md\",\"include_ids\":true} plus ids (pre-order), hashes (the CAS tokens expect.content_hash takes), parents (null = top level)", "{\"id\":\"d_14\"} by id (a query hit's id)", REVISION_RUNG, "Structure only: query md:section nodes. Many documents: a query with select raw. Grammar: query_syntax."),
|
|
419
432
|
inputSchema: {
|
|
420
433
|
...DOC_ARGS,
|
|
421
434
|
include_ids: z.boolean().optional(),
|
|
435
|
+
at: z.string().optional(),
|
|
422
436
|
...REPO_ARG,
|
|
423
437
|
},
|
|
424
438
|
}, async (args) => {
|
|
425
439
|
try {
|
|
426
440
|
const { repoId } = repoScope(args.repo);
|
|
427
|
-
const
|
|
441
|
+
const shape = registry.get("docs_read").shape;
|
|
442
|
+
const docId = locateDoc(repoId, "docs_read", shape, args);
|
|
443
|
+
if (args.at !== undefined) {
|
|
444
|
+
// `at` (4.7): the document AS OF that revision — `docs_read_at`'s
|
|
445
|
+
// code path, so the two return the same bytes. Block ids at a past
|
|
446
|
+
// revision are a later question: `include_ids` is refused with `at`.
|
|
447
|
+
if (args.include_ids)
|
|
448
|
+
throw argumentError("docs_read", "`include_ids` is not available with `at`", shape);
|
|
449
|
+
return ok(docsReadAt(docId, args.at));
|
|
450
|
+
}
|
|
428
451
|
const res = docsRead(store, docId, args.include_ids ? { includeIds: true } : {});
|
|
429
452
|
if (!res)
|
|
430
453
|
throw new EngineError("doc_missing", `no document with id or path ${JSON.stringify(args.id ?? args.path)}`, { data: { id: docId } });
|
|
@@ -437,7 +460,7 @@ export function buildServer(ctx) {
|
|
|
437
460
|
tool("docs_read_many", {
|
|
438
461
|
description: "Batch whole-document read — the hydrate half of query→hydrate. The plural of docs_read: pass `docs`, a list of refs (each a document id OR a path, in either form), and get back one full read per ref. Returns `{ items, errors, truncated }`: each found doc is a full docs_read projection (`raw` = complete file bytes verbatim, `properties` grouped by source, plus `id`/`path`/`rev`, and — with include_ids:true — the document's ordered block `ids`, a `hashes` {block `id` → block `hash`} map for raw-`apply` CAS pinning, and a `parents` {block id → parent block id | null} map so top-level vs nested blocks are distinguishable without an outline read); a ref that resolves to no live document lands in `errors` as {ref, error:\"doc_not_found\"} WITHOUT failing the call, so one bad ref never sinks the batch. Duplicate refs collapse first-seen (a repeated ref yields a single item). Capped at " + MANY_DOCS_CAP + " refs per call; excess refs are dropped and `truncated` is set. Pass budget_tokens to cap total hydrated size — the batch stops early and flags `truncated` when the next doc would exceed it; the FIRST doc always ships even when it alone exceeds the budget (the same at-least-one-row contract as docs_list/docs_tree, so a budgeted caller always makes progress). Use this after query/blocks_search/blocks_resolve to pull N whole docs in ONE round-trip instead of N serial docs_read calls; for a single doc use docs_read, and for lean structure-only orientation use docs_outline (docs_get_many before surface 3.0).",
|
|
439
462
|
inputSchema: {
|
|
440
|
-
docs:
|
|
463
|
+
docs: stringList(),
|
|
441
464
|
include_ids: z.boolean().optional(),
|
|
442
465
|
budget_tokens: z.number().int().optional(),
|
|
443
466
|
...REPO_ARG,
|
|
@@ -502,7 +525,7 @@ export function buildServer(ctx) {
|
|
|
502
525
|
tool("blocks_read_many", {
|
|
503
526
|
description: "Fetch up to 100 blocks by id (in request order) with budget truncation — the plural of blocks_read (nodes_get_many before surface 3.0). Block ids are globally unique, so `ids` may span ANY number of documents — each id is resolved to its owning doc server-side. Result carries `nodes` (the hydrated blocks, one per resolved id), `truncated` (cap or `budget_tokens` hit — the first resolved block always ships even when it alone exceeds the budget, so a budgeted caller always makes progress), and `unresolved` — the requested ids that name no live block (never silently dropped).",
|
|
504
527
|
inputSchema: {
|
|
505
|
-
ids:
|
|
528
|
+
ids: stringList(),
|
|
506
529
|
resolution: z.enum(["skeleton", "outline", "text", "raw", "full"]).optional(),
|
|
507
530
|
budget_tokens: z.number().int().optional(),
|
|
508
531
|
...REPO_ARG,
|
|
@@ -573,7 +596,7 @@ export function buildServer(ctx) {
|
|
|
573
596
|
inputSchema: {},
|
|
574
597
|
}, async () => ok({ syntax: QUERY_SYNTAX }));
|
|
575
598
|
tool("query", {
|
|
576
|
-
description: ladder("query —
|
|
599
|
+
description: ladder("query — rows (id, path, projection) from docs, blocks, nodes or links.", "{\"query\":\"select title, stage from docs where \\\"process\\\" in tags order by path limit 5\"} frontmatter; membership; order, limit", "{\"query\":\"select type, text from blocks where path == \\\"/processes/calcination.md\\\"\"} a document's blocks in order", "{\"query\":\"select block_id from nodes where kind == \\\"md:section\\\" and name == \\\"Tasks\\\" and path == \\\"/processes/calcination.md\\\"\"} a heading's block id", "{\"query\":\"select path, value from nodes where kind == \\\"md:task\\\" and not checked\"} open tasks", "{\"query\":\"select path, block_id from links where dst_path == \\\"/processes/calcination.md\\\"\"} inbound links (path = source)", "{\"query\":\"select path from docs where title ~* \\\"salt\\\"\"} regex (~* ci, !~ not); substring: .contains(\\\"s\\\")", "Also docs count { where … }, follow, ^docs joins, text()/semantic(): query_syntax."),
|
|
577
600
|
inputSchema: {
|
|
578
601
|
query: z.string(),
|
|
579
602
|
limit: z.number().int().optional(),
|
|
@@ -601,11 +624,11 @@ export function buildServer(ctx) {
|
|
|
601
624
|
tool("graph", {
|
|
602
625
|
description: "Neighborhood macro: the bounded graph AROUND one or more root documents in ONE call — { documents, links, frontier } — so you can orient without hand-writing an OQX `follow`. This is a CONVENIENCE WRAPPER, not a new engine: it compiles its args into an OQX `follow out_links collect { from dst }` / `follow in_links collect { from doc }` query and runs it through the same `query` path (the returned `queries` field is the exact follow query it generated). It does NOT replace `follow` or `from links` — reach for the `query` tool directly when you need successor/frontier predicates, `$key`-keyed identity, `$ordinal` budgets, cross-document correlation, or the links scan with its own projections. `roots` are one or more doc refs (paths and/or ids); they are depth 0. `degrees` is the max hop distance (root = 0; maps to `follow … collect { … depth degrees+1 }`, capped so degrees+1 ≤ 8; default 1). `direction` is `out` (outgoing links), `in` (backlinks), or `both` (default). `predicate` restricts the neighborhood to documents reachable from a root via edges of that predicate (both the documents and the edges are filtered). `select` adds document projections (OQX select expressions, e.g. \"layer\", \"path\"). `max_documents` caps the distinct document set (default 200) with a `truncated` flag. Returns: `documents` (each with `id`/`path`/`degree` = min hops from a root/`frontier` bool + your projections — a bare-name selection such as \"title\" is its own key, `id`/`path` land on the envelope keys, and any other expression is `sel_<i>`), `links` (the traversed links as rows of the `links` target under its own field names — `id`, `doc_id`/`block_id` (the source), `via_id`, `dst_id`, `dst_path`, `uri`, `dst_kind`, `predicate`, `provenance`, `anchor`, `field`; external `x_…` and dangling phantom endpoints are preserved as edge stubs, exactly as `from links` surfaces them), and `frontier` (the documents on the outer boundary — `{ id, path, degree }`, min degree == degrees; at degrees 0 that is the roots themselves).",
|
|
603
626
|
inputSchema: {
|
|
604
|
-
roots:
|
|
627
|
+
roots: stringList({ min: 1 }),
|
|
605
628
|
degrees: z.number().int().optional(),
|
|
606
629
|
direction: z.enum(["in", "out", "both"]).optional(),
|
|
607
630
|
predicate: z.string().optional(),
|
|
608
|
-
select:
|
|
631
|
+
select: stringList().optional(),
|
|
609
632
|
max_documents: z.number().int().optional(),
|
|
610
633
|
...REPO_ARG,
|
|
611
634
|
},
|
|
@@ -678,14 +701,21 @@ export function buildServer(ctx) {
|
|
|
678
701
|
// commits exactly a plan (the plan form, or the digest form `plan_id` +
|
|
679
702
|
// `effects`); `apply { effects }` above is plan + execute in one call.
|
|
680
703
|
tool("plan", {
|
|
681
|
-
description: ladder("plan — Preview a write: the
|
|
682
|
-
inputSchema: { effects: z.array(effectSchema), select: z.string().optional(), expect: selectionExpectSchema.optional(), ...REPO_ARG },
|
|
704
|
+
description: ladder("plan — Preview a write: the Plan (summary, $new.n ids) burns no id; execute next. diffs: true for file bodies.", "{\"effects\":[{\"op\":\"set\",\"target\":\"/processes/calcination.md\",\"field\":\"frontmatter.verified\",\"value\":false}]} path = doc", "{\"effects\":[{\"op\":\"insert\",\"into\":{\"kind\":\"section\",\"heading_id\":\"b_153\"},\"markdown\":\"- [ ] Weigh the calx\"}]} in a section", "{\"effects\":[{\"op\":\"create\",\"path\":\"lab/march.md\",\"markdown\":\"# March\\n\"},{\"op\":\"insert\",\"into\":\"$new.0\",\"markdown\":\"Hi.\"}]} fill it: $new.0", "{\"select\":\"from docs where stage == \\\"nigredo\\\"\",\"effects\":[{\"op\":\"set\",\"field\":\"frontmatter.ok\",\"value\":true}]} no target: the hit", "{\"select\":\"from docs where type == \\\"lab-note\\\"\",\"effects\":[{\"op\":\"insert\",\"into\":\"-> `/processes/${subject}.md`\",\"markdown\":\"-> `[${title}](${path})`\"}]} -> reads the row", "Ops: create insert set unset move remove split merge retarget; grammar: query_syntax."),
|
|
705
|
+
inputSchema: { effects: z.array(effectSchema), select: z.string().optional(), expect: selectionExpectSchema.optional(), diffs: z.boolean().optional(), ...REPO_ARG },
|
|
683
706
|
}, async (args) => {
|
|
684
707
|
try {
|
|
685
708
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
686
709
|
if (!rootPath)
|
|
687
710
|
throw new EngineError("repo_not_found", "repo has no filesystem source; mutation disabled");
|
|
688
|
-
|
|
711
|
+
const plan = surfacePlan(planEffects(store, { repoId, rootPath, effects: args.effects, ...selectorArgs(args), ...LOCK_DIR }));
|
|
712
|
+
// (4.5) `preview.diffs` — whole before/after bodies of every touched
|
|
713
|
+
// document — only on request: `diffs: true`. The digest is over the
|
|
714
|
+
// core, never the preview, so `plan_id` is the same either way.
|
|
715
|
+
if (args.diffs === true || plan.preview === undefined)
|
|
716
|
+
return ok(plan);
|
|
717
|
+
const { diffs: _diffs, ...preview } = plan.preview;
|
|
718
|
+
return ok({ ...plan, preview });
|
|
689
719
|
}
|
|
690
720
|
catch (e) {
|
|
691
721
|
return fail(e);
|
|
@@ -762,7 +792,7 @@ export function buildServer(ctx) {
|
|
|
762
792
|
});
|
|
763
793
|
tool("blocks_move", {
|
|
764
794
|
description: "Move block(s) under a new parent at a position. `blocks` are block refs; `to` (the parent) is a block ref (a container — a leaf parent is `type_mismatch`, as for blocks_insert), or the blocks' OWN document (id or path) to move them to its top level — moving to ANOTHER document's root is not expressible (target_missing): anchor on a block in that document with `at` {before|after} instead. `at` is \"end\"/\"start\"/{before|after: <ref>}. Optional `expect.parent_children_hash` is the destination-parent CAS (as blocks_insert: the destination parent's CURRENT direct child ids joined by \",\", sha256 hex), checked once for the whole run before anything moves — `stale_expectation` with `data.current.parent_children_hash` if the destination's children changed; `content_hash` is meaningless here and not accepted. Sugar for `apply { effects: [{ op: \"move\", targets, into, at, expect? }] }` — returns the Receipt; preview with `plan`.",
|
|
765
|
-
inputSchema: { blocks:
|
|
795
|
+
inputSchema: { blocks: stringList(), to: z.string(), at: atSchema.optional(), expect: parentExpectSchema.optional(), ...REPO_ARG },
|
|
766
796
|
}, async (args) => {
|
|
767
797
|
try {
|
|
768
798
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
@@ -783,7 +813,7 @@ export function buildServer(ctx) {
|
|
|
783
813
|
});
|
|
784
814
|
tool("blocks_remove", {
|
|
785
815
|
description: "Remove block(s) (the resurrection pool catches regret). `blocks` are refs; removing a block removes its whole subtree, and a set that names both a container and some of its descendants (e.g. every id of a section straight from docs_read include_ids) is fine — it collapses to the top-most blocks. Sugar for `apply { effects: [{ op: \"remove\", targets }] }` — returns the Receipt; preview with `plan` (its `preview.results[0].removed` lists everything that would leave). To delete a whole document use docs_delete.",
|
|
786
|
-
inputSchema: { blocks:
|
|
816
|
+
inputSchema: { blocks: stringList(), ...REPO_ARG },
|
|
787
817
|
}, async (args) => {
|
|
788
818
|
try {
|
|
789
819
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
@@ -813,7 +843,7 @@ export function buildServer(ctx) {
|
|
|
813
843
|
});
|
|
814
844
|
tool("blocks_merge", {
|
|
815
845
|
description: "Merge adjacent blocks into the first, joined by `separator` (default a blank line). `blocks` are refs (≥2). Sugar for `apply { effects: [{ op: \"merge\", targets, separator? }] }` — returns the Receipt; preview with `plan`.",
|
|
816
|
-
inputSchema: { blocks:
|
|
846
|
+
inputSchema: { blocks: stringList(), separator: z.string().optional(), ...REPO_ARG },
|
|
817
847
|
}, async (args) => {
|
|
818
848
|
try {
|
|
819
849
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
@@ -828,7 +858,7 @@ export function buildServer(ctx) {
|
|
|
828
858
|
});
|
|
829
859
|
tool("blocks_check", {
|
|
830
860
|
description: "Macro: check (or uncheck with checked:false) the given task blocks. `ids` are the blocks' ids (or refs). Sugar for `apply { effects: [{ op: \"set\", target: { kind: \"block\", id }, field: \"attrs.checked\", value }, …] }`, one per block, each CAS-pinned — returns the Receipt; preview with `plan` (tasks_complete { blocks } before surface 3.0).",
|
|
831
|
-
inputSchema: { ids:
|
|
861
|
+
inputSchema: { ids: stringList(), checked: z.boolean().optional(), ...REPO_ARG },
|
|
832
862
|
}, async (args) => {
|
|
833
863
|
try {
|
|
834
864
|
const { repoId, rootPath } = repoScope(args.repo);
|
|
@@ -1056,7 +1086,7 @@ export function buildServer(ctx) {
|
|
|
1056
1086
|
});
|
|
1057
1087
|
tool("docs_set_meta", {
|
|
1058
1088
|
description: "Surgical frontmatter patch: set the given keys and/or unset named keys on the document addressed by `id` or `path`. Other frontmatter and the whole body are preserved (every block keeps its id). Sugar for `apply { effects: [{ op: \"set\", target: { kind: \"document\", id }, field: \"frontmatter.<key>\", value }, { op: \"unset\", …, field: \"frontmatter.<key>\" }, …] }`, one effect per key — returns the Receipt; at least one of `set`/`unset` is required. Preview with `plan` over those effects (`preview.diffs[path]` is the file before → after).",
|
|
1059
|
-
inputSchema: { ...DOC_ARGS, set: z.record(z.string(), z.unknown()).optional(), unset:
|
|
1089
|
+
inputSchema: { ...DOC_ARGS, set: z.record(z.string(), z.unknown()).optional(), unset: stringList().optional(), ...REPO_ARG },
|
|
1060
1090
|
}, async (args) => {
|
|
1061
1091
|
try {
|
|
1062
1092
|
const scope = repoScope(args.repo);
|
|
@@ -1127,7 +1157,7 @@ export function buildServer(ctx) {
|
|
|
1127
1157
|
}
|
|
1128
1158
|
});
|
|
1129
1159
|
tool("blocks_history", {
|
|
1130
|
-
description: "A block's biography: the commits that
|
|
1160
|
+
description: "A block's biography: the commits that CHANGED the block `id` names — inserted, edited, moved, split/merge lineage, resurrected — with disposition kind/confidence/reason, newest first. A commit that rewrote the document but left this block byte-identical in place is not listed (store 14.2: `same` verdicts are not persisted; through 14.1 every commit to the document appeared here), so the newest entry's `ts` is the block's `updated_at`; to learn whether the block was present in some other commit, diff two revisions with docs_diff (history_node before surface 3.0 — it always took a block id and returned the block's changes). Not repo-scoped: block ids are global.",
|
|
1131
1161
|
inputSchema: { id: z.string(), limit: z.number().int().optional() },
|
|
1132
1162
|
}, async (args) => {
|
|
1133
1163
|
try {
|
|
@@ -1172,23 +1202,20 @@ export function buildServer(ctx) {
|
|
|
1172
1202
|
}
|
|
1173
1203
|
});
|
|
1174
1204
|
tool("docs_read_at", {
|
|
1175
|
-
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`. 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 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. `properties
|
|
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`.",
|
|
1176
1206
|
inputSchema: { ...DOC_ARGS, rev: z.string(), ...REPO_ARG },
|
|
1177
1207
|
}, async (args) => {
|
|
1178
1208
|
try {
|
|
1179
1209
|
const { repoId } = repoScope(args.repo);
|
|
1180
1210
|
const docId = locateDoc(repoId, "docs_read_at", registry.get("docs_read_at").shape, args);
|
|
1181
|
-
|
|
1182
|
-
if (!res)
|
|
1183
|
-
throw new EngineError("target_missing", `no revision ${JSON.stringify(args.rev)} for document ${docId}`, { data: { doc: docId, rev: args.rev } });
|
|
1184
|
-
return ok(res);
|
|
1211
|
+
return ok(docsReadAt(docId, args.rev));
|
|
1185
1212
|
}
|
|
1186
1213
|
catch (e) {
|
|
1187
1214
|
return fail(e);
|
|
1188
1215
|
}
|
|
1189
1216
|
});
|
|
1190
1217
|
tool("docs_history", {
|
|
1191
|
-
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.",
|
|
1192
1219
|
inputSchema: {
|
|
1193
1220
|
path_glob: z.string().optional(),
|
|
1194
1221
|
...DOC_ARGS,
|
|
@@ -8,6 +8,7 @@ import { type LinkRepairCount, type RetargetHit } from "./macros.js";
|
|
|
8
8
|
import type { At, Expect } from "./ops.js";
|
|
9
9
|
import type { IdentityPreview } from "./opset.js";
|
|
10
10
|
import { MutationError } from "./tree.js";
|
|
11
|
+
import { type FixRequest } from "./canonical.js";
|
|
11
12
|
/** What an effect addresses. `section` is a derived range (ADR-005), never a stored id.
|
|
12
13
|
* `symbol` (3.2, §11.8 "Sugar"): a bare `"$new.n"` target whose kind — document or block — the
|
|
13
14
|
* symbol table decides when the effect is resolved; it never reaches a resolved effect. */
|
|
@@ -172,8 +173,25 @@ export type ResolvedEffect = {
|
|
|
172
173
|
}[];
|
|
173
174
|
path_glob?: string;
|
|
174
175
|
};
|
|
175
|
-
/** The argument-shape error of the surface, thrown from the planner: `filter_invalid { reason, hint: "arguments" }
|
|
176
|
-
export declare function effectInvalid(message: string): MutationError;
|
|
176
|
+
/** The argument-shape error of the surface, thrown from the planner: `filter_invalid { reason, hint: "arguments" }` — with a `fix` (3.4, §8) when the shape check can spell the corrected call. */
|
|
177
|
+
export declare function effectInvalid(message: string, fix?: string): MutationError;
|
|
178
|
+
/**
|
|
179
|
+
* What the shape check needs to spell a `fix` for the shape errors of §8
|
|
180
|
+
* (3.4): the request as given (the corrected call is that request with the
|
|
181
|
+
* offending effect rewritten — the effect at `effects[i]` AS WRITTEN, before
|
|
182
|
+
* any desugaring) and a live-row lookup for the one fix that needs the store:
|
|
183
|
+
* `parentOf(block id)` → the block's parent id (`null` at the top level) and
|
|
184
|
+
* its document's path (storage form), or `null` when no live block has the id
|
|
185
|
+
* (then there is no fix). The planner's compile (`select.ts`) supplies it; a
|
|
186
|
+
* library call without it gets the same errors with no `fix`.
|
|
187
|
+
*/
|
|
188
|
+
export interface ShapeFixContext {
|
|
189
|
+
request: FixRequest;
|
|
190
|
+
parentOf: (blockId: string) => {
|
|
191
|
+
parent: string | null;
|
|
192
|
+
path: string;
|
|
193
|
+
} | null;
|
|
194
|
+
}
|
|
177
195
|
/** The `-> ` leader (3.2, §11.8 "Sugar"): a string beginning exactly so is an OQX expression, the rest of the string. */
|
|
178
196
|
export declare const EXPRESSION_LEADER = "-> ";
|
|
179
197
|
/** A path-looking string: contains `/` or ends `.md` (the bare document target of §11.8 "Sugar"). */
|
|
@@ -191,10 +209,10 @@ export declare function isTargetId(s: string): boolean;
|
|
|
191
209
|
*/
|
|
192
210
|
export declare function bareTarget(ref: string, here: string): EffectTarget;
|
|
193
211
|
export declare function validateTarget(v: unknown, here: string, kinds: readonly EffectTarget["kind"][]): EffectTarget;
|
|
194
|
-
/** The shape check of one effect (§11.1). `i` names it in the message (`effects[i]`). */
|
|
195
|
-
export declare function validateEffect(v: unknown, i: number): Effect;
|
|
212
|
+
/** The shape check of one effect (§11.1). `i` names it in the message (`effects[i]`); `fix` (3.4) lets the shape errors of §8 carry their corrected call. */
|
|
213
|
+
export declare function validateEffect(v: unknown, i: number, fix?: ShapeFixContext): Effect;
|
|
196
214
|
/** The whole list; a non-array or an empty list is a shape error too. */
|
|
197
|
-
export declare function validateEffects(v: unknown): Effect[];
|
|
215
|
+
export declare function validateEffects(v: unknown, fix?: ShapeFixContext): Effect[];
|
|
198
216
|
export declare const FRONTMATTER_FIELD: RegExp;
|
|
199
217
|
/** The focus a conflict names: `{ kind, target, field? }`. */
|
|
200
218
|
export interface Focus {
|