@omgbase/core 0.15.0 → 0.21.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/ingest.d.ts +2 -0
- package/dist/src/core/ingest.js +13 -0
- package/dist/src/core/read/document.d.ts +9 -8
- package/dist/src/core/read/document.js +0 -2
- package/dist/src/core/store/gc.d.ts +13 -1
- package/dist/src/core/store/gc.js +24 -5
- package/dist/src/core/store/size.d.ts +79 -0
- package/dist/src/core/store/size.js +232 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +5 -1
- 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/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +63 -10
- package/dist/src/mcp/server.d.ts +34 -1
- package/dist/src/mcp/server.js +158 -46
- package/dist/src/mutate/canonical.d.ts +27 -0
- package/dist/src/mutate/canonical.js +138 -0
- package/dist/src/mutate/effects.d.ts +58 -8
- package/dist/src/mutate/effects.js +312 -43
- 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/plan.d.ts +26 -16
- package/dist/src/mutate/plan.js +152 -112
- package/dist/src/mutate/select.d.ts +78 -0
- package/dist/src/mutate/select.js +903 -0
- package/dist/src/mutate/tree.d.ts +2 -2
- package/dist/src/oqx-js/context.d.ts +9 -2
- package/dist/src/oqx-js/context.js +74 -28
- package/dist/src/oqx-js/names.d.ts +40 -0
- package/dist/src/oqx-js/names.js +347 -0
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +17 -9
- 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 +45 -1
- package/dist/src/oqx-js/run.js +114 -32
- 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/dist/src/version.d.ts +7 -2
- package/dist/src/version.js +5 -2
- package/package.json +2 -2
|
@@ -10,6 +10,8 @@ export interface DispositionRow {
|
|
|
10
10
|
matcherV: string | null;
|
|
11
11
|
detail: Record<string, unknown>;
|
|
12
12
|
}
|
|
13
|
+
/** spec/store §5.4 step 10 (14.2): which verdicts the store persists — every kind but `same`. */
|
|
14
|
+
export declare function persistsDisposition(kind: string): boolean;
|
|
13
15
|
export interface ResolvedEdgeRow {
|
|
14
16
|
srcDoc: string;
|
|
15
17
|
srcBlock: string | null;
|
package/dist/src/core/ingest.js
CHANGED
|
@@ -13,6 +13,10 @@ import { flattenFrontmatter, flattenComputed, writeDocProperties } from "./store
|
|
|
13
13
|
import { maintainEdges, adoptPhantoms } from "./store/edges.js";
|
|
14
14
|
import { parse as parseYaml } from "yaml";
|
|
15
15
|
import { adapterForPath } from "../format/index.js";
|
|
16
|
+
/** spec/store §5.4 step 10 (14.2): which verdicts the store persists — every kind but `same`. */
|
|
17
|
+
export function persistsDisposition(kind) {
|
|
18
|
+
return kind !== "same";
|
|
19
|
+
}
|
|
16
20
|
/** The parse step of ingest, exported so a batch caller can parse + reconcile
|
|
17
21
|
* every document of a checkpoint BEFORE committing any (the cross-document
|
|
18
22
|
* phase needs all per-doc results first) and hand the tree back via
|
|
@@ -279,10 +283,19 @@ export function ingestInto(db, repoId, path, content, opts) {
|
|
|
279
283
|
const dispositions = resolved.dispositions.length > 0 || opts.resolveIds
|
|
280
284
|
? resolved.dispositions
|
|
281
285
|
: rows.map((r) => ({ blockId: r.blockId, kind: "inserted", confidence: null, reason: null, matcherV: null, detail: {} }));
|
|
286
|
+
// spec/store §5.4 step 10 (14.2): a `same` verdict is not persisted. The
|
|
287
|
+
// matcher (and the known-id resolver) still produce one per untouched block
|
|
288
|
+
// — the outcome counts them — but the table records what CHANGED, so a
|
|
289
|
+
// block's `updated_at` (MAX commit ts over its `block_changes`) and its
|
|
290
|
+
// `blocks_history` are the block's own changes, not its siblings'. Every
|
|
291
|
+
// other kind — the first revision's `inserted` rows the mint oracle (§2.1)
|
|
292
|
+
// depends on included — is written as before.
|
|
282
293
|
const insDisp = db.prepare(`INSERT OR IGNORE INTO dispositions (commit_id, block_id, kind, confidence, reason, matcher_v, detail)
|
|
283
294
|
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
284
295
|
const bc = db.prepare("INSERT OR IGNORE INTO block_changes (block_id, commit_id, kind) VALUES (?, ?, ?)");
|
|
285
296
|
for (const d of dispositions) {
|
|
297
|
+
if (!persistsDisposition(d.kind))
|
|
298
|
+
continue;
|
|
286
299
|
insDisp.run(commitId, d.blockId, d.kind, d.confidence, d.reason, d.matcherV, JSON.stringify(d.detail));
|
|
287
300
|
bc.run(d.blockId, commitId, d.kind);
|
|
288
301
|
}
|
|
@@ -76,6 +76,15 @@ export declare function docsReadMany(store: Store, repoId: string, refs: string[
|
|
|
76
76
|
includeIds?: boolean;
|
|
77
77
|
budgetTokens?: number;
|
|
78
78
|
}): DocsReadManyResult;
|
|
79
|
+
/**
|
|
80
|
+
* `docs_read { at }` / `docs_read_at` (spec/surface §2, 4.7): the document AS
|
|
81
|
+
* OF a revision. No `properties`: the properties table is current-state only
|
|
82
|
+
* (`spec/properties`), and recomputing the bag from the reconstructed bytes is
|
|
83
|
+
* not yet a one-call affair of the format layer (the inline fields come out of
|
|
84
|
+
* the ingest's node projection) — so the time-travel read carries the bytes
|
|
85
|
+
* and nothing it would have to guess at (through 4.6 `docs_read_at` returned
|
|
86
|
+
* the CURRENT bag flagged `propertiesAreCurrent: true`).
|
|
87
|
+
*/
|
|
79
88
|
export interface DocsReadAtResult {
|
|
80
89
|
id: string;
|
|
81
90
|
/** The path as of that revision, in the reference form. */
|
|
@@ -88,14 +97,6 @@ export interface DocsReadAtResult {
|
|
|
88
97
|
* rendered_hash — a byte-faithful round-trip. False flags the fidelity gap
|
|
89
98
|
* described below (doc-level trivia may differ for an old revision). */
|
|
90
99
|
renderedHashMatch: boolean;
|
|
91
|
-
/**
|
|
92
|
-
* Properties as they are CURRENTLY (not at `rev`): the properties table is
|
|
93
|
-
* current-state only (properties-table), so historical property values are
|
|
94
|
-
* not recoverable. Included as best-effort context, flagged by
|
|
95
|
-
* `propertiesAreCurrent: true`.
|
|
96
|
-
*/
|
|
97
|
-
properties: Record<string, Record<string, unknown>>;
|
|
98
|
-
propertiesAreCurrent: true;
|
|
99
100
|
}
|
|
100
101
|
/**
|
|
101
102
|
* Whole-document TIME-TRAVEL read: reconstruct a document's full file bytes AS
|
|
@@ -204,8 +204,6 @@ export function readDocumentAtRevision(store, docId, revId) {
|
|
|
204
204
|
rev: revId,
|
|
205
205
|
raw: content,
|
|
206
206
|
renderedHashMatch,
|
|
207
|
-
properties: surfaceProperties(docPropertiesGrouped(db, docId)),
|
|
208
|
-
propertiesAreCurrent: true,
|
|
209
207
|
};
|
|
210
208
|
}
|
|
211
209
|
//# sourceMappingURL=document.js.map
|
|
@@ -2,16 +2,28 @@ import type { Store } from "./store.js";
|
|
|
2
2
|
export interface GcResult {
|
|
3
3
|
blobsSwept: number;
|
|
4
4
|
treeNodesSwept: number;
|
|
5
|
+
/** spec/store §7 (14.2): `same` rows a 14.1 store wrote into `dispositions`/`block_changes` — history nothing reads. */
|
|
6
|
+
sameDispositionsSwept: number;
|
|
5
7
|
}
|
|
8
|
+
/** The `same` rows a store through 14.1 persisted (spec/store §7, 14.2). */
|
|
9
|
+
export declare function countSameDispositions(store: Store): number;
|
|
6
10
|
/**
|
|
7
11
|
* Mark-and-sweep GC (flag-gated; enabled must be true to run — v1.0 ships it
|
|
8
12
|
* off). Removes blobs/tree_nodes unreachable from any revision root. Because
|
|
9
13
|
* revisions are append-only and never pruned in v1, nothing is actually
|
|
10
|
-
* unreachable unless revision pruning ran first — so
|
|
14
|
+
* unreachable unless revision pruning ran first — so that half is a no-op in a
|
|
11
15
|
* default v1 repo, exactly as specified. Rebuild-equivalence is preserved.
|
|
16
|
+
*
|
|
17
|
+
* spec/store §7 (14.2): the same sweep deletes every `same` row of
|
|
18
|
+
* `dispositions` and `block_changes` — what a 14.1 store wrote for every
|
|
19
|
+
* untouched block of every commit, and nothing reads (`updated_at` and
|
|
20
|
+
* `blocks_history` are the block's own changes now). Not a migration: a
|
|
21
|
+
* data-only change needs no `user_version`, and opening a database must never
|
|
22
|
+
* bulk-delete. `dryRun` counts everything and deletes nothing.
|
|
12
23
|
*/
|
|
13
24
|
export declare function runGc(store: Store, opts?: {
|
|
14
25
|
enabled?: boolean;
|
|
26
|
+
dryRun?: boolean;
|
|
15
27
|
}): GcResult;
|
|
16
28
|
/** Resurrection-pool expiry sweep (02 §7): drop rows past their expires_ts.
|
|
17
29
|
* Runs routinely (lazy, at checkpoint time). */
|
|
@@ -29,24 +29,37 @@ function markReachable(store) {
|
|
|
29
29
|
walk(r.root_tree.toString("hex"));
|
|
30
30
|
return { trees, blobs };
|
|
31
31
|
}
|
|
32
|
+
/** The `same` rows a store through 14.1 persisted (spec/store §7, 14.2). */
|
|
33
|
+
export function countSameDispositions(store) {
|
|
34
|
+
return store.db.prepare("SELECT count(*) AS c FROM dispositions WHERE kind = 'same'").get().c;
|
|
35
|
+
}
|
|
32
36
|
/**
|
|
33
37
|
* Mark-and-sweep GC (flag-gated; enabled must be true to run — v1.0 ships it
|
|
34
38
|
* off). Removes blobs/tree_nodes unreachable from any revision root. Because
|
|
35
39
|
* revisions are append-only and never pruned in v1, nothing is actually
|
|
36
|
-
* unreachable unless revision pruning ran first — so
|
|
40
|
+
* unreachable unless revision pruning ran first — so that half is a no-op in a
|
|
37
41
|
* default v1 repo, exactly as specified. Rebuild-equivalence is preserved.
|
|
42
|
+
*
|
|
43
|
+
* spec/store §7 (14.2): the same sweep deletes every `same` row of
|
|
44
|
+
* `dispositions` and `block_changes` — what a 14.1 store wrote for every
|
|
45
|
+
* untouched block of every commit, and nothing reads (`updated_at` and
|
|
46
|
+
* `blocks_history` are the block's own changes now). Not a migration: a
|
|
47
|
+
* data-only change needs no `user_version`, and opening a database must never
|
|
48
|
+
* bulk-delete. `dryRun` counts everything and deletes nothing.
|
|
38
49
|
*/
|
|
39
50
|
export function runGc(store, opts = {}) {
|
|
40
51
|
if (!opts.enabled)
|
|
41
|
-
return { blobsSwept: 0, treeNodesSwept: 0 };
|
|
52
|
+
return { blobsSwept: 0, treeNodesSwept: 0, sameDispositionsSwept: 0 };
|
|
42
53
|
return store.write((db) => {
|
|
43
54
|
const { trees, blobs } = markReachable(store);
|
|
55
|
+
const dry = Boolean(opts.dryRun);
|
|
44
56
|
let treeNodesSwept = 0;
|
|
45
57
|
const allTrees = db.prepare("SELECT hash FROM tree_nodes").all();
|
|
46
58
|
const delTree = db.prepare("DELETE FROM tree_nodes WHERE hash = ?");
|
|
47
59
|
for (const t of allTrees)
|
|
48
60
|
if (!trees.has(t.hash.toString("hex"))) {
|
|
49
|
-
|
|
61
|
+
if (!dry)
|
|
62
|
+
delTree.run(t.hash);
|
|
50
63
|
treeNodesSwept++;
|
|
51
64
|
}
|
|
52
65
|
let blobsSwept = 0;
|
|
@@ -54,10 +67,16 @@ export function runGc(store, opts = {}) {
|
|
|
54
67
|
const delBlob = db.prepare("DELETE FROM blobs WHERE hash = ?");
|
|
55
68
|
for (const b of allBlobs)
|
|
56
69
|
if (!blobs.has(b.hash.toString("hex"))) {
|
|
57
|
-
|
|
70
|
+
if (!dry)
|
|
71
|
+
delBlob.run(b.hash);
|
|
58
72
|
blobsSwept++;
|
|
59
73
|
}
|
|
60
|
-
|
|
74
|
+
const sameDispositionsSwept = countSameDispositions(store);
|
|
75
|
+
if (!dry && sameDispositionsSwept > 0) {
|
|
76
|
+
db.prepare("DELETE FROM block_changes WHERE kind = 'same'").run();
|
|
77
|
+
db.prepare("DELETE FROM dispositions WHERE kind = 'same'").run();
|
|
78
|
+
}
|
|
79
|
+
return { blobsSwept, treeNodesSwept, sameDispositionsSwept };
|
|
61
80
|
});
|
|
62
81
|
}
|
|
63
82
|
/** Resurrection-pool expiry sweep (02 §7): drop rows past their expires_ts.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export declare const SIZE_CATEGORIES: readonly ["content", "history", "search", "sync"];
|
|
3
|
+
export type SizeCategory = (typeof SIZE_CATEGORIES)[number];
|
|
4
|
+
/** spec/store §7: the category of every table `schema.sql` creates. */
|
|
5
|
+
export declare const TABLE_CATEGORY: Readonly<Record<string, SizeCategory>>;
|
|
6
|
+
/** The category of a table: the spec's assignment, `sync` for anything else. */
|
|
7
|
+
export declare function categoryOf(table: string): SizeCategory;
|
|
8
|
+
export interface SizeTable {
|
|
9
|
+
name: string;
|
|
10
|
+
/** Pages of the table and its indexes (and shadow tables), in bytes; null without dbstat. */
|
|
11
|
+
bytes: number | null;
|
|
12
|
+
/** `bytes` over the total, one decimal; null without dbstat. */
|
|
13
|
+
share: number | null;
|
|
14
|
+
rows: number;
|
|
15
|
+
/** The bytes of the table's indexes alone (included in `bytes`); null without dbstat. */
|
|
16
|
+
indexes: number | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SizeCategoryReport {
|
|
19
|
+
name: SizeCategory;
|
|
20
|
+
bytes: number | null;
|
|
21
|
+
share: number | null;
|
|
22
|
+
rows: number;
|
|
23
|
+
/** By bytes desc then name (rows desc then name without dbstat). */
|
|
24
|
+
tables: SizeTable[];
|
|
25
|
+
}
|
|
26
|
+
export interface SizeFile {
|
|
27
|
+
path: string;
|
|
28
|
+
/** The main file's bytes on disk (`page_count * page_size` for an in-memory database). */
|
|
29
|
+
bytes: number;
|
|
30
|
+
/** The `-wal` file's bytes on disk, 0 when absent. */
|
|
31
|
+
wal_bytes: number;
|
|
32
|
+
page_size: number;
|
|
33
|
+
page_count: number;
|
|
34
|
+
freelist_count: number;
|
|
35
|
+
/** `freelist_count * page_size`: what `VACUUM` returns to the filesystem. */
|
|
36
|
+
reclaimable: number;
|
|
37
|
+
}
|
|
38
|
+
export interface SizeReport {
|
|
39
|
+
file: SizeFile;
|
|
40
|
+
/** Always the four categories, in the fixed order content, history, search, sync. */
|
|
41
|
+
categories: SizeCategoryReport[];
|
|
42
|
+
/** spec/store §7 (14.2): `dispositions` rows with `kind = 'same'` — what a store through 14.1 wrote and `omg gc` removes. */
|
|
43
|
+
same_dispositions: number;
|
|
44
|
+
advice: string[];
|
|
45
|
+
/** Present only when the SQLite build lacks `dbstat` (row counts only). */
|
|
46
|
+
detail?: "dbstat unavailable";
|
|
47
|
+
}
|
|
48
|
+
/** The advice thresholds (spec/store §7). */
|
|
49
|
+
export declare const VACUUM_ADVICE_MIN_BYTES: number;
|
|
50
|
+
export declare const VACUUM_ADVICE_MIN_SHARE = 10;
|
|
51
|
+
export declare const HISTORY_ADVICE_MIN_SHARE = 50;
|
|
52
|
+
export declare const SAME_ADVICE_MIN_SHARE = 25;
|
|
53
|
+
export declare const EMBEDDINGS_ADVICE_MIN_SHARE = 25;
|
|
54
|
+
/**
|
|
55
|
+
* `<n> B` under 1024, else one decimal of KB/MB/GB/TB (1024-based), the tenths
|
|
56
|
+
* rounded half up — `4096` → `4.0 KB`, `13734297` → `13.1 MB`. Both engines
|
|
57
|
+
* format from the integer tenths so the bytes never differ by a rounding mode.
|
|
58
|
+
*/
|
|
59
|
+
export declare function humanBytes(n: number): string;
|
|
60
|
+
/** A share as `<n>.<d>%` from its one-decimal value (`48.2` → `48.2%`, `50` → `50.0%`). */
|
|
61
|
+
export declare function formatShare(share: number): string;
|
|
62
|
+
/** `bytes / total` in percent, one decimal, rounded half up; 0 when the total is 0. */
|
|
63
|
+
export declare function shareOf(bytes: number, total: number): number;
|
|
64
|
+
/** spec/store §7: the advice lines, in this order; `nothing to reclaim` when none applies. */
|
|
65
|
+
export declare function sizeAdvice(file: SizeFile, categories: SizeCategoryReport[], sameDispositions?: number): string[];
|
|
66
|
+
/** The size report of an open store (spec/store §7 "Size"). */
|
|
67
|
+
export declare function sizeReport(store: Store): SizeReport;
|
|
68
|
+
export interface VacuumResult {
|
|
69
|
+
/** `freelist_count * page_size` before the `VACUUM`: the bytes it returned. */
|
|
70
|
+
reclaimable: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `VACUUM` the database (spec/store §7). Not a transaction's work — SQLite
|
|
74
|
+
* refuses a VACUUM inside one — so it runs on the bare connection; returns the
|
|
75
|
+
* free-page bytes measured before, which is what the file shrinks by.
|
|
76
|
+
*/
|
|
77
|
+
export declare function vacuum(store: Store): VacuumResult;
|
|
78
|
+
/** `freelist_count * page_size`: the bytes a `VACUUM` would return now. */
|
|
79
|
+
export declare function reclaimableBytes(store: Store): number;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
// The size report (spec/store §7 "Size", 14.1): what the database is made of,
|
|
3
|
+
// table by table through SQLite's `dbstat` virtual table, grouped into the four
|
|
4
|
+
// categories a reader reasons about — the live state (`content`), the
|
|
5
|
+
// append-only record (`history`), the derived indexes (`search`) and the
|
|
6
|
+
// workspace bookkeeping (`sync`) — plus the file's page accounting and the
|
|
7
|
+
// deterministic advice `omg doctor --size` prints. The categorization is the
|
|
8
|
+
// spec's: every table of `schema.sql` is assigned, an index counts toward its
|
|
9
|
+
// table, an FTS5 virtual table's shadow tables fold into it, and anything the
|
|
10
|
+
// spec does not name (`sqlite_schema`, `sqlite_stat1`, a table from a newer
|
|
11
|
+
// schema) is `sync`.
|
|
12
|
+
export const SIZE_CATEGORIES = ["content", "history", "search", "sync"];
|
|
13
|
+
/** spec/store §7: the category of every table `schema.sql` creates. */
|
|
14
|
+
export const TABLE_CATEGORY = {
|
|
15
|
+
docs: "content",
|
|
16
|
+
blocks: "content",
|
|
17
|
+
sections: "content",
|
|
18
|
+
nodes: "content",
|
|
19
|
+
edges: "content",
|
|
20
|
+
doc_edges: "content",
|
|
21
|
+
inferred_edges: "content",
|
|
22
|
+
external_nodes: "content",
|
|
23
|
+
properties: "content",
|
|
24
|
+
collections: "content",
|
|
25
|
+
blobs: "history",
|
|
26
|
+
tree_nodes: "history",
|
|
27
|
+
revisions: "history",
|
|
28
|
+
commits: "history",
|
|
29
|
+
dispositions: "history",
|
|
30
|
+
block_changes: "history",
|
|
31
|
+
resurrection_pool: "history",
|
|
32
|
+
pending_materializations: "history",
|
|
33
|
+
blocks_fts: "search",
|
|
34
|
+
nodes_fts: "search",
|
|
35
|
+
embeddings: "search",
|
|
36
|
+
doc_embeddings: "search",
|
|
37
|
+
repos: "sync",
|
|
38
|
+
sources: "sync",
|
|
39
|
+
adapters: "sync",
|
|
40
|
+
attachments: "sync",
|
|
41
|
+
checkpoints: "sync",
|
|
42
|
+
file_stats: "sync",
|
|
43
|
+
sync_state: "sync",
|
|
44
|
+
workspace_settings: "sync",
|
|
45
|
+
};
|
|
46
|
+
/** The category of a table: the spec's assignment, `sync` for anything else. */
|
|
47
|
+
export function categoryOf(table) {
|
|
48
|
+
return TABLE_CATEGORY[table] ?? "sync";
|
|
49
|
+
}
|
|
50
|
+
/** The advice thresholds (spec/store §7). */
|
|
51
|
+
export const VACUUM_ADVICE_MIN_BYTES = 1024 * 1024;
|
|
52
|
+
export const VACUUM_ADVICE_MIN_SHARE = 10;
|
|
53
|
+
export const HISTORY_ADVICE_MIN_SHARE = 50;
|
|
54
|
+
export const SAME_ADVICE_MIN_SHARE = 25;
|
|
55
|
+
export const EMBEDDINGS_ADVICE_MIN_SHARE = 25;
|
|
56
|
+
/**
|
|
57
|
+
* `<n> B` under 1024, else one decimal of KB/MB/GB/TB (1024-based), the tenths
|
|
58
|
+
* rounded half up — `4096` → `4.0 KB`, `13734297` → `13.1 MB`. Both engines
|
|
59
|
+
* format from the integer tenths so the bytes never differ by a rounding mode.
|
|
60
|
+
*/
|
|
61
|
+
export function humanBytes(n) {
|
|
62
|
+
if (n < 1024)
|
|
63
|
+
return `${n} B`;
|
|
64
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
65
|
+
let v = n / 1024;
|
|
66
|
+
let u = 0;
|
|
67
|
+
while (v >= 1024 && u < units.length - 1) {
|
|
68
|
+
v /= 1024;
|
|
69
|
+
u++;
|
|
70
|
+
}
|
|
71
|
+
const tenths = Math.round(v * 10);
|
|
72
|
+
return `${Math.floor(tenths / 10)}.${tenths % 10} ${units[u]}`;
|
|
73
|
+
}
|
|
74
|
+
/** A share as `<n>.<d>%` from its one-decimal value (`48.2` → `48.2%`, `50` → `50.0%`). */
|
|
75
|
+
export function formatShare(share) {
|
|
76
|
+
const tenths = Math.round(share * 10);
|
|
77
|
+
return `${Math.floor(tenths / 10)}.${tenths % 10}%`;
|
|
78
|
+
}
|
|
79
|
+
/** `bytes / total` in percent, one decimal, rounded half up; 0 when the total is 0. */
|
|
80
|
+
export function shareOf(bytes, total) {
|
|
81
|
+
if (total <= 0)
|
|
82
|
+
return 0;
|
|
83
|
+
return Math.round((bytes * 1000) / total) / 10;
|
|
84
|
+
}
|
|
85
|
+
/** spec/store §7: the advice lines, in this order; `nothing to reclaim` when none applies. */
|
|
86
|
+
export function sizeAdvice(file, categories, sameDispositions = 0) {
|
|
87
|
+
const advice = [];
|
|
88
|
+
const logical = file.page_count * file.page_size;
|
|
89
|
+
if (file.reclaimable > VACUUM_ADVICE_MIN_BYTES && file.reclaimable * 100 > logical * VACUUM_ADVICE_MIN_SHARE) {
|
|
90
|
+
advice.push(`run \`omg gc --vacuum\` to return ${humanBytes(file.reclaimable)} of free pages to the filesystem`);
|
|
91
|
+
}
|
|
92
|
+
const history = categories.find((c) => c.name === "history");
|
|
93
|
+
if (history && history.share !== null && history.share > HISTORY_ADVICE_MIN_SHARE) {
|
|
94
|
+
advice.push(`history dominates (${formatShare(history.share)}): blobs, tree nodes and dispositions grow with every commit — enable gc (\`omg config set gc.enabled true\`) and run \`omg gc --vacuum\``);
|
|
95
|
+
}
|
|
96
|
+
// 14.2: `same` rows are what a 14.1 store wrote for every untouched block;
|
|
97
|
+
// past a quarter of the table they are worth a sweep (the share is over rows,
|
|
98
|
+
// so it holds with or without dbstat).
|
|
99
|
+
const dispositionRows = history?.tables.find((t) => t.name === "dispositions")?.rows ?? 0;
|
|
100
|
+
if (sameDispositions > 0 && sameDispositions * 100 > dispositionRows * SAME_ADVICE_MIN_SHARE) {
|
|
101
|
+
advice.push(`${sameDispositions} same dispositions are history nothing reads (store 14.2): \`omg gc\` removes them, \`--vacuum\` reclaims the pages`);
|
|
102
|
+
}
|
|
103
|
+
const search = categories.find((c) => c.name === "search");
|
|
104
|
+
if (search && search.bytes !== null) {
|
|
105
|
+
const total = categories.reduce((n, c) => n + (c.bytes ?? 0), 0);
|
|
106
|
+
const embBytes = search.tables.filter((t) => t.name === "embeddings" || t.name === "doc_embeddings").reduce((n, t) => n + (t.bytes ?? 0), 0);
|
|
107
|
+
const embShare = shareOf(embBytes, total);
|
|
108
|
+
if (embShare > EMBEDDINGS_ADVICE_MIN_SHARE) {
|
|
109
|
+
advice.push(`embeddings are ${formatShare(embShare)} of the database: a smaller model (\`embedding.model\` / \`embedding.dim\`) or \`omg embed drain --prune\` after a model switch shrinks them`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (advice.length === 0)
|
|
113
|
+
advice.push("nothing to reclaim");
|
|
114
|
+
return advice;
|
|
115
|
+
}
|
|
116
|
+
/** The `dbstat` bytes per b-tree name, or null when the build lacks the virtual table. */
|
|
117
|
+
function dbstatBytes(store) {
|
|
118
|
+
try {
|
|
119
|
+
const rows = store.db.prepare("SELECT name, sum(pgsize) AS bytes FROM dbstat GROUP BY name").all();
|
|
120
|
+
return new Map(rows.map((r) => [r.name, Number(r.bytes)]));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** The size report of an open store (spec/store §7 "Size"). */
|
|
127
|
+
export function sizeReport(store) {
|
|
128
|
+
const db = store.db;
|
|
129
|
+
const path = db.name === "" ? ":memory:" : db.name;
|
|
130
|
+
const page_size = db.pragma("page_size", { simple: true });
|
|
131
|
+
const page_count = db.pragma("page_count", { simple: true });
|
|
132
|
+
const freelist_count = db.pragma("freelist_count", { simple: true });
|
|
133
|
+
const onDisk = path !== ":memory:";
|
|
134
|
+
const fileBytes = (p) => {
|
|
135
|
+
try {
|
|
136
|
+
return statSync(p).size;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const file = {
|
|
143
|
+
path,
|
|
144
|
+
bytes: onDisk ? fileBytes(path) : page_count * page_size,
|
|
145
|
+
wal_bytes: onDisk ? fileBytes(`${path}-wal`) : 0,
|
|
146
|
+
page_size,
|
|
147
|
+
page_count,
|
|
148
|
+
freelist_count,
|
|
149
|
+
reclaimable: freelist_count * page_size,
|
|
150
|
+
};
|
|
151
|
+
const master = db.prepare("SELECT type, name, tbl_name, sql FROM sqlite_master").all();
|
|
152
|
+
const virtuals = master.filter((r) => r.type === "table" && /^CREATE VIRTUAL TABLE/i.test(r.sql ?? "")).map((r) => r.name);
|
|
153
|
+
// A shadow table (`blocks_fts_data`, …) folds into its virtual table.
|
|
154
|
+
const ownerOf = (name) => virtuals.find((v) => name.startsWith(`${v}_`)) ?? name;
|
|
155
|
+
const stat = dbstatBytes(store);
|
|
156
|
+
// Every table: sqlite_master's (shadow tables folded into their owner), plus
|
|
157
|
+
// the schema table itself, which sqlite_master does not list but dbstat does.
|
|
158
|
+
const tableNames = new Set(["sqlite_schema"]);
|
|
159
|
+
for (const r of master)
|
|
160
|
+
if (r.type === "table")
|
|
161
|
+
tableNames.add(ownerOf(r.name));
|
|
162
|
+
if (stat)
|
|
163
|
+
for (const name of stat.keys())
|
|
164
|
+
if (!master.some((r) => r.name === name && r.type === "index"))
|
|
165
|
+
tableNames.add(ownerOf(name));
|
|
166
|
+
const indexBytes = new Map();
|
|
167
|
+
const tableBytes = new Map();
|
|
168
|
+
if (stat) {
|
|
169
|
+
for (const [name, bytes] of stat) {
|
|
170
|
+
const idx = master.find((r) => r.type === "index" && r.name === name);
|
|
171
|
+
if (idx) {
|
|
172
|
+
indexBytes.set(idx.tbl_name, (indexBytes.get(idx.tbl_name) ?? 0) + bytes);
|
|
173
|
+
tableBytes.set(idx.tbl_name, (tableBytes.get(idx.tbl_name) ?? 0) + bytes);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const owner = ownerOf(name);
|
|
177
|
+
tableBytes.set(owner, (tableBytes.get(owner) ?? 0) + bytes);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const total = stat ? [...stat.values()].reduce((n, b) => n + b, 0) : 0;
|
|
182
|
+
const countRows = (name) => {
|
|
183
|
+
// An external-content FTS5 table answers `count(*)` from its content table;
|
|
184
|
+
// the rows it indexes are its `_docsize` shadow's.
|
|
185
|
+
const from = virtuals.includes(name) ? `${name}_docsize` : name;
|
|
186
|
+
try {
|
|
187
|
+
return db.prepare(`SELECT count(*) AS c FROM "${from.replace(/"/g, '""')}"`).get().c;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const byCategory = new Map(SIZE_CATEGORIES.map((c) => [c, []]));
|
|
194
|
+
for (const name of tableNames) {
|
|
195
|
+
const bytes = stat ? (tableBytes.get(name) ?? 0) : null;
|
|
196
|
+
byCategory.get(categoryOf(name)).push({
|
|
197
|
+
name,
|
|
198
|
+
bytes,
|
|
199
|
+
share: bytes === null ? null : shareOf(bytes, total),
|
|
200
|
+
rows: countRows(name),
|
|
201
|
+
indexes: stat ? (indexBytes.get(name) ?? 0) : null,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const byBytesThenName = (a, b) => (b.bytes ?? b.rows) - (a.bytes ?? a.rows) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
205
|
+
const categories = SIZE_CATEGORIES.map((name) => {
|
|
206
|
+
const tables = byCategory.get(name).sort(byBytesThenName);
|
|
207
|
+
const bytes = stat ? tables.reduce((n, t) => n + (t.bytes ?? 0), 0) : null;
|
|
208
|
+
return { name, bytes, share: bytes === null ? null : shareOf(bytes, total), rows: tables.reduce((n, t) => n + t.rows, 0), tables };
|
|
209
|
+
});
|
|
210
|
+
const same_dispositions = db.prepare("SELECT count(*) AS c FROM dispositions WHERE kind = 'same'").get().c;
|
|
211
|
+
const report = { file, categories, same_dispositions, advice: sizeAdvice(file, categories, same_dispositions) };
|
|
212
|
+
if (!stat)
|
|
213
|
+
report.detail = "dbstat unavailable";
|
|
214
|
+
return report;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* `VACUUM` the database (spec/store §7). Not a transaction's work — SQLite
|
|
218
|
+
* refuses a VACUUM inside one — so it runs on the bare connection; returns the
|
|
219
|
+
* free-page bytes measured before, which is what the file shrinks by.
|
|
220
|
+
*/
|
|
221
|
+
export function vacuum(store) {
|
|
222
|
+
const reclaimable = reclaimableBytes(store);
|
|
223
|
+
store.db.exec("VACUUM");
|
|
224
|
+
return { reclaimable };
|
|
225
|
+
}
|
|
226
|
+
/** `freelist_count * page_size`: the bytes a `VACUUM` would return now. */
|
|
227
|
+
export function reclaimableBytes(store) {
|
|
228
|
+
const page_size = store.db.pragma("page_size", { simple: true });
|
|
229
|
+
const freelist_count = store.db.pragma("freelist_count", { simple: true });
|
|
230
|
+
return freelist_count * page_size;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=size.js.map
|
package/dist/src/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export { createExternalSource, type ExternalSourceSpec } from "./sync/external-s
|
|
|
28
28
|
export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, type AdapterRow, type SourceRow, } from "./sync/sources.js";
|
|
29
29
|
export { reconcileChanges, attachSource } from "./sync/driver.js";
|
|
30
30
|
export { observeFile, observeMany, observeOne, observeDelete, observeBatch, type ObserveResult, type ObserveOneResult, type ObserveDeleteResult, type BatchItem, type BatchOutcome } from "./sync/observe.js";
|
|
31
|
-
export { buildServer, WRITE_TOOLS, type ServerContext } from "./mcp/server.js";
|
|
31
|
+
export { buildServer, toolDescriptions, toolNames, unknownTool, UNKNOWN_TOOL_CODE, WRITE_TOOLS, MINIMAL_TOOLS, PROFILES, TOOLS_PROFILES, parseToolsProfile, type ServerContext, type ToolsProfile } from "./mcp/server.js";
|
|
32
32
|
export { serveStdio, type ServeStdioHandle } from "./mcp/stdio.js";
|
|
33
33
|
export { processCheckpoint, type CheckpointResult } from "./sync/checkpoint.js";
|
|
34
34
|
export { reposStatus, syncStatus, reposList, type RepoStatus, type SyncStatus, type DiskStatus, type ReposList } from "./sync/admin.js";
|
|
@@ -67,9 +67,12 @@ export { canonicalDocPath, composeFile, splitFrontmatter, planInboundRetarget }
|
|
|
67
67
|
export { createEffects, moveDocEffects, deleteDocEffects, setMetaEffects, updateContentEffects, insertEffects, updateBlockEffects, moveBlocksEffects, removeBlocksEffects, splitEffects, mergeEffects, checkEffects, nodeSetEffects, appendEffects, docsAppendEffects, retargetEffects, guardOf, requireAbsentPath, applySugar, createdDoc, docOutcome, moveLinks, type Sugar, type SugarDoc, type SugarRequest, type DocOpOutcome, type MoveLinks, } from "./mutate/sugar.js";
|
|
68
68
|
export { planEffects, executePlan, executeByDigest, applyEffects, applyEffectsDetailed, substituteSymbols, admitPlan, canonicalJson, planDigest, PLAN_VERSION, PLANNER_V, type Plan, type PlanCore, type PlanOpset, type PlanPreconditions, type PlanPreview, type Receipt, type PlanEffectsRequest, type ExecuteRequest, } from "./mutate/plan.js";
|
|
69
69
|
export { validateEffects, EFFECT_OPS, type Effect, type EffectTarget, type EffectExpect, type ResolvedEffect, type CreateEntry, type RetargetPreview } from "./mutate/effects.js";
|
|
70
|
+
export { compileSelection, selectionQuery, SELECTION_CEILING, type Selection, type SelectionExpect, type CompiledSelection } from "./mutate/select.js";
|
|
71
|
+
export { oqxRunQuery, reservedAlias } from "./oqx-js/run.js";
|
|
70
72
|
export { inboundLinksTo, retargetLinksInRaw, type InboundLink } from "./graph/inbound-links.js";
|
|
71
73
|
export { planUpdate, type PlanUpdateOptions } from "./mutate/plan-update.js";
|
|
72
74
|
export { opsetKernelOps, renderOpsetPlan, renderIdentityPlan, type IdentityPreview, serializeOpset, parseOpset, type Opset, type PlanOp, type OpsetSummary, type OpsetPrecondition, type PlanDisposition, } from "./mutate/opset.js";
|
|
73
75
|
export { rebuildIndex, type RebuildTarget } from "./core/store/rebuild.js";
|
|
74
76
|
export { runGc, type GcResult } from "./core/store/gc.js";
|
|
77
|
+
export { sizeReport, sizeAdvice, vacuum, reclaimableBytes, humanBytes, formatShare, shareOf, categoryOf, TABLE_CATEGORY, SIZE_CATEGORIES, type SizeReport, type SizeCategory, type SizeCategoryReport, type SizeTable, type SizeFile, type VacuumResult, } from "./core/store/size.js";
|
|
75
78
|
export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
|
package/dist/src/index.js
CHANGED
|
@@ -28,7 +28,7 @@ export { createExternalSource } from "./sync/external-source.js";
|
|
|
28
28
|
export { ensureAdapter, listAdapters, createSource, deleteSource, listSources, getSourceByName, attachSourceToRepo, detachSourceFromRepo, sourcesForRepo, renderConfigFlags, } from "./sync/sources.js";
|
|
29
29
|
export { reconcileChanges, attachSource } from "./sync/driver.js";
|
|
30
30
|
export { observeFile, observeMany, observeOne, observeDelete, observeBatch } from "./sync/observe.js";
|
|
31
|
-
export { buildServer, WRITE_TOOLS } from "./mcp/server.js";
|
|
31
|
+
export { buildServer, toolDescriptions, toolNames, unknownTool, UNKNOWN_TOOL_CODE, WRITE_TOOLS, MINIMAL_TOOLS, PROFILES, TOOLS_PROFILES, parseToolsProfile } from "./mcp/server.js";
|
|
32
32
|
export { serveStdio } from "./mcp/stdio.js";
|
|
33
33
|
export { processCheckpoint } from "./sync/checkpoint.js";
|
|
34
34
|
export { reposStatus, syncStatus, reposList } from "./sync/admin.js";
|
|
@@ -78,6 +78,9 @@ export { createEffects, moveDocEffects, deleteDocEffects, setMetaEffects, update
|
|
|
78
78
|
// The Effect layer (ADR-021, spec/mutate §11): plan / execute / apply { effects } and the plan's shapes.
|
|
79
79
|
export { planEffects, executePlan, executeByDigest, applyEffects, applyEffectsDetailed, substituteSymbols, admitPlan, canonicalJson, planDigest, PLAN_VERSION, PLANNER_V, } from "./mutate/plan.js";
|
|
80
80
|
export { validateEffects, EFFECT_OPS } from "./mutate/effects.js";
|
|
81
|
+
// Query-directed planning (spec/mutate §11.8, 3.1): the selector binding behind `plan`/`apply`/`execute { select }`.
|
|
82
|
+
export { compileSelection, selectionQuery, SELECTION_CEILING } from "./mutate/select.js";
|
|
83
|
+
export { oqxRunQuery, reservedAlias } from "./oqx-js/run.js";
|
|
81
84
|
// Inbound-link discovery + destination-aware rewrite (docs_move's dangling report / retarget_inbound).
|
|
82
85
|
export { inboundLinksTo, retargetLinksInRaw } from "./graph/inbound-links.js";
|
|
83
86
|
// Whole-document update: reconcile → opset (the `set content` effect's internal planner since 3.0; `preview.identity` surfaces it).
|
|
@@ -85,5 +88,6 @@ export { planUpdate } from "./mutate/plan-update.js";
|
|
|
85
88
|
export { opsetKernelOps, renderOpsetPlan, renderIdentityPlan, serializeOpset, parseOpset, } from "./mutate/opset.js";
|
|
86
89
|
export { rebuildIndex } from "./core/store/rebuild.js";
|
|
87
90
|
export { runGc } from "./core/store/gc.js";
|
|
91
|
+
export { sizeReport, sizeAdvice, vacuum, reclaimableBytes, humanBytes, formatShare, shareOf, categoryOf, TABLE_CATEGORY, SIZE_CATEGORIES, } from "./core/store/size.js";
|
|
88
92
|
export { LIVE_LEAF_SQL, ftsIndexedRowCount, liveLeafCount } from "./core/store/fts.js";
|
|
89
93
|
//# sourceMappingURL=index.js.map
|
|
@@ -4,8 +4,16 @@ import { EngineError } from "./errors.js";
|
|
|
4
4
|
export type ArgShape = Record<string, z.ZodTypeAny>;
|
|
5
5
|
/** The `(tool takes …)` tail: names in bytewise order, `?` on the optional ones. */
|
|
6
6
|
export declare function argumentsHint(tool: string, shape: ArgShape): string;
|
|
7
|
-
/** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape. */
|
|
8
|
-
export declare function argumentError(tool: string, message: string, shape: ArgShape): EngineError;
|
|
7
|
+
/** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape — and a `fix` (4.5, spec/surface §4) when the corrected call can be spelled. */
|
|
8
|
+
export declare function argumentError(tool: string, message: string, shape: ArgShape, fix?: string): EngineError;
|
|
9
|
+
/** The schema of a string-list argument: an array of strings, or one bare string (normalized to `[s]` by `validateArguments`). */
|
|
10
|
+
export declare function stringList(opts?: {
|
|
11
|
+
min?: number;
|
|
12
|
+
}): z.ZodType<string[]>;
|
|
13
|
+
/** Is this argument a string list (through `optional`/`nullable`)? */
|
|
14
|
+
export declare function isStringList(schema: z.ZodTypeAny): boolean;
|
|
15
|
+
/** The parts of a comma-joined string: split on `,`, trimmed, empty parts dropped. */
|
|
16
|
+
export declare function commaSplit(s: string): string[];
|
|
9
17
|
/**
|
|
10
18
|
* Validate `raw` against `shape`: unknown names first (in bytewise order), then
|
|
11
19
|
* each declared argument (bytewise order) for presence and type. Returns the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { EngineError } from "./errors.js";
|
|
3
|
+
import { canonicalJson } from "../mutate/canonical.js";
|
|
3
4
|
/** The `(tool takes …)` tail: names in bytewise order, `?` on the optional ones. */
|
|
4
5
|
export function argumentsHint(tool, shape) {
|
|
5
6
|
const names = Object.keys(shape).sort(cmpBytes);
|
|
@@ -7,10 +8,33 @@ export function argumentsHint(tool, shape) {
|
|
|
7
8
|
return `${tool} takes no arguments`;
|
|
8
9
|
return `${tool} takes ${names.map((n) => (isOptional(shape[n]) ? `${n}?` : n)).join(", ")}`;
|
|
9
10
|
}
|
|
10
|
-
/** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape. */
|
|
11
|
-
export function argumentError(tool, message, shape) {
|
|
11
|
+
/** A `filter_invalid` for an argument-shape failure: the message plus the tool's shape — and a `fix` (4.5, spec/surface §4) when the corrected call can be spelled. */
|
|
12
|
+
export function argumentError(tool, message, shape, fix) {
|
|
12
13
|
const full = `${message} (${argumentsHint(tool, shape)})`;
|
|
13
|
-
return new EngineError("filter_invalid", full, { data: { reason: full, hint: "arguments" } });
|
|
14
|
+
return new EngineError("filter_invalid", full, { data: { reason: full, hint: "arguments", ...(fix !== undefined ? { fix } : {}) } });
|
|
15
|
+
}
|
|
16
|
+
// ---- string lists (spec/surface §4 "Arguments", 4.5) ---------------------------------
|
|
17
|
+
//
|
|
18
|
+
// Every list-of-strings argument (`docs`, `ids`, `roots`, `blocks`, `select`,
|
|
19
|
+
// `unset`) also takes ONE bare string as a one-element list — the first guess
|
|
20
|
+
// of a caller who has one ref. A comma-joined string is still "must be an array
|
|
21
|
+
// of strings", but that error carries a `fix`: the same call with the string
|
|
22
|
+
// split on commas (each part trimmed, empty parts dropped), canonical JSON.
|
|
23
|
+
const STRING_LISTS = new WeakSet();
|
|
24
|
+
/** The schema of a string-list argument: an array of strings, or one bare string (normalized to `[s]` by `validateArguments`). */
|
|
25
|
+
export function stringList(opts = {}) {
|
|
26
|
+
const list = opts.min !== undefined ? z.array(z.string()).min(opts.min) : z.array(z.string());
|
|
27
|
+
const schema = z.union([list, z.string()]);
|
|
28
|
+
STRING_LISTS.add(schema);
|
|
29
|
+
return schema;
|
|
30
|
+
}
|
|
31
|
+
/** Is this argument a string list (through `optional`/`nullable`)? */
|
|
32
|
+
export function isStringList(schema) {
|
|
33
|
+
return STRING_LISTS.has(unwrap(schema));
|
|
34
|
+
}
|
|
35
|
+
/** The parts of a comma-joined string: split on `,`, trimmed, empty parts dropped. */
|
|
36
|
+
export function commaSplit(s) {
|
|
37
|
+
return s.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
14
38
|
}
|
|
15
39
|
/**
|
|
16
40
|
* Validate `raw` against `shape`: unknown names first (in bytewise order), then
|
|
@@ -36,6 +60,14 @@ export function validateArguments(tool, shape, raw) {
|
|
|
36
60
|
throw argumentError(tool, `\`${key}\` is required`, shape);
|
|
37
61
|
continue;
|
|
38
62
|
}
|
|
63
|
+
if (isStringList(schema) && typeof value === "string") {
|
|
64
|
+
// (4.5) one bare string is a one-element list; a comma-joined one is the
|
|
65
|
+
// array error with the split list as its `fix`.
|
|
66
|
+
if (value.includes(","))
|
|
67
|
+
throw argumentError(tool, `\`${key}\` must be an array of strings`, shape, canonicalJson({ ...args, [key]: commaSplit(value) }));
|
|
68
|
+
out[key] = [value];
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
39
71
|
const parsed = schema.safeParse(value);
|
|
40
72
|
if (!parsed.success)
|
|
41
73
|
throw argumentError(tool, issueMessage(key, schema, parsed.error.issues), shape);
|
|
@@ -141,6 +173,8 @@ function unwrap(schema) {
|
|
|
141
173
|
}
|
|
142
174
|
/** The expected-type phrase of an argument, as the Rust catalog spells it from its JSON schema. */
|
|
143
175
|
export function phraseFor(schema) {
|
|
176
|
+
if (isStringList(schema))
|
|
177
|
+
return "an array of strings";
|
|
144
178
|
const d = defOf(unwrap(schema));
|
|
145
179
|
switch (d.type) {
|
|
146
180
|
case "string": return "a string";
|
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";
|
|
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";
|
|
2
2
|
export interface EngineErrorBody {
|
|
3
3
|
error: ErrorCode;
|
|
4
4
|
message: string;
|