@omgbase/core 0.10.1 → 0.11.1
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/README.md +10 -10
- package/dist/src/core/read/document.d.ts +18 -5
- package/dist/src/core/read/document.js +21 -7
- package/dist/src/core/read/nodes.d.ts +7 -7
- package/dist/src/core/read/nodes.js +5 -3
- package/dist/src/core/read/reader.d.ts +11 -7
- package/dist/src/core/read/reader.js +8 -10
- package/dist/src/format/adapter.d.ts +1 -1
- package/dist/src/graph/history.d.ts +20 -14
- package/dist/src/graph/history.js +12 -12
- package/dist/src/graph/inbound-links.d.ts +4 -4
- package/dist/src/graph/inbound-links.js +4 -4
- package/dist/src/graph/link-health.d.ts +8 -8
- package/dist/src/graph/link-health.js +6 -6
- package/dist/src/graph/links.d.ts +3 -3
- package/dist/src/graph/links.js +4 -4
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/mcp/arguments.d.ts +27 -0
- package/dist/src/mcp/arguments.js +147 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/graph.d.ts +16 -9
- package/dist/src/mcp/graph.js +58 -35
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +200 -111
- package/dist/src/mcp/server.d.ts +9 -1
- package/dist/src/mcp/server.js +359 -268
- package/dist/src/mutate/apply.d.ts +11 -4
- package/dist/src/mutate/apply.js +3 -2
- package/dist/src/mutate/docs.js +7 -7
- package/dist/src/mutate/macros.d.ts +4 -1
- package/dist/src/mutate/macros.js +1 -1
- package/dist/src/mutate/ops.js +2 -2
- package/dist/src/mutate/opset.d.ts +8 -5
- package/dist/src/mutate/plan-update.js +9 -8
- package/dist/src/oqx-js/context.d.ts +17 -4
- package/dist/src/oqx-js/context.js +268 -131
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +14 -15
- package/dist/src/oqx-js/root-row.d.ts +10 -3
- package/dist/src/oqx-js/root-row.js +18 -8
- package/dist/src/oqx-js/run.d.ts +6 -0
- package/dist/src/oqx-js/run.js +72 -25
- package/dist/src/oqx-js/sql/scan.js +7 -7
- package/dist/src/oqx-js/sql/translate.d.ts +12 -9
- package/dist/src/oqx-js/sql/translate.js +183 -75
- package/dist/src/oqx-js/store-index.d.ts +1 -1
- package/dist/src/oqx-js/store-index.js +28 -26
- package/dist/src/search/cel/ast.d.ts +1 -1
- package/dist/src/search/cel/compile.d.ts +1 -1
- package/dist/src/search/cel/compile.js +43 -70
- package/dist/src/search/cel/parser.js +1 -1
- package/dist/src/search/resolve.d.ts +2 -0
- package/dist/src/search/rrf.js +2 -2
- package/dist/src/search/text.d.ts +2 -2
- package/dist/src/search/text.js +1 -1
- package/dist/src/spec-versions.d.ts +6 -6
- package/dist/src/spec-versions.js +5 -5
- package/dist/src/surface-paths.d.ts +41 -8
- package/dist/src/surface-paths.js +34 -18
- package/dist/src/sync/admin.d.ts +2 -1
- package/dist/src/sync/admin.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,16 +51,16 @@ const repoId = ensureRepo(ws.store, "notes", root); // repo row + an `fs` sour
|
|
|
51
51
|
const sweep = freshnessSweep(ws.store, repoId, root); // ingest new/changed *.md as one checkpoint
|
|
52
52
|
sweep.ingested; // ["todo.md"]
|
|
53
53
|
|
|
54
|
-
// Query with OQX. Nodes are projections of blocks;
|
|
54
|
+
// Query with OQX. Nodes are projections of blocks; block_id is the anchor block.
|
|
55
55
|
const open = oqxRun(ws.store, repoId,
|
|
56
|
-
'select block:
|
|
57
|
-
open.hits; // [{ id: "n_…", path: "todo.md", block: "b_…" }]
|
|
56
|
+
'select block: block_id from nodes where kind == "md:task" && !attrs.checked');
|
|
57
|
+
open.hits; // [{ id: "n_…", path: "/todo.md", block: "b_…" }]
|
|
58
58
|
|
|
59
59
|
// Read a whole document back byte-exact, with block ids + CAS hashes.
|
|
60
60
|
const doc = findDoc(ws.store, { repoId, path: "todo.md" });
|
|
61
61
|
const read = docsRead(ws.store, doc.docId, { includeIds: true });
|
|
62
|
-
read.
|
|
63
|
-
read.ids; // ["b_…", "b_…", "b_…", "b_…"] read.hashes[id] → content_hash
|
|
62
|
+
read.raw; // "# Todo\n\n- [ ] write the README\n- [x] ship 0.1\n"
|
|
63
|
+
read.ids; // ["b_…", "b_…", "b_…", "b_…"] read.hashes[id] → the block's hash (the expect.content_hash value)
|
|
64
64
|
|
|
65
65
|
// Mutate. The macro expands to kernel `update` ops pinned by content_hash;
|
|
66
66
|
// apply() renders, writes the file, and commits a new revision.
|
|
@@ -71,7 +71,7 @@ const result = apply(ws.store, {
|
|
|
71
71
|
ops: tasksComplete(ws.store, open.hits.map((h) => h.block)),
|
|
72
72
|
origin: { actor: "agent:quickstart", reason: "demo" },
|
|
73
73
|
});
|
|
74
|
-
result; // { results: [{ ids: ["b_…"] }], revisions: [{
|
|
74
|
+
result; // { results: [{ ids: ["b_…"] }], revisions: [{ id: "d_…", path: "todo.md", hash: "…" }], committed: true }
|
|
75
75
|
// todo.md on disk now reads "- [x] write the README"
|
|
76
76
|
|
|
77
77
|
historyNode(ws.store, open.hits[0].block); // [{ origin: "import", kind: "edited", … }, { origin: "observed", kind: "inserted", … }]
|
|
@@ -126,7 +126,7 @@ server in your own process with `buildServer(ctx)` and any MCP SDK transport.
|
|
|
126
126
|
3. A doc is an ordered tree of **blocks** (`b_`): paragraphs, headings, list items, fences, YAML entries, JSON properties…
|
|
127
127
|
4. Block ids are minted once and threaded through re-ingests by the reconciler, so they survive edits and moves.
|
|
128
128
|
5. Blocks are the **mutation anchors**: every kernel op addresses `b_` ids; `expect.content_hash` (sha256 of the block's raw bytes) is the CAS token.
|
|
129
|
-
6. **Nodes** (`n_`) are semantic projections of blocks — `md:task`, `md:heading`, `md:section`, `md:link`, `md:wikilink`, `md:anchor`, `md:inline_field` — queryable but derived;
|
|
129
|
+
6. **Nodes** (`n_`) are semantic projections of blocks — `md:task`, `md:heading`, `md:section`, `md:link`, `md:wikilink`, `md:anchor`, `md:inline_field` — queryable but derived; `block_id` points back at the anchor.
|
|
130
130
|
7. **Edges** are typed links between blocks/docs (wikilinks, `$ref`, frontmatter relations) with commit intervals.
|
|
131
131
|
8. Every ingest or mutation appends a **commit** (`c_`) and a per-doc **revision** (`r_`) whose rendered hash equals the file's bytes.
|
|
132
132
|
9. Commits carry `origin` (`observed` from disk, `import` from `apply`) and an actor; `historyNode`/`changesSince` read them back.
|
|
@@ -144,11 +144,11 @@ and `semantic("…")` (embedding score) are omgbase additions.
|
|
|
144
144
|
|
|
145
145
|
```js
|
|
146
146
|
oqxRun(ws.store, repoId, `
|
|
147
|
-
select
|
|
147
|
+
select path, title, open: nodes collect { select block_id where kind == "md:task" && !attrs.checked }
|
|
148
148
|
from docs where nodes count { where kind == "md:task" && !attrs.checked } >= 2
|
|
149
|
-
order by
|
|
149
|
+
order by path limit 10
|
|
150
150
|
`);
|
|
151
|
-
// hits: [{ id: "d_…", path: "more.md",
|
|
151
|
+
// hits: [{ id: "d_…", path: "/more.md", title: "More", open: [{ block_id: "b_…" }, …] }]
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
See [`docs/query-language.md`](https://github.com/omgbase/omgbase/blob/main/docs/query-language.md)
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { Database } from "better-sqlite3";
|
|
2
2
|
import type { Store } from "../store/store.js";
|
|
3
|
+
import { type PropertySource } from "../store/properties.js";
|
|
3
4
|
export interface DocsReadResult {
|
|
5
|
+
id: string;
|
|
4
6
|
/** The reference form (`/a/b.md`, spec/surface §1 "Paths"). */
|
|
5
7
|
path: string;
|
|
6
|
-
docId: string;
|
|
7
8
|
rev: string | null;
|
|
8
9
|
/** Properties grouped by source: { frontmatter, inline, computed }. */
|
|
9
10
|
properties: Record<string, Record<string, unknown>>;
|
|
10
|
-
|
|
11
|
+
/** The reconstructed file bytes (spec/store §6.1) — the document's `raw`. */
|
|
12
|
+
raw: string;
|
|
11
13
|
/** Block ids in document order, for follow-up edits (include_ids). */
|
|
12
14
|
ids?: string[];
|
|
13
15
|
/**
|
|
@@ -15,7 +17,7 @@ export interface DocsReadResult {
|
|
|
15
17
|
* `ids` (include_ids). This is the exact value the raw `apply` kernel wants in
|
|
16
18
|
* an update/split/merge/remove op's `expect.content_hash`, so a single read
|
|
17
19
|
* yields both the stable ids AND the CAS tokens to mutate them — no follow-up
|
|
18
|
-
*
|
|
20
|
+
* blocks_read_many(resolution:"full") hydration round trip.
|
|
19
21
|
*/
|
|
20
22
|
hashes?: Record<string, string>;
|
|
21
23
|
/**
|
|
@@ -36,6 +38,15 @@ export interface DocsReadOptions {
|
|
|
36
38
|
* convergence check reloads through this) as well as from a Store.
|
|
37
39
|
*/
|
|
38
40
|
export declare function reconstructContent(db: Database, docId: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* The surface's `properties` (spec/surface §2, 3.0): the `spec/properties` §5
|
|
43
|
+
* grouped shape with the computed group's keys spelled as the attributes that
|
|
44
|
+
* read them — the store writes the computed rows under `$title`/`$tags`
|
|
45
|
+
* (spec/properties §3.3, unchanged), the surface has no `$` names, so
|
|
46
|
+
* `properties.computed` is `{ title, tags }`. Authored groups are untouched (an
|
|
47
|
+
* authored `$x:` key keeps its sigil, as `frontmatter.$x` reads it).
|
|
48
|
+
*/
|
|
49
|
+
export declare function surfaceProperties(grouped: Record<PropertySource, Record<string, unknown>>): Record<string, Record<string, unknown>>;
|
|
39
50
|
/** Read a whole document by id: full file bytes + properties. Null if missing. */
|
|
40
51
|
export declare function docsRead(store: Store, docId: string, opts?: DocsReadOptions): DocsReadResult | null;
|
|
41
52
|
/** Max doc refs honored per docsReadMany call (matches nodesGetMany's cap). */
|
|
@@ -66,11 +77,13 @@ export declare function docsReadMany(store: Store, repoId: string, refs: string[
|
|
|
66
77
|
budgetTokens?: number;
|
|
67
78
|
}): DocsReadManyResult;
|
|
68
79
|
export interface DocsReadAtResult {
|
|
80
|
+
id: string;
|
|
81
|
+
/** The path as of that revision, in the reference form. */
|
|
69
82
|
path: string;
|
|
70
|
-
docId: string;
|
|
71
83
|
/** The revision travelled to (the argument, echoed for confirmation). */
|
|
72
84
|
rev: string;
|
|
73
|
-
|
|
85
|
+
/** The reconstructed file bytes at `rev` — the document's `raw` then. */
|
|
86
|
+
raw: string;
|
|
74
87
|
/** True when the reconstructed bytes' sha256 matched the revision's stored
|
|
75
88
|
* rendered_hash — a byte-faithful round-trip. False flags the fidelity gap
|
|
76
89
|
* described below (doc-level trivia may differ for an old revision). */
|
|
@@ -41,6 +41,20 @@ export function reconstructContent(db, docId) {
|
|
|
41
41
|
}
|
|
42
42
|
return out.join("");
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The surface's `properties` (spec/surface §2, 3.0): the `spec/properties` §5
|
|
46
|
+
* grouped shape with the computed group's keys spelled as the attributes that
|
|
47
|
+
* read them — the store writes the computed rows under `$title`/`$tags`
|
|
48
|
+
* (spec/properties §3.3, unchanged), the surface has no `$` names, so
|
|
49
|
+
* `properties.computed` is `{ title, tags }`. Authored groups are untouched (an
|
|
50
|
+
* authored `$x:` key keeps its sigil, as `frontmatter.$x` reads it).
|
|
51
|
+
*/
|
|
52
|
+
export function surfaceProperties(grouped) {
|
|
53
|
+
const computed = {};
|
|
54
|
+
for (const [k, v] of Object.entries(grouped.computed))
|
|
55
|
+
computed[k.startsWith("$") ? k.slice(1) : k] = v;
|
|
56
|
+
return { frontmatter: grouped.frontmatter, inline: grouped.inline, computed };
|
|
57
|
+
}
|
|
44
58
|
/** Read a whole document by id: full file bytes + properties. Null if missing. */
|
|
45
59
|
export function docsRead(store, docId, opts = {}) {
|
|
46
60
|
const info = findDoc(store, { docId });
|
|
@@ -50,11 +64,11 @@ export function docsRead(store, docId, opts = {}) {
|
|
|
50
64
|
if (content === null)
|
|
51
65
|
return null;
|
|
52
66
|
const result = {
|
|
67
|
+
id: info.docId,
|
|
53
68
|
path: referencePath(info.path),
|
|
54
|
-
docId: info.docId,
|
|
55
69
|
rev: info.currentRev,
|
|
56
|
-
properties: docPropertiesGrouped(store.db, docId),
|
|
57
|
-
content,
|
|
70
|
+
properties: surfaceProperties(docPropertiesGrouped(store.db, docId)),
|
|
71
|
+
raw: content,
|
|
58
72
|
};
|
|
59
73
|
if (opts.includeIds) {
|
|
60
74
|
const ids = [];
|
|
@@ -81,7 +95,7 @@ export function docsRead(store, docId, opts = {}) {
|
|
|
81
95
|
// analog of docsRead: given several doc refs (each an id OR a path, via the
|
|
82
96
|
// shared findDocByRef dispatch), it returns `items` (the full reads, identical
|
|
83
97
|
// in shape to docsRead) alongside `errors` (per-ref misses). A miss NEVER fails
|
|
84
|
-
// the whole call — mirrors
|
|
98
|
+
// the whole call — mirrors nodesGetMany's tolerance of unknown ids, but made
|
|
85
99
|
// explicit as an errors array so a caller can tell which refs didn't resolve.
|
|
86
100
|
// Duplicate refs collapse first-seen (a repeated ref yields one item). Capped
|
|
87
101
|
// at MANY_DOCS_CAP refs per call (matching nodesGetMany's 100), with the same
|
|
@@ -184,13 +198,13 @@ export function readDocumentAtRevision(store, docId, revId) {
|
|
|
184
198
|
const content = out.join("");
|
|
185
199
|
const renderedHashMatch = sha256(content).equals(rev.rendered_hash);
|
|
186
200
|
return {
|
|
201
|
+
id: docId,
|
|
187
202
|
// The revision records the path as of that revision (docs may be moved).
|
|
188
203
|
path: referencePath(rev.path),
|
|
189
|
-
docId,
|
|
190
204
|
rev: revId,
|
|
191
|
-
content,
|
|
205
|
+
raw: content,
|
|
192
206
|
renderedHashMatch,
|
|
193
|
-
properties: docPropertiesGrouped(db, docId),
|
|
207
|
+
properties: surfaceProperties(docPropertiesGrouped(db, docId)),
|
|
194
208
|
propertiesAreCurrent: true,
|
|
195
209
|
};
|
|
196
210
|
}
|
|
@@ -5,15 +5,15 @@ export interface GetNode {
|
|
|
5
5
|
type: string;
|
|
6
6
|
label?: string;
|
|
7
7
|
text?: string;
|
|
8
|
+
/** The block's raw source bytes (`raw` / `full`). */
|
|
8
9
|
raw?: string;
|
|
9
|
-
/** The block's raw_hash (hex) — the value update/split expect as content_hash
|
|
10
|
-
|
|
10
|
+
/** The block's raw_hash (hex) — the value update/split expect as `expect.content_hash`. */
|
|
11
|
+
hash?: string;
|
|
11
12
|
attrs?: Record<string, unknown>;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
13
|
+
ordinal?: number;
|
|
14
|
+
depth?: number;
|
|
15
|
+
/** The parent block's id, null at the top level (`full`). */
|
|
16
|
+
parent_id?: string | null;
|
|
17
17
|
children?: GetNode[];
|
|
18
18
|
}
|
|
19
19
|
export interface NodesGetOptions {
|
|
@@ -17,14 +17,16 @@ function project(store, node, resolution, includeChildren) {
|
|
|
17
17
|
break;
|
|
18
18
|
case "raw":
|
|
19
19
|
out.raw = blockRaw(store, node.rawHashHex);
|
|
20
|
-
out.
|
|
20
|
+
out.hash = node.rawHashHex;
|
|
21
21
|
break;
|
|
22
22
|
case "full":
|
|
23
23
|
out.raw = blockRaw(store, node.rawHashHex);
|
|
24
|
-
out.
|
|
24
|
+
out.hash = node.rawHashHex;
|
|
25
25
|
out.text = node.text;
|
|
26
26
|
out.attrs = node.attrs;
|
|
27
|
-
out.
|
|
27
|
+
out.ordinal = node.ordinal;
|
|
28
|
+
out.depth = node.depth;
|
|
29
|
+
out.parent_id = node.parentBlock;
|
|
28
30
|
break;
|
|
29
31
|
}
|
|
30
32
|
if (includeChildren && node.children.length > 0) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Store } from "../store/store.js";
|
|
2
2
|
/** One row of the `ls` / docs_list surface: a live document with its block
|
|
3
|
-
* count and last-commit timestamp
|
|
3
|
+
* count and last-commit timestamp, under the row attributes' own names
|
|
4
|
+
* (spec/surface §1.2, 3.0: `id`, `path`, `updated_at`). */
|
|
4
5
|
export interface DocListRow {
|
|
6
|
+
id: string;
|
|
5
7
|
path: string;
|
|
8
|
+
updated_at: string | null;
|
|
6
9
|
blocks: number;
|
|
7
|
-
ts: string | null;
|
|
8
10
|
}
|
|
9
11
|
/** A page of the docs_list surface. Honors the uniform list contract (mcp-api
|
|
10
12
|
* §1): `truncated` is honest, and when true `cursor` resumes after the last
|
|
@@ -32,16 +34,18 @@ export declare const DOCS_LIST_DEFAULT_LIMIT = 200;
|
|
|
32
34
|
* implementation for both surfaces). For a directory-aware summary use
|
|
33
35
|
* `docsTree`. */
|
|
34
36
|
export declare function docsList(store: Store, repoId: string, opts?: DocListOptions): DocListPage;
|
|
35
|
-
/** One entry of the docs_tree surface: a live document (`kind: "doc"
|
|
36
|
-
* collapsed directory (`kind: "dir"`, path ends in
|
|
37
|
-
* depth. `docs`/`blocks` are totals under the entry (a
|
|
38
|
-
* `
|
|
37
|
+
/** One entry of the docs_tree surface: a live document (`kind: "doc"`, with
|
|
38
|
+
* its `id`) or a collapsed directory (`kind: "dir"`, `path` ends in `/`, no
|
|
39
|
+
* id) at the requested depth. `docs`/`blocks` are totals under the entry (a
|
|
40
|
+
* doc counts 1 doc); `updated_at` is the latest last-commit timestamp under
|
|
41
|
+
* it (spec/surface §1.2, 3.0 names). */
|
|
39
42
|
export interface DocTreeEntry {
|
|
43
|
+
id?: string;
|
|
40
44
|
path: string;
|
|
41
45
|
kind: "dir" | "doc";
|
|
42
46
|
docs: number;
|
|
43
47
|
blocks: number;
|
|
44
|
-
|
|
48
|
+
updated_at: string | null;
|
|
45
49
|
}
|
|
46
50
|
export interface DocTreePage {
|
|
47
51
|
/** The normalized directory prefix the tree was taken under, in the
|
|
@@ -23,18 +23,16 @@ function liveDocRows(store, repoId, like, after, limit) {
|
|
|
23
23
|
params.push(limit);
|
|
24
24
|
}
|
|
25
25
|
const rows = store.db
|
|
26
|
-
.prepare(`SELECT d.path AS path,
|
|
27
|
-
(SELECT
|
|
28
|
-
(SELECT
|
|
26
|
+
.prepare(`SELECT d.doc_id AS id, d.path AS path,
|
|
27
|
+
(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = d.current_rev) AS ts,
|
|
28
|
+
(SELECT count(*) FROM blocks b WHERE b.doc_id = d.doc_id AND b.deleted_commit IS NULL) AS blocks
|
|
29
29
|
FROM docs d
|
|
30
30
|
WHERE d.repo_id = ? AND d.deleted_commit IS NULL AND d.path LIKE ? ESCAPE '\\'${where}
|
|
31
31
|
ORDER BY d.path${tail}`)
|
|
32
32
|
.all(...params);
|
|
33
33
|
// The surface speaks the reference form (spec/surface §1 "Paths"): rooted on
|
|
34
34
|
// the way out, the storage form never leaves this module.
|
|
35
|
-
|
|
36
|
-
r.path = referencePath(r.path);
|
|
37
|
-
return rows;
|
|
35
|
+
return rows.map((r) => ({ id: r.id, path: referencePath(r.path), updated_at: r.ts, blocks: r.blocks }));
|
|
38
36
|
}
|
|
39
37
|
// Path-keyset cursors for the path-ordered list surfaces: the shared kernel
|
|
40
38
|
// encoding (core/cursor.ts) with a one-part tuple holding the row's (rooted)
|
|
@@ -115,7 +113,7 @@ export function docsTree(store, repoId, opts = {}) {
|
|
|
115
113
|
total.blocks += row.blocks;
|
|
116
114
|
const segs = storage.slice(prefix.length).split("/");
|
|
117
115
|
if (segs.length <= depth) {
|
|
118
|
-
byPath.set(storage, { path: row.path, kind: "doc", docs: 1, blocks: row.blocks,
|
|
116
|
+
byPath.set(storage, { id: row.id, path: row.path, kind: "doc", docs: 1, blocks: row.blocks, updated_at: row.updated_at });
|
|
119
117
|
continue;
|
|
120
118
|
}
|
|
121
119
|
const dir = referencePath(prefix + segs.slice(0, depth).join("/") + "/");
|
|
@@ -123,11 +121,11 @@ export function docsTree(store, repoId, opts = {}) {
|
|
|
123
121
|
if (cur) {
|
|
124
122
|
cur.docs += 1;
|
|
125
123
|
cur.blocks += row.blocks;
|
|
126
|
-
if (row.
|
|
127
|
-
cur.
|
|
124
|
+
if (row.updated_at !== null && (cur.updated_at === null || row.updated_at > cur.updated_at))
|
|
125
|
+
cur.updated_at = row.updated_at;
|
|
128
126
|
}
|
|
129
127
|
else {
|
|
130
|
-
byPath.set(dir, { path: dir, kind: "dir", docs: 1, blocks: row.blocks,
|
|
128
|
+
byPath.set(dir, { path: dir, kind: "dir", docs: 1, blocks: row.blocks, updated_at: row.updated_at });
|
|
131
129
|
}
|
|
132
130
|
}
|
|
133
131
|
let entries = [...byPath.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
@@ -17,7 +17,7 @@ export interface ProjectedNode {
|
|
|
17
17
|
/**
|
|
18
18
|
* `[spanStart, spanEnd)` of the feature within the block's raw, as JavaScript
|
|
19
19
|
* string indices (UTF-16 code units). Ingest converts to UTF-8 byte offsets
|
|
20
|
-
* before the `nodes` row is written (spec/graph §2.3), and `
|
|
20
|
+
* before the `nodes` row is written (spec/graph §2.3), and `nodes_set`
|
|
21
21
|
* converts back before an editor slices the raw.
|
|
22
22
|
*/
|
|
23
23
|
spanStart?: number | undefined;
|
|
@@ -8,7 +8,7 @@ export interface NodeChange {
|
|
|
8
8
|
confidence: number | null;
|
|
9
9
|
reason: string | null;
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
11
|
+
/** blocks_history: a block's biography from block_changes + commits (06 §3). */
|
|
12
12
|
export declare function historyNode(store: Store, blockId: string, opts?: {
|
|
13
13
|
limit?: number;
|
|
14
14
|
}): NodeChange[];
|
|
@@ -18,15 +18,17 @@ export declare class RevisionNotFound extends Error {
|
|
|
18
18
|
readonly rev: string;
|
|
19
19
|
constructor(docId: string, rev: string);
|
|
20
20
|
}
|
|
21
|
+
/** One block-grain diff entry: the block's `id` (spec/surface §1.2, 3.0) and
|
|
22
|
+
* its raw `before`/`after` as applicable. */
|
|
21
23
|
export interface DiffEntry {
|
|
22
24
|
kind: "added" | "removed" | "changed" | "unchanged";
|
|
23
|
-
|
|
25
|
+
id: string;
|
|
24
26
|
before?: string;
|
|
25
27
|
after?: string;
|
|
26
28
|
}
|
|
27
29
|
export declare function diffBlocks(store: Store, docId: string, fromRev: string, toRev: string): DiffEntry[];
|
|
28
30
|
/**
|
|
29
|
-
* `
|
|
31
|
+
* `docs_diff_unified` (spec/surface §3): a unified diff of the two revisions'
|
|
30
32
|
* **reconstructed file text** — exactly the bytes `docs_read_at` returns for
|
|
31
33
|
* each revision ({@link readDocumentAtRevision}: frontmatter, trivia and
|
|
32
34
|
* top-level order included) — via {@link unifiedDiff}, so hunk line numbers
|
|
@@ -36,16 +38,16 @@ export declare function diffBlocks(store: Store, docId: string, fromRev: string,
|
|
|
36
38
|
* first item (§9). An unknown revision is {@link RevisionNotFound}.
|
|
37
39
|
*/
|
|
38
40
|
export declare function diffUnified(store: Store, docId: string, fromRev: string, toRev: string): string;
|
|
39
|
-
/** The `
|
|
41
|
+
/** The `docs_diff_unified` tool's result (spec/surface §3): the resolved revision pair and the diff. */
|
|
40
42
|
export interface DocDiffUnified {
|
|
41
|
-
|
|
43
|
+
id: string;
|
|
42
44
|
path: string;
|
|
43
45
|
from: string;
|
|
44
46
|
to: string;
|
|
45
47
|
diff: string;
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
|
-
* `
|
|
50
|
+
* `docs_diff_unified` over a resolved document: `to` defaults to the current revision,
|
|
49
51
|
* `from` to the one before it (or the current one again when there is only one:
|
|
50
52
|
* an empty diff). Shared by the MCP tool and `omg diff` so the shape cannot
|
|
51
53
|
* drift. `label` is how the caller named the document, for the error message.
|
|
@@ -95,12 +97,15 @@ export interface CommitDigest {
|
|
|
95
97
|
origin: string;
|
|
96
98
|
actor: string | null;
|
|
97
99
|
summary: string;
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
+
/** The revisions the commit wrote, each the document's row attributes
|
|
101
|
+
* (spec/surface §1.2, 3.0): `id`, `path` as of the commit, `hash` (hex of
|
|
102
|
+
* the revision's rendered_hash) — the hash lets a puller decide "changed vs
|
|
103
|
+
* echo" without a follow-up docs_read/docs_history (ADR-014 §4.2). The same
|
|
104
|
+
* three keys an `apply` revision carries. */
|
|
100
105
|
revisions: {
|
|
101
|
-
|
|
106
|
+
id: string;
|
|
102
107
|
path: string;
|
|
103
|
-
|
|
108
|
+
hash: string;
|
|
104
109
|
}[];
|
|
105
110
|
}
|
|
106
111
|
/** changes_since: commit digests after a cursor (repo commit seq) — the change
|
|
@@ -124,17 +129,18 @@ export interface DocVersion {
|
|
|
124
129
|
rev: string;
|
|
125
130
|
seq: number;
|
|
126
131
|
commit: string;
|
|
127
|
-
ts
|
|
132
|
+
/** the commit's ts — the document's `updated_at` as of this version (spec/surface §1.2, 3.0). */
|
|
133
|
+
updated_at: string;
|
|
128
134
|
origin: string;
|
|
129
135
|
actor: string | null;
|
|
130
|
-
/** hex of the revision's rendered_hash — spot no-op vs real change, feed into docs_read_at/diff. */
|
|
131
|
-
|
|
136
|
+
/** hex of the revision's rendered_hash — the document's `hash` as of this version; spot no-op vs real change, feed into docs_read_at/diff. */
|
|
137
|
+
hash: string;
|
|
132
138
|
/** true when this rev == the doc's current_rev. */
|
|
133
139
|
isCurrent: boolean;
|
|
134
140
|
}
|
|
135
141
|
/** A matching document with its ordered version history. */
|
|
136
142
|
export interface DocVersions {
|
|
137
|
-
|
|
143
|
+
id: string;
|
|
138
144
|
path: string;
|
|
139
145
|
deleted: boolean;
|
|
140
146
|
currentRev: string | null;
|
|
@@ -3,7 +3,7 @@ import { readDocumentAtRevision } from "../core/read/document.js";
|
|
|
3
3
|
import { isValidId } from "../core/ids.js";
|
|
4
4
|
import { EngineError } from "../mcp/errors.js";
|
|
5
5
|
import { referencePath, storagePath } from "../core/paths.js";
|
|
6
|
-
/**
|
|
6
|
+
/** blocks_history: a block's biography from block_changes + commits (06 §3). */
|
|
7
7
|
export function historyNode(store, blockId, opts = {}) {
|
|
8
8
|
const limit = opts.limit ?? 100;
|
|
9
9
|
return store.db
|
|
@@ -36,13 +36,13 @@ export function diffBlocks(store, docId, fromRev, toRev) {
|
|
|
36
36
|
const entries = [];
|
|
37
37
|
for (const [id, raw] of before) {
|
|
38
38
|
if (!after.has(id))
|
|
39
|
-
entries.push({ kind: "removed",
|
|
39
|
+
entries.push({ kind: "removed", id: id, before: raw });
|
|
40
40
|
else if (after.get(id) !== raw)
|
|
41
|
-
entries.push({ kind: "changed",
|
|
41
|
+
entries.push({ kind: "changed", id: id, before: raw, after: after.get(id) });
|
|
42
42
|
}
|
|
43
43
|
for (const [id, raw] of after)
|
|
44
44
|
if (!before.has(id))
|
|
45
|
-
entries.push({ kind: "added",
|
|
45
|
+
entries.push({ kind: "added", id: id, after: raw });
|
|
46
46
|
return entries;
|
|
47
47
|
}
|
|
48
48
|
// Reconstruct the (block id → raw) map for a document at a revision by walking
|
|
@@ -78,7 +78,7 @@ function blocksAtRevision(store, docId, revId) {
|
|
|
78
78
|
return out;
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
|
-
* `
|
|
81
|
+
* `docs_diff_unified` (spec/surface §3): a unified diff of the two revisions'
|
|
82
82
|
* **reconstructed file text** — exactly the bytes `docs_read_at` returns for
|
|
83
83
|
* each revision ({@link readDocumentAtRevision}: frontmatter, trivia and
|
|
84
84
|
* top-level order included) — via {@link unifiedDiff}, so hunk line numbers
|
|
@@ -92,12 +92,12 @@ export function diffUnified(store, docId, fromRev, toRev) {
|
|
|
92
92
|
const at = readDocumentAtRevision(store, docId, rev);
|
|
93
93
|
if (!at)
|
|
94
94
|
throw new RevisionNotFound(docId, rev);
|
|
95
|
-
return at.
|
|
95
|
+
return at.raw;
|
|
96
96
|
};
|
|
97
97
|
return unifiedDiff(rendered(fromRev), rendered(toRev));
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
-
* `
|
|
100
|
+
* `docs_diff_unified` over a resolved document: `to` defaults to the current revision,
|
|
101
101
|
* `from` to the one before it (or the current one again when there is only one:
|
|
102
102
|
* an empty diff). Shared by the MCP tool and `omg diff` so the shape cannot
|
|
103
103
|
* drift. `label` is how the caller named the document, for the error message.
|
|
@@ -113,7 +113,7 @@ export function docDiffUnified(store, docId, opts = {}) {
|
|
|
113
113
|
if (!to || !from)
|
|
114
114
|
throw new EngineError("target_missing", `no revisions to diff for ${opts.label ?? docId}`);
|
|
115
115
|
const path = store.db.prepare("SELECT path FROM docs WHERE doc_id = ?").get(docId)?.path;
|
|
116
|
-
return {
|
|
116
|
+
return { id: docId, path: path === undefined ? "" : referencePath(path), from, to, diff: diffUnified(store, docId, from, to) };
|
|
117
117
|
}
|
|
118
118
|
// ---- unified diff (spec/surface §3) ------------------------------------------
|
|
119
119
|
// A line-grain unified diff with a deterministic Myers script, so both engines
|
|
@@ -276,7 +276,7 @@ export function changesSince(store, repoId, opts = {}) {
|
|
|
276
276
|
// Every path in a digest — the revisions', a deletion's, a move's two, and
|
|
277
277
|
// the summary's — is the reference form (spec/surface §1 "Paths").
|
|
278
278
|
const digests = page.map((c) => {
|
|
279
|
-
const revs = store.db.prepare("SELECT r.doc_id AS doc, r.path AS path, r.rendered_hash AS rendered_hash FROM revisions r WHERE r.commit_id = ?").all(c.commit_id).map((r) => ({
|
|
279
|
+
const revs = store.db.prepare("SELECT r.doc_id AS doc, r.path AS path, r.rendered_hash AS rendered_hash FROM revisions r WHERE r.commit_id = ?").all(c.commit_id).map((r) => ({ id: r.doc, path: referencePath(r.path), hash: r.rendered_hash.toString("hex") }));
|
|
280
280
|
const dispCounts = store.db.prepare("SELECT kind, count(*) n FROM dispositions WHERE commit_id = ? GROUP BY kind").all(c.commit_id);
|
|
281
281
|
// A deletion or a move writes no revision: the tombstone names the commit
|
|
282
282
|
// (`docs.deleted_commit`), a move only its reason (`move <from> -> <to>`).
|
|
@@ -352,13 +352,13 @@ export function docHistory(store, repoId, opts = {}) {
|
|
|
352
352
|
rev: r.rev,
|
|
353
353
|
seq: r.seq,
|
|
354
354
|
commit: r.commitId,
|
|
355
|
-
|
|
355
|
+
updated_at: r.ts,
|
|
356
356
|
origin: r.origin,
|
|
357
357
|
actor: r.actor,
|
|
358
|
-
|
|
358
|
+
hash: r.rendered_hash.toString("hex"),
|
|
359
359
|
isCurrent: r.rev === d.current_rev,
|
|
360
360
|
}));
|
|
361
|
-
return {
|
|
361
|
+
return { id: d.doc_id, path: referencePath(d.path), deleted: d.deleted_commit !== null, currentRev: d.current_rev, versions };
|
|
362
362
|
});
|
|
363
363
|
return { docs, truncated };
|
|
364
364
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
2
|
export interface InboundLink {
|
|
3
3
|
/** source document id */
|
|
4
|
-
|
|
5
|
-
/** source document repo-relative path */
|
|
4
|
+
doc_id: string;
|
|
5
|
+
/** source document repo-relative path (the surface roots it as `path`) */
|
|
6
6
|
path: string;
|
|
7
7
|
/** source block id; null for a frontmatter-level edge */
|
|
8
|
-
|
|
8
|
+
block_id: string | null;
|
|
9
9
|
/** the link's destination path as written (e.g. "/old.md", "../old.md", "old.md") */
|
|
10
10
|
target: string;
|
|
11
11
|
/** #heading / ^ref fragment, if any */
|
|
@@ -19,7 +19,7 @@ export interface InboundLink {
|
|
|
19
19
|
* re-scanned from the block raw so `target`/`anchor` are as written (and so a
|
|
20
20
|
* self-doc pure-fragment link `#H`, which also has dst_node == docId, is NOT
|
|
21
21
|
* reported — it does not depend on the path). Frontmatter edges are reported
|
|
22
|
-
* with `
|
|
22
|
+
* with `block_id: null`, `target: docPath`, and their `field`.
|
|
23
23
|
*/
|
|
24
24
|
export declare function inboundLinksTo(store: Store, repoId: string, docId: string, docPath: string): InboundLink[];
|
|
25
25
|
/**
|
|
@@ -17,7 +17,7 @@ function resolvesTo(dest, srcDir, docPath) {
|
|
|
17
17
|
* re-scanned from the block raw so `target`/`anchor` are as written (and so a
|
|
18
18
|
* self-doc pure-fragment link `#H`, which also has dst_node == docId, is NOT
|
|
19
19
|
* reported — it does not depend on the path). Frontmatter edges are reported
|
|
20
|
-
* with `
|
|
20
|
+
* with `block_id: null`, `target: docPath`, and their `field`.
|
|
21
21
|
*/
|
|
22
22
|
export function inboundLinksTo(store, repoId, docId, docPath) {
|
|
23
23
|
const rows = store.db
|
|
@@ -30,7 +30,7 @@ export function inboundLinksTo(store, repoId, docId, docPath) {
|
|
|
30
30
|
const seenBlocks = new Set();
|
|
31
31
|
for (const r of rows) {
|
|
32
32
|
if (r.srcBlock === null) {
|
|
33
|
-
out.push({
|
|
33
|
+
out.push({ doc_id: r.srcDoc, path: r.srcPath, block_id: null, target: docPath, anchor: null, ...(r.srcField ? { field: r.srcField } : {}) });
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
36
|
if (seenBlocks.has(r.srcBlock))
|
|
@@ -46,9 +46,9 @@ export function inboundLinksTo(store, repoId, docId, docPath) {
|
|
|
46
46
|
if (e.dstKind === "external" || !resolvesTo(e.target, srcDir, docPath))
|
|
47
47
|
continue;
|
|
48
48
|
out.push({
|
|
49
|
-
|
|
49
|
+
doc_id: r.srcDoc,
|
|
50
50
|
path: r.srcPath,
|
|
51
|
-
|
|
51
|
+
block_id: r.srcBlock,
|
|
52
52
|
target: e.target,
|
|
53
53
|
anchor: e.anchor,
|
|
54
54
|
...(e.srcField ? { field: e.srcField } : {}),
|
|
@@ -2,14 +2,14 @@ import type { Store } from "../core/store/store.js";
|
|
|
2
2
|
export type StaleReason = "dangling_doc" | "dangling_block" | "broken_anchor";
|
|
3
3
|
export interface StaleLink {
|
|
4
4
|
/** source document id */
|
|
5
|
-
|
|
6
|
-
/** source document
|
|
7
|
-
|
|
5
|
+
doc_id: string;
|
|
6
|
+
/** source document path in the reference form */
|
|
7
|
+
path: string;
|
|
8
8
|
/** source block id (null for frontmatter-level edges) */
|
|
9
|
-
|
|
9
|
+
block_id: string | null;
|
|
10
10
|
predicate: string;
|
|
11
11
|
provenance: string;
|
|
12
|
-
|
|
12
|
+
dst_kind: string;
|
|
13
13
|
/** the canonical target path in the reference form (phantom prefix stripped,
|
|
14
14
|
* rooted), e.g. "/guides/old.md" — spec/surface §1 "Paths" */
|
|
15
15
|
target: string;
|
|
@@ -29,16 +29,16 @@ export interface LinkHealthSummary {
|
|
|
29
29
|
target: string;
|
|
30
30
|
count: number;
|
|
31
31
|
}[];
|
|
32
|
-
/** source documents carrying dangling links, most first (ties by path) */
|
|
32
|
+
/** source documents carrying dangling links, most first (ties by path); `path` the reference form */
|
|
33
33
|
bySource: {
|
|
34
|
-
|
|
34
|
+
path: string;
|
|
35
35
|
count: number;
|
|
36
36
|
}[];
|
|
37
37
|
externalCount: number;
|
|
38
38
|
totalOpenEdges: number;
|
|
39
39
|
}
|
|
40
40
|
export interface LinkHealth {
|
|
41
|
-
/** dangling internal links, ordered by
|
|
41
|
+
/** dangling internal links, ordered by source path then target */
|
|
42
42
|
stale: StaleLink[];
|
|
43
43
|
/** open external (http(s)) edges — unverifiable, not counted as stale */
|
|
44
44
|
externalCount: number;
|
|
@@ -88,15 +88,15 @@ export function linksStale(store, repoId, opts = {}) {
|
|
|
88
88
|
const stale = page.map((r) => {
|
|
89
89
|
// Strip the "phantom:" prefix to expose the canonical path.
|
|
90
90
|
const target = r.dstNode.slice("phantom:".length);
|
|
91
|
-
// The surface speaks the reference form: `
|
|
91
|
+
// The surface speaks the reference form: `path` and `target` rooted;
|
|
92
92
|
// `authored` stays exactly as written in the source.
|
|
93
93
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
doc_id: r.srcDoc,
|
|
95
|
+
path: referencePath(r.srcPath),
|
|
96
|
+
block_id: r.srcBlock,
|
|
97
97
|
predicate: r.predicate,
|
|
98
98
|
provenance: r.provenance,
|
|
99
|
-
|
|
99
|
+
dst_kind: r.dstKind,
|
|
100
100
|
target: referencePath(target),
|
|
101
101
|
authored: authoredFor(r.srcBlock, r.srcPath, target, r.anchor),
|
|
102
102
|
anchor: r.anchor,
|
|
@@ -140,7 +140,7 @@ export function linksStaleSummary(store, repoId, opts = {}) {
|
|
|
140
140
|
const bySource = store.db
|
|
141
141
|
.prepare(`SELECT d.path AS srcPath, count(*) AS count ${phantom} GROUP BY d.path ORDER BY count DESC, d.path`)
|
|
142
142
|
.all(...params)
|
|
143
|
-
.map((r) => ({
|
|
143
|
+
.map((r) => ({ path: referencePath(r.srcPath), count: r.count }));
|
|
144
144
|
return {
|
|
145
145
|
staleCount: byTarget.reduce((n, t) => n + t.count, 0),
|
|
146
146
|
byTarget,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { Store } from "../core/store/store.js";
|
|
2
2
|
export interface LinkEdge {
|
|
3
3
|
predicate: string;
|
|
4
|
-
/** the other endpoint (dst for out-edges, src for in-edges) */
|
|
5
|
-
|
|
4
|
+
/** the other endpoint (dst for out-edges, src for in-edges): a doc id, an external node id or `phantom:<path>` */
|
|
5
|
+
doc_id: string;
|
|
6
6
|
kind: string;
|
|
7
7
|
/** how many underlying block-grain edges this doc-grain row rolls up */
|
|
8
8
|
count: number;
|
|
9
9
|
/** sample source block ids (out-edges only) */
|
|
10
10
|
samples?: string[];
|
|
11
11
|
/** block-grain (`blocks: true`): the source block of this edge (null for a frontmatter edge) */
|
|
12
|
-
|
|
12
|
+
block_id?: string | null;
|
|
13
13
|
}
|
|
14
14
|
export interface LinksResult {
|
|
15
15
|
out: LinkEdge[];
|