@omgbase/core 0.23.0 → 0.27.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/edit-script.d.ts +15 -0
- package/dist/src/core/edit-script.js +72 -0
- package/dist/src/core/ingest.js +42 -12
- package/dist/src/core/read/document.d.ts +7 -0
- package/dist/src/core/read/document.js +38 -17
- package/dist/src/core/store/archive.d.ts +28 -0
- package/dist/src/core/store/archive.js +106 -17
- package/dist/src/core/store/delta.d.ts +131 -0
- package/dist/src/core/store/delta.js +470 -0
- package/dist/src/core/store/gc.d.ts +10 -0
- package/dist/src/core/store/gc.js +23 -7
- package/dist/src/core/store/retention.d.ts +1 -1
- package/dist/src/core/store/retention.js +3 -17
- package/dist/src/core/store/schema.d.ts +1 -1
- package/dist/src/core/store/schema.js +2 -1
- package/dist/src/core/store/store.js +7 -0
- package/dist/src/core/store/writers.d.ts +42 -6
- package/dist/src/core/store/writers.js +67 -25
- package/dist/src/format/json.js +5 -0
- package/dist/src/format/yaml.js +15 -3
- package/dist/src/graph/history.d.ts +1 -15
- package/dist/src/graph/history.js +14 -74
- package/dist/src/mcp/arguments.d.ts +11 -0
- package/dist/src/mcp/arguments.js +47 -4
- package/dist/src/mcp/errors.js +1 -1
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +29 -9
- package/dist/src/mcp/server.js +4 -2
- package/dist/src/mutate/effects.js +87 -13
- package/dist/src/mutate/frontmatter-parts.d.ts +71 -0
- package/dist/src/mutate/frontmatter-parts.js +237 -0
- package/dist/src/mutate/load.js +9 -9
- package/dist/src/mutate/select.js +14 -4
- package/dist/src/oqx-js/context.js +32 -3
- package/dist/src/oqx-js/names.js +1 -1
- package/dist/src/oqx-js/planner.js +11 -7
- package/dist/src/oqx-js/run.d.ts +8 -0
- package/dist/src/oqx-js/run.js +103 -2
- package/dist/src/oqx-js/sql/translate.js +33 -6
- package/dist/src/oqx-js/store-index.js +1 -1
- package/dist/src/search/cel/parser.d.ts +3 -1
- package/dist/src/search/cel/parser.js +4 -1
- package/dist/src/spec-versions.d.ts +5 -5
- package/dist/src/spec-versions.js +4 -4
- package/package.json +2 -2
|
@@ -114,7 +114,7 @@ export declare class ReachMarker {
|
|
|
114
114
|
private readonly treeSizes;
|
|
115
115
|
private readonly blobSizes;
|
|
116
116
|
constructor(store: Store);
|
|
117
|
-
/** Mark every tree node and blob
|
|
117
|
+
/** Mark every tree node and blob the given revisions name EXPLICITLY (spec/store §7, 16.0: a `{`-node's own entries, not its base's) into `trees`/`blobs` (hex). */
|
|
118
118
|
mark(revs: {
|
|
119
119
|
root_tree: Buffer | null;
|
|
120
120
|
frontmatter_blob: Buffer | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { referencePath } from "../paths.js";
|
|
2
|
+
import { explicitWalker } from "./gc.js";
|
|
2
3
|
export const LADDER_KEEPS = ["one per day", "one per week", "one per month", "one per year"];
|
|
3
4
|
/** The defaults (spec/store §7): a month hot, then one a day for a year, then one a month; 200 per document; api commits with a reason. */
|
|
4
5
|
export const DEFAULT_RETENTION = {
|
|
@@ -222,24 +223,9 @@ export class ReachMarker {
|
|
|
222
223
|
this.treeNode = store.db.prepare("SELECT entries FROM tree_nodes WHERE hash = ?");
|
|
223
224
|
this.blobSize = store.db.prepare("SELECT size FROM blobs WHERE hash = ?");
|
|
224
225
|
}
|
|
225
|
-
/** Mark every tree node and blob
|
|
226
|
+
/** Mark every tree node and blob the given revisions name EXPLICITLY (spec/store §7, 16.0: a `{`-node's own entries, not its base's) into `trees`/`blobs` (hex). */
|
|
226
227
|
mark(revs, trees, blobs) {
|
|
227
|
-
const walk = (hashHex) =>
|
|
228
|
-
if (trees.has(hashHex))
|
|
229
|
-
return;
|
|
230
|
-
trees.add(hashHex);
|
|
231
|
-
const node = this.treeNode.get(Buffer.from(hashHex, "hex"));
|
|
232
|
-
if (!node)
|
|
233
|
-
return;
|
|
234
|
-
this.treeSizes.set(hashHex, Buffer.byteLength(node.entries));
|
|
235
|
-
for (const [, rawHashHex, childHashHex, , , triviaHashHex] of JSON.parse(node.entries)) {
|
|
236
|
-
blobs.add(rawHashHex);
|
|
237
|
-
if (triviaHashHex)
|
|
238
|
-
blobs.add(triviaHashHex);
|
|
239
|
-
if (childHashHex)
|
|
240
|
-
walk(childHashHex);
|
|
241
|
-
}
|
|
242
|
-
};
|
|
228
|
+
const walk = explicitWalker(this.store.db, trees, blobs, (hashHex, entries) => this.treeSizes.set(hashHex, Buffer.byteLength(entries)));
|
|
243
229
|
for (const r of revs) {
|
|
244
230
|
if (r.root_tree)
|
|
245
231
|
walk(r.root_tree.toString("hex"));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SCHEMA_VERSION =
|
|
1
|
+
export declare const SCHEMA_VERSION = 16;
|
|
2
2
|
export declare const FILE_STATS_DDL = "\nCREATE TABLE IF NOT EXISTS file_stats (\n repo_id TEXT NOT NULL,\n path TEXT NOT NULL,\n mtime_ns INTEGER NOT NULL,\n size INTEGER NOT NULL,\n hash BLOB NOT NULL,\n PRIMARY KEY (repo_id, path)\n);\n";
|
|
3
3
|
export declare const PROPERTIES_DDL = "\nCREATE TABLE IF NOT EXISTS properties (\n prop_id TEXT PRIMARY KEY,\n repo_id TEXT NOT NULL,\n doc_id TEXT NOT NULL,\n block_id TEXT, -- NULL = document-scoped (frontmatter, computed)\n source TEXT NOT NULL CHECK (source IN ('frontmatter','inline','computed')),\n key TEXT NOT NULL, -- dotted, flattened: \"layer\",\"meta.owner\"; computed carry $: \"$title\"\n card TEXT NOT NULL CHECK (card IN ('scalar','list')),\n ord INTEGER NOT NULL DEFAULT 0,\n val_text TEXT,\n val_num REAL,\n val_bool INTEGER,\n val_json TEXT,\n type TEXT NOT NULL CHECK (type IN ('string','number','bool','null','json')),\n created_commit TEXT NOT NULL,\n deleted_commit TEXT\n);\nCREATE INDEX IF NOT EXISTS idx_props_doc ON properties(doc_id) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_text ON properties(repo_id, key, val_text) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_key_num ON properties(repo_id, key, val_num) WHERE deleted_commit IS NULL;\nCREATE INDEX IF NOT EXISTS idx_props_src_key ON properties(repo_id, source, key) WHERE deleted_commit IS NULL;\n";
|
|
4
4
|
export declare const SYNC_DDL = "\nCREATE TABLE IF NOT EXISTS adapters (\n name TEXT PRIMARY KEY, -- workspace-unique adapter name (e.g. \"fs\")\n command TEXT NOT NULL, -- argv[0] to spawn (e.g. \"omgbase-fs-adapter\")\n args TEXT NOT NULL DEFAULT '[]' -- fixed leading args (JSON string[])\n);\n\nCREATE TABLE IF NOT EXISTS sources (\n source_id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE, -- workspace-unique source name\n adapter TEXT NOT NULL REFERENCES adapters(name),\n config TEXT NOT NULL DEFAULT '{}', -- JSON object \u2192 rendered to flags (13 \u00A73.1)\n env TEXT NOT NULL DEFAULT '{}' -- JSON object \u2192 spawn env (secrets; 13 \u00A73.2)\n);\n\nCREATE TABLE IF NOT EXISTS attachments (\n repo_id TEXT NOT NULL REFERENCES repos(repo_id),\n source_id TEXT NOT NULL REFERENCES sources(source_id),\n PRIMARY KEY (repo_id, source_id)\n);\n\nCREATE TABLE IF NOT EXISTS sync_state (\n repo_id TEXT NOT NULL,\n source_id TEXT NOT NULL,\n path TEXT NOT NULL, -- '' reserved for the attachment-level cursor row\n revision TEXT, -- last-observed source revision for this path\n cursor TEXT, -- last poll/webhook cursor (attachment-level row)\n PRIMARY KEY (repo_id, source_id, path)\n);\n";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Full DDL — data-model §3 (durable) + §4 (derived). SQLite dialect.
|
|
2
2
|
// Kept as one string so migrations and rebuild-index can apply it verbatim.
|
|
3
3
|
// No dialect-specific SQL leaks above the store module (02 §8).
|
|
4
|
-
export const SCHEMA_VERSION =
|
|
4
|
+
export const SCHEMA_VERSION = 16;
|
|
5
5
|
// file_stats (02 §4; derived, rebuildable by a full re-stat) backs the CLI
|
|
6
6
|
// freshness sweep (11 §3.3): (mtime_ns, size) cheap-change detection so a
|
|
7
7
|
// one-shot command re-ingests only files that changed on disk since last ingest.
|
|
@@ -202,6 +202,7 @@ export const MIGRATIONS = {
|
|
|
202
202
|
13: "", // drop repos.root_path (migrate → fs sources), handled programmatically in store.ts
|
|
203
203
|
14: PENDING_MATERIALIZATIONS_DDL, // the api path's recorded file intent (spec/store §5.7)
|
|
204
204
|
15: "", // revisions.root_tree/frontmatter_blob nullable + `archive`, create `archives` (spec/store §7 "Retention"), handled programmatically in store.ts
|
|
205
|
+
16: "", // no DDL: delta tree nodes (spec/store §4.1 "Delta nodes", §3.4 step 16) — every 15.x node is already a valid checkpoint, so the step only moves user_version; handled in store.ts
|
|
205
206
|
};
|
|
206
207
|
export const DDL = /* sql */ `
|
|
207
208
|
-- ---- durable tables (02 §3) --------------------------------------------------
|
|
@@ -102,6 +102,13 @@ export class Store {
|
|
|
102
102
|
this.migrateV15();
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
105
|
+
// v16 (spec/store §3.4 step 16, §4.1 "Delta nodes"): no DDL and no data
|
|
106
|
+
// pass. A 15.x store holds only full (`[`-form) nodes and raw frontmatter
|
|
107
|
+
// blobs, which ARE the 16.0 checkpoint forms, so every existing revision
|
|
108
|
+
// is a checkpoint; deltas appear with the next commit. The major exists
|
|
109
|
+
// because a 15.x engine cannot read a `{`-form node (§1 step 3 keeps it out).
|
|
110
|
+
if (v === 16)
|
|
111
|
+
continue;
|
|
105
112
|
const ddl = MIGRATIONS[v];
|
|
106
113
|
if (!ddl)
|
|
107
114
|
throw new Error(`no migration to schema v${v}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Database } from "better-sqlite3";
|
|
2
2
|
import { type TreeEntry } from "../hash.js";
|
|
3
3
|
import type { RawBlock } from "../parse/types.js";
|
|
4
|
+
import { type ResolvedTree } from "./delta.js";
|
|
4
5
|
/** A block with a minted/assigned id, ready to encode into the tree. */
|
|
5
6
|
export interface TreeInputBlock {
|
|
6
7
|
blockId: string;
|
|
@@ -20,15 +21,40 @@ export interface TreeInputBlock {
|
|
|
20
21
|
export declare function assignIds(blocks: RawBlock[]): TreeInputBlock[];
|
|
21
22
|
/** Content-address raw text into blobs; returns hex hash. Dedups. */
|
|
22
23
|
export declare function putBlob(db: Database, text: string): string;
|
|
23
|
-
/** Content-address a
|
|
24
|
+
/** Content-address a node's text (a `[`-form or a `{"d":…}` delta, spec/store §4.1); returns hex hash. Dedups. */
|
|
25
|
+
export declare function putNodeText(db: Database, text: string): string;
|
|
26
|
+
/** Content-address a tree node's entries (full form); returns hex hash. Dedups. */
|
|
24
27
|
export declare function putTreeNode(db: Database, entries: TreeEntry[]): string;
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
29
|
+
* The revision context a tree is written in (spec/store §4.4, 16.0): the
|
|
30
|
+
* previous revision's resolved tree (`null` when there is none, or it is
|
|
31
|
+
* archived — then every node is full), and whether this revision is a
|
|
32
|
+
* checkpoint. Without a context `writeBlockTree` writes 15.x full nodes.
|
|
30
33
|
*/
|
|
31
|
-
export
|
|
34
|
+
export interface TreeWriteContext {
|
|
35
|
+
base: ResolvedTree | null;
|
|
36
|
+
checkpoint: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** spec/store §4.4: a revision is a checkpoint by cadence, or when it has no live predecessor. */
|
|
39
|
+
export declare function isCheckpointRevision(seq: number, previous: {
|
|
40
|
+
archive: string | null;
|
|
41
|
+
} | undefined): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Encode a list of sibling blocks into Merkle tree nodes bottom-up, writing
|
|
44
|
+
* blobs (raw + trivia) and child nodes. Returns the root node's hex hash.
|
|
45
|
+
* Structural sharing: an unchanged subtree whose base node is self-contained
|
|
46
|
+
* is not re-written (the base's hash is reused); otherwise, in a non-checkpoint
|
|
47
|
+
* revision, a list is written as the smaller of its delta against the same
|
|
48
|
+
* list of the previous revision and its full form (spec/store §4.1, §4.4).
|
|
49
|
+
*/
|
|
50
|
+
export declare function writeBlockTree(db: Database, blocks: TreeInputBlock[], ctx?: TreeWriteContext): string;
|
|
51
|
+
/**
|
|
52
|
+
* The frontmatter blob of a revision (spec/store §5.4 step 1, 16.0): the raw
|
|
53
|
+
* itself at a checkpoint or when the previous revision had none; else the
|
|
54
|
+
* smaller of the line delta against the previous raw and the raw. Returns the
|
|
55
|
+
* blob's hex hash.
|
|
56
|
+
*/
|
|
57
|
+
export declare function writeFrontmatterBlob(db: Database, raw: string, baseRaw: string | null, checkpoint: boolean): string;
|
|
32
58
|
export interface NewCommitInput {
|
|
33
59
|
repoId: string;
|
|
34
60
|
ts: string;
|
|
@@ -50,7 +76,17 @@ export interface NewRevisionInput {
|
|
|
50
76
|
renderedHash: Buffer;
|
|
51
77
|
path: string;
|
|
52
78
|
commitId: string;
|
|
79
|
+
/** The per-document seq when the caller computed it already (ingest needs it before the tree is written); `1 + max(seq)` otherwise. */
|
|
80
|
+
seq?: number;
|
|
53
81
|
}
|
|
82
|
+
/** The previous revision of a document (its greatest seq), or undefined — the base a delta is written against. */
|
|
83
|
+
export declare function previousRevision(db: Database, docId: string): {
|
|
84
|
+
rev_id: string;
|
|
85
|
+
seq: number;
|
|
86
|
+
root_tree: Buffer | null;
|
|
87
|
+
frontmatter_blob: Buffer | null;
|
|
88
|
+
archive: string | null;
|
|
89
|
+
} | undefined;
|
|
54
90
|
/** Append a revision; assigns the per-document monotonic seq. */
|
|
55
91
|
export declare function writeRevision(db: Database, input: NewRevisionInput): {
|
|
56
92
|
revId: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sha256, serializeTreeEntries } from "../hash.js";
|
|
2
2
|
import { mintId } from "../ids.js";
|
|
3
|
+
import { byteLength, encodeBodyDelta, encodeFrontmatterDelta, entryText, fullNodeText, isCheckpointSeq, } from "./delta.js";
|
|
3
4
|
/** Assign fresh minted ids to a parsed block tree (Stage-1 re-mint path). */
|
|
4
5
|
export function assignIds(blocks) {
|
|
5
6
|
return blocks.map((b) => ({
|
|
@@ -19,34 +20,69 @@ export function putBlob(db, text) {
|
|
|
19
20
|
db.prepare("INSERT OR IGNORE INTO blobs (hash, bytes, size) VALUES (?, ?, ?)").run(hash, bytes, bytes.length);
|
|
20
21
|
return hash.toString("hex");
|
|
21
22
|
}
|
|
22
|
-
/** Content-address a
|
|
23
|
-
export function
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
db.prepare("INSERT OR IGNORE INTO tree_nodes (hash, entries) VALUES (?, ?)").run(hash, serialized);
|
|
23
|
+
/** Content-address a node's text (a `[`-form or a `{"d":…}` delta, spec/store §4.1); returns hex hash. Dedups. */
|
|
24
|
+
export function putNodeText(db, text) {
|
|
25
|
+
const hash = sha256(text);
|
|
26
|
+
db.prepare("INSERT OR IGNORE INTO tree_nodes (hash, entries) VALUES (?, ?)").run(hash, text);
|
|
27
27
|
return hash.toString("hex");
|
|
28
28
|
}
|
|
29
|
+
/** Content-address a tree node's entries (full form); returns hex hash. Dedups. */
|
|
30
|
+
export function putTreeNode(db, entries) {
|
|
31
|
+
return putNodeText(db, serializeTreeEntries(entries));
|
|
32
|
+
}
|
|
33
|
+
/** spec/store §4.4: a revision is a checkpoint by cadence, or when it has no live predecessor. */
|
|
34
|
+
export function isCheckpointRevision(seq, previous) {
|
|
35
|
+
return isCheckpointSeq(seq) || previous === undefined || previous.archive !== null;
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
|
-
* Encode a list of sibling blocks into
|
|
31
|
-
* blobs (raw + trivia) and child
|
|
32
|
-
* Structural sharing: unchanged
|
|
33
|
-
* re-
|
|
38
|
+
* Encode a list of sibling blocks into Merkle tree nodes bottom-up, writing
|
|
39
|
+
* blobs (raw + trivia) and child nodes. Returns the root node's hex hash.
|
|
40
|
+
* Structural sharing: an unchanged subtree whose base node is self-contained
|
|
41
|
+
* is not re-written (the base's hash is reused); otherwise, in a non-checkpoint
|
|
42
|
+
* revision, a list is written as the smaller of its delta against the same
|
|
43
|
+
* list of the previous revision and its full form (spec/store §4.1, §4.4).
|
|
34
44
|
*/
|
|
35
|
-
export function writeBlockTree(db, blocks) {
|
|
36
|
-
const
|
|
45
|
+
export function writeBlockTree(db, blocks, ctx) {
|
|
46
|
+
const context = ctx ?? { base: null, checkpoint: true };
|
|
47
|
+
return writeList(db, blocks, null, context);
|
|
48
|
+
}
|
|
49
|
+
function writeList(db, blocks, parent, ctx) {
|
|
50
|
+
const texts = blocks.map((b) => {
|
|
37
51
|
const rawHashHex = putBlob(db, b.raw);
|
|
38
52
|
const triviaHashHex = b.trivia.length > 0 ? putBlob(db, b.trivia) : null;
|
|
39
|
-
const childTreeHashHex = b.children.length > 0 ?
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
rawHashHex,
|
|
43
|
-
childTreeHashHex,
|
|
44
|
-
type: b.type,
|
|
45
|
-
attrs: b.attrs,
|
|
46
|
-
triviaHashHex,
|
|
47
|
-
};
|
|
53
|
+
const childTreeHashHex = b.children.length > 0 ? writeList(db, b.children, b.blockId, ctx) : null;
|
|
54
|
+
const entry = [b.blockId, rawHashHex, childTreeHashHex, b.type, b.attrs, triviaHashHex];
|
|
55
|
+
return entryText(entry);
|
|
48
56
|
});
|
|
49
|
-
|
|
57
|
+
const full = fullNodeText(texts);
|
|
58
|
+
const base = ctx.base ? (parent === null ? ctx.base.root : ctx.base.byBlock.get(parent)) : undefined;
|
|
59
|
+
// An unchanged list reuses the base's node only when that node is context-free.
|
|
60
|
+
if (base && base.selfContained && sameTexts(base.texts, texts))
|
|
61
|
+
return base.hash;
|
|
62
|
+
if (ctx.checkpoint || !base)
|
|
63
|
+
return putNodeText(db, full);
|
|
64
|
+
const delta = encodeBodyDelta(base.texts, texts);
|
|
65
|
+
return putNodeText(db, byteLength(delta) < byteLength(full) ? delta : full);
|
|
66
|
+
}
|
|
67
|
+
function sameTexts(a, b) {
|
|
68
|
+
if (a.length !== b.length)
|
|
69
|
+
return false;
|
|
70
|
+
for (let i = 0; i < a.length; i++)
|
|
71
|
+
if (a[i] !== b[i])
|
|
72
|
+
return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The frontmatter blob of a revision (spec/store §5.4 step 1, 16.0): the raw
|
|
77
|
+
* itself at a checkpoint or when the previous revision had none; else the
|
|
78
|
+
* smaller of the line delta against the previous raw and the raw. Returns the
|
|
79
|
+
* blob's hex hash.
|
|
80
|
+
*/
|
|
81
|
+
export function writeFrontmatterBlob(db, raw, baseRaw, checkpoint) {
|
|
82
|
+
if (checkpoint || baseRaw === null)
|
|
83
|
+
return putBlob(db, raw);
|
|
84
|
+
const delta = encodeFrontmatterDelta(baseRaw, raw);
|
|
85
|
+
return putBlob(db, byteLength(delta) < byteLength(raw) ? delta : raw);
|
|
50
86
|
}
|
|
51
87
|
/** Append a commit; assigns the per-repo total-order seq. */
|
|
52
88
|
export function newCommit(db, input) {
|
|
@@ -68,23 +104,29 @@ export function newCommit(db, input) {
|
|
|
68
104
|
});
|
|
69
105
|
return { commitId, seq: row.seq };
|
|
70
106
|
}
|
|
107
|
+
/** The previous revision of a document (its greatest seq), or undefined — the base a delta is written against. */
|
|
108
|
+
export function previousRevision(db, docId) {
|
|
109
|
+
return db
|
|
110
|
+
.prepare("SELECT rev_id, seq, root_tree, frontmatter_blob, archive FROM revisions WHERE doc_id = ? ORDER BY seq DESC LIMIT 1")
|
|
111
|
+
.get(docId);
|
|
112
|
+
}
|
|
71
113
|
/** Append a revision; assigns the per-document monotonic seq. */
|
|
72
114
|
export function writeRevision(db, input) {
|
|
73
115
|
const revId = mintId("r");
|
|
74
|
-
const
|
|
116
|
+
const seq = input.seq ?? db
|
|
75
117
|
.prepare("SELECT COALESCE(MAX(seq),0)+1 AS seq FROM revisions WHERE doc_id = ?")
|
|
76
|
-
.get(input.docId);
|
|
118
|
+
.get(input.docId).seq;
|
|
77
119
|
db.prepare(`INSERT INTO revisions (rev_id, doc_id, seq, root_tree, frontmatter_blob, rendered_hash, path, commit_id)
|
|
78
120
|
VALUES (@revId, @docId, @seq, @rootTree, @frontmatterBlob, @renderedHash, @path, @commitId)`).run({
|
|
79
121
|
revId,
|
|
80
122
|
docId: input.docId,
|
|
81
|
-
seq
|
|
123
|
+
seq,
|
|
82
124
|
rootTree: Buffer.from(input.rootTreeHex, "hex"),
|
|
83
125
|
frontmatterBlob: input.frontmatterBlobHex ? Buffer.from(input.frontmatterBlobHex, "hex") : null,
|
|
84
126
|
renderedHash: input.renderedHash,
|
|
85
127
|
path: input.path,
|
|
86
128
|
commitId: input.commitId,
|
|
87
129
|
});
|
|
88
|
-
return { revId, seq
|
|
130
|
+
return { revId, seq };
|
|
89
131
|
}
|
|
90
132
|
//# sourceMappingURL=writers.js.map
|
package/dist/src/format/json.js
CHANGED
|
@@ -308,6 +308,11 @@ export const jsonAdapter = {
|
|
|
308
308
|
extractJsonEdges(metadata, "", edges, new Set());
|
|
309
309
|
return edges;
|
|
310
310
|
},
|
|
311
|
+
// The document's authored mapping (spec/properties §3.4, 1.2): the root object
|
|
312
|
+
// of a well-formed JSON text; a non-object root (an array, a scalar), a parse
|
|
313
|
+
// error or an empty file yields null — no frontmatter rows. `JSON.parse` is
|
|
314
|
+
// strict (no comments, no trailing commas, no BOM); a repeated key keeps the
|
|
315
|
+
// LAST value at the FIRST position (the host's object semantics, §8).
|
|
311
316
|
extractMetadata(source) {
|
|
312
317
|
try {
|
|
313
318
|
const parsed = JSON.parse(source);
|
package/dist/src/format/yaml.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// YAML format adapter. Decomposes YAML files into blocks with yaml:-prefixed
|
|
2
2
|
// kinds, extracts $ref/extends/$schema edges, and projects top-level mapping
|
|
3
3
|
// entries as metadata.
|
|
4
|
-
import { parseDocument, isMap, isSeq, isScalar, } from "yaml";
|
|
4
|
+
import { parseAllDocuments, parseDocument, isMap, isSeq, isScalar, } from "yaml";
|
|
5
5
|
import { AdapterCapability } from "./adapter.js";
|
|
6
6
|
import { normalizeText } from "../core/hash.js";
|
|
7
7
|
export const YAML_FORMAT = "yaml";
|
|
@@ -358,10 +358,22 @@ export const yamlAdapter = {
|
|
|
358
358
|
walk(blocks);
|
|
359
359
|
return nodes;
|
|
360
360
|
},
|
|
361
|
+
// The document's authored mapping (spec/properties §3.4, 1.2): the FIRST
|
|
362
|
+
// document of the stream, parsed under the same YAML 1.2 core-schema contract
|
|
363
|
+
// as a Markdown frontmatter fence (§4), when it parses without error and its
|
|
364
|
+
// root is a mapping. A stream's later documents are ignored (a multi-document
|
|
365
|
+
// file is first-document-only, by the spec); a syntax error — including a
|
|
366
|
+
// duplicate key — a non-mapping root (a sequence, a scalar) or an empty file
|
|
367
|
+
// yields null: no frontmatter rows, exactly as a malformed fence yields none.
|
|
368
|
+
// `parseDocument` is NOT used here: it is lenient (it records errors on the
|
|
369
|
+
// document and still returns contents), which §3.4 refuses.
|
|
361
370
|
extractMetadata(source) {
|
|
362
371
|
try {
|
|
363
|
-
const
|
|
364
|
-
const
|
|
372
|
+
const docs = parseAllDocuments(source);
|
|
373
|
+
const first = Array.isArray(docs) ? docs[0] : undefined;
|
|
374
|
+
if (!first || first.errors.length > 0)
|
|
375
|
+
return null;
|
|
376
|
+
const obj = first.toJS();
|
|
365
377
|
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
366
378
|
return obj;
|
|
367
379
|
}
|
|
@@ -64,21 +64,7 @@ export declare function docDiffUnified(store: Store, docId: string, opts?: {
|
|
|
64
64
|
* a single special case: the empty text has *no* lines (an empty file is zero
|
|
65
65
|
* lines, not one empty line), so a diff from/to nothing is `@@ -0,0 +1,n @@`. */
|
|
66
66
|
export declare function diffLines(text: string): string[];
|
|
67
|
-
|
|
68
|
-
* `delete` one from the old text, `insert` one from the new text. */
|
|
69
|
-
export interface EditOp {
|
|
70
|
-
kind: "keep" | "delete" | "insert";
|
|
71
|
-
line: string;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Myers' O(ND) shortest edit script (forward, with a per-`d` trace for the
|
|
75
|
-
* backtrack). `V[k]` is the furthest x on diagonal `k = x - y` reachable with
|
|
76
|
-
* `d` edits. The canonical tie rule, identical in both engines: at each step
|
|
77
|
-
* take the diagonal from `k+1` (moving down — an insertion of `b[y]`) when
|
|
78
|
-
* `k == -d || (k != d && V[k-1] < V[k+1])`, else from `k-1` (moving right —
|
|
79
|
-
* a deletion of `a[x]`). On a tie (`V[k-1] == V[k+1]`) that is the deletion.
|
|
80
|
-
*/
|
|
81
|
-
export declare function myersScript(a: string[], b: string[]): EditOp[];
|
|
67
|
+
export { myersScript, type EditOp } from "../core/edit-script.js";
|
|
82
68
|
/**
|
|
83
69
|
* The unified diff of two texts (spec/surface §3): hunks of `DIFF_CONTEXT` (3)
|
|
84
70
|
* lines of context; a change group extends to include the next change when
|
|
@@ -4,6 +4,8 @@ import { isValidId } from "../core/ids.js";
|
|
|
4
4
|
import { EngineError } from "../mcp/errors.js";
|
|
5
5
|
import { referencePath, storagePath } from "../core/paths.js";
|
|
6
6
|
import { revisionBody } from "../core/store/archive.js";
|
|
7
|
+
import { myersScript } from "../core/edit-script.js";
|
|
8
|
+
import { TreeResolver } from "../core/store/delta.js";
|
|
7
9
|
/** blocks_history: a block's biography from block_changes + commits (06 §3). */
|
|
8
10
|
export function historyNode(store, blockId, opts = {}) {
|
|
9
11
|
const limit = opts.limit ?? 100;
|
|
@@ -64,20 +66,19 @@ function blocksAtRevision(store, docId, revId) {
|
|
|
64
66
|
throw new RevisionNotFound(docId, revId);
|
|
65
67
|
const out = new Map();
|
|
66
68
|
const blob = store.db.prepare(`SELECT bytes FROM ${rev.prefix}blobs WHERE hash = ?`);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const entries = JSON.parse(node.entries);
|
|
73
|
-
for (const [blockId, rawHashHex, childHashHex] of entries) {
|
|
69
|
+
// Every list resolved in the revision's context (spec/store §6, 16.0: a
|
|
70
|
+
// delta node walks back to the previous revisions' same list).
|
|
71
|
+
const tree = new TreeResolver(store.db, rev.prefix).tree(rev.doc_id, rev.seq, rev.root_tree.toString("hex"));
|
|
72
|
+
const walk = (entries) => {
|
|
73
|
+
for (const [blockId, rawHashHex] of entries) {
|
|
74
74
|
const raw = blob.get(Buffer.from(rawHashHex, "hex"))?.bytes.toString("utf8") ?? "";
|
|
75
75
|
out.set(blockId, raw);
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const children = tree.byBlock.get(blockId);
|
|
77
|
+
if (children)
|
|
78
|
+
walk(children.entries);
|
|
78
79
|
}
|
|
79
80
|
};
|
|
80
|
-
walk(
|
|
81
|
+
walk(tree.root.entries);
|
|
81
82
|
return out;
|
|
82
83
|
}
|
|
83
84
|
/**
|
|
@@ -130,70 +131,9 @@ export function diffLines(text) {
|
|
|
130
131
|
return text === "" ? [] : text.split("\n");
|
|
131
132
|
}
|
|
132
133
|
const DIFF_CONTEXT = 3;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
* `d` edits. The canonical tie rule, identical in both engines: at each step
|
|
137
|
-
* take the diagonal from `k+1` (moving down — an insertion of `b[y]`) when
|
|
138
|
-
* `k == -d || (k != d && V[k-1] < V[k+1])`, else from `k-1` (moving right —
|
|
139
|
-
* a deletion of `a[x]`). On a tie (`V[k-1] == V[k+1]`) that is the deletion.
|
|
140
|
-
*/
|
|
141
|
-
export function myersScript(a, b) {
|
|
142
|
-
const n = a.length;
|
|
143
|
-
const m = b.length;
|
|
144
|
-
const max = n + m;
|
|
145
|
-
// V is indexed by k ∈ [-max-1, max+1]; `off` maps it onto a plain array.
|
|
146
|
-
const off = max + 1;
|
|
147
|
-
const v = new Array(2 * max + 3).fill(0);
|
|
148
|
-
const trace = [];
|
|
149
|
-
let found = false;
|
|
150
|
-
for (let d = 0; d <= max && !found; d++) {
|
|
151
|
-
trace.push(v.slice());
|
|
152
|
-
for (let k = -d; k <= d; k += 2) {
|
|
153
|
-
let x;
|
|
154
|
-
if (k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]))
|
|
155
|
-
x = v[off + k + 1];
|
|
156
|
-
else
|
|
157
|
-
x = v[off + k - 1] + 1;
|
|
158
|
-
let y = x - k;
|
|
159
|
-
while (x < n && y < m && a[x] === b[y]) {
|
|
160
|
-
x++;
|
|
161
|
-
y++;
|
|
162
|
-
}
|
|
163
|
-
v[off + k] = x;
|
|
164
|
-
if (x >= n && y >= m) {
|
|
165
|
-
found = true;
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// Backtrack from (n, m) through the trace, emitting ops newest-first.
|
|
171
|
-
const ops = [];
|
|
172
|
-
let x = n;
|
|
173
|
-
let y = m;
|
|
174
|
-
for (let d = trace.length - 1; d >= 0; d--) {
|
|
175
|
-
const vd = trace[d];
|
|
176
|
-
const k = x - y;
|
|
177
|
-
const prevK = k === -d || (k !== d && vd[off + k - 1] < vd[off + k + 1]) ? k + 1 : k - 1;
|
|
178
|
-
const prevX = vd[off + prevK];
|
|
179
|
-
const prevY = prevX - prevK;
|
|
180
|
-
while (x > prevX && y > prevY) {
|
|
181
|
-
x--;
|
|
182
|
-
y--;
|
|
183
|
-
ops.push({ kind: "keep", line: a[x] });
|
|
184
|
-
}
|
|
185
|
-
if (d > 0) {
|
|
186
|
-
if (x === prevX)
|
|
187
|
-
ops.push({ kind: "insert", line: b[prevY] });
|
|
188
|
-
else
|
|
189
|
-
ops.push({ kind: "delete", line: a[prevX] });
|
|
190
|
-
}
|
|
191
|
-
x = prevX;
|
|
192
|
-
y = prevY;
|
|
193
|
-
}
|
|
194
|
-
ops.reverse();
|
|
195
|
-
return ops;
|
|
196
|
-
}
|
|
134
|
+
// The edit script lives in core/edit-script.ts (shared with the store's delta
|
|
135
|
+
// nodes, spec/store §4.1); re-exported here for the surface and its tests.
|
|
136
|
+
export { myersScript } from "../core/edit-script.js";
|
|
197
137
|
/**
|
|
198
138
|
* The unified diff of two texts (spec/surface §3): hunks of `DIFF_CONTEXT` (3)
|
|
199
139
|
* lines of context; a change group extends to include the next change when
|
|
@@ -14,6 +14,17 @@ export declare function stringList(opts?: {
|
|
|
14
14
|
export declare function isStringList(schema: z.ZodTypeAny): boolean;
|
|
15
15
|
/** The parts of a comma-joined string: split on `,`, trimmed, empty parts dropped. */
|
|
16
16
|
export declare function commaSplit(s: string): string[];
|
|
17
|
+
/**
|
|
18
|
+
* (4.11) A string whose trimmed form opens `[`, read as JSON: `{ list }` when it
|
|
19
|
+
* is a JSON array of strings (accepted as that list); otherwise `{ fix }` — the
|
|
20
|
+
* list the error's corrected call should carry: the parsed array when the text
|
|
21
|
+
* parsed (a JSON array that is not all strings), else the comma split.
|
|
22
|
+
*/
|
|
23
|
+
export declare function jsonArrayString(s: string): {
|
|
24
|
+
list: string[];
|
|
25
|
+
} | {
|
|
26
|
+
fix: unknown[];
|
|
27
|
+
} | null;
|
|
17
28
|
/**
|
|
18
29
|
* Validate `raw` against `shape`: unknown names first (in bytewise order), then
|
|
19
30
|
* each declared argument (bytewise order) for presence and type. Returns the
|
|
@@ -13,13 +13,19 @@ export function argumentError(tool, message, shape, fix) {
|
|
|
13
13
|
const full = `${message} (${argumentsHint(tool, shape)})`;
|
|
14
14
|
return new EngineError("filter_invalid", full, { data: { reason: full, hint: "arguments", ...(fix !== undefined ? { fix } : {}) } });
|
|
15
15
|
}
|
|
16
|
-
// ---- string lists (spec/surface §4 "Arguments", 4.5)
|
|
16
|
+
// ---- string lists (spec/surface §4 "Arguments", 4.5 / 4.11) ---------------------------
|
|
17
17
|
//
|
|
18
18
|
// Every list-of-strings argument (`docs`, `ids`, `roots`, `blocks`, `select`,
|
|
19
19
|
// `unset`) also takes ONE bare string as a one-element list — the first guess
|
|
20
|
-
// of a caller who has one ref.
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
// of a caller who has one ref — and (4.11) a string that IS a JSON array of
|
|
21
|
+
// strings (`"[\"b_1\",\"b_2\"]"`, the list stringified by a client that
|
|
22
|
+
// serializes every argument) as that array: the intent is unambiguous, the
|
|
23
|
+
// same tolerance class as the bare string. A comma-joined string is still
|
|
24
|
+
// "must be an array of strings", but that error carries a `fix`: the same call
|
|
25
|
+
// with the string split on commas (each part trimmed, empty parts dropped),
|
|
26
|
+
// canonical JSON. A string opening `[` that is not a JSON array of strings is
|
|
27
|
+
// the same error, its `fix` the parsed array when it parsed (a JSON array of
|
|
28
|
+
// numbers) and the comma split otherwise (a malformed `[`).
|
|
23
29
|
const STRING_LISTS = new WeakSet();
|
|
24
30
|
/** The schema of a string-list argument: an array of strings, or one bare string (normalized to `[s]` by `validateArguments`). */
|
|
25
31
|
export function stringList(opts = {}) {
|
|
@@ -36,6 +42,29 @@ export function isStringList(schema) {
|
|
|
36
42
|
export function commaSplit(s) {
|
|
37
43
|
return s.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
38
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* (4.11) A string whose trimmed form opens `[`, read as JSON: `{ list }` when it
|
|
47
|
+
* is a JSON array of strings (accepted as that list); otherwise `{ fix }` — the
|
|
48
|
+
* list the error's corrected call should carry: the parsed array when the text
|
|
49
|
+
* parsed (a JSON array that is not all strings), else the comma split.
|
|
50
|
+
*/
|
|
51
|
+
export function jsonArrayString(s) {
|
|
52
|
+
const trimmed = s.trim();
|
|
53
|
+
if (!trimmed.startsWith("["))
|
|
54
|
+
return null;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(trimmed);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { fix: commaSplit(s) };
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(parsed))
|
|
63
|
+
return { fix: commaSplit(s) };
|
|
64
|
+
if (parsed.every((x) => typeof x === "string"))
|
|
65
|
+
return { list: parsed };
|
|
66
|
+
return { fix: parsed };
|
|
67
|
+
}
|
|
39
68
|
/**
|
|
40
69
|
* Validate `raw` against `shape`: unknown names first (in bytewise order), then
|
|
41
70
|
* each declared argument (bytewise order) for presence and type. Returns the
|
|
@@ -61,6 +90,20 @@ export function validateArguments(tool, shape, raw) {
|
|
|
61
90
|
continue;
|
|
62
91
|
}
|
|
63
92
|
if (isStringList(schema) && typeof value === "string") {
|
|
93
|
+
// (4.11) a stringified JSON array of strings is that list — checked
|
|
94
|
+
// against the list schema itself (the `roots` minimum holds for it too);
|
|
95
|
+
// any other `[…` is the array error with the parsed array (or the comma
|
|
96
|
+
// split) as its `fix`.
|
|
97
|
+
const json = jsonArrayString(value);
|
|
98
|
+
if (json !== null) {
|
|
99
|
+
if ("fix" in json)
|
|
100
|
+
throw argumentError(tool, `\`${key}\` must be an array of strings`, shape, canonicalJson({ ...args, [key]: json.fix }));
|
|
101
|
+
const parsed = schema.safeParse(json.list);
|
|
102
|
+
if (!parsed.success)
|
|
103
|
+
throw argumentError(tool, issueMessage(key, schema, parsed.error.issues), shape);
|
|
104
|
+
out[key] = json.list;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
64
107
|
// (4.5) one bare string is a one-element list; a comma-joined one is the
|
|
65
108
|
// array error with the split list as its `fix`.
|
|
66
109
|
if (value.includes(","))
|
package/dist/src/mcp/errors.js
CHANGED
|
@@ -22,7 +22,7 @@ export function errorBody(err) {
|
|
|
22
22
|
if (err instanceof EngineError)
|
|
23
23
|
return { error: err.code, message: err.message, ...(err.data !== undefined ? { data: err.data } : {}), retriable: err.retriable };
|
|
24
24
|
if (err instanceof FilterInvalid)
|
|
25
|
-
return { error: "filter_invalid", message: err.message, data: { reason: err.reason, hint: err.hint }, retriable: false };
|
|
25
|
+
return { error: "filter_invalid", message: err.message, data: { reason: err.reason, hint: err.hint, ...(err.fix !== undefined ? { fix: err.fix } : {}) }, retriable: false };
|
|
26
26
|
if (err instanceof CursorInvalid)
|
|
27
27
|
return { error: "filter_invalid", message: err.message, data: { reason: err.reason ?? `cursor was not issued by ${err.surface}`, hint: "resume only with a `cursor` returned by a truncated page of the same tool" }, retriable: false };
|
|
28
28
|
if (err instanceof MutationError)
|