@gmickel/gno 2.7.1 → 2.8.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 +3 -2
- package/assets/skill/SKILL.md +11 -1
- package/assets/skill/cli-reference.md +8 -1
- package/assets/skill/examples.md +2 -1
- package/assets/skill/mcp-reference.md +3 -1
- package/assets/skill/recipes/memory-scoped-recall.md +10 -5
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.1.zip → gno-browser-clipper-v2.8.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +86 -9
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +26 -7
- package/spec/output-schemas/audit-report.schema.json +18 -4
- package/spec/output-schemas/backlinks.schema.json +4 -0
- package/spec/output-schemas/collection-list.schema.json +13 -0
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/memory-recall.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +18 -3
- package/src/cli/commands/audit.ts +23 -4
- package/src/cli/commands/collection/list.ts +39 -5
- package/src/cli/commands/embed.ts +3 -3
- package/src/cli/commands/graph.ts +3 -1
- package/src/cli/commands/links.ts +61 -180
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +6 -0
- package/src/cli/program.ts +12 -2
- package/src/config/loader.ts +43 -0
- package/src/config/types.ts +8 -0
- package/src/core/audit-contract.ts +16 -4
- package/src/core/audit-freshness.ts +11 -1
- package/src/core/audit-links.ts +197 -25
- package/src/core/audit-outside-index.ts +215 -0
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +30 -9
- package/src/core/audit.ts +76 -16
- package/src/core/context-compiler.ts +3 -0
- package/src/core/context-evidence.ts +11 -0
- package/src/core/graph-edge-confidence.ts +23 -1
- package/src/core/host-paths.ts +1 -0
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-inventory-markdown.ts +2 -3
- package/src/core/link-workspace.ts +324 -0
- package/src/core/links.ts +40 -17
- package/src/core/memory-recall.ts +254 -15
- package/src/core/memory-types.ts +12 -0
- package/src/core/memory.ts +2 -0
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/index.ts +14 -1
- package/src/ingestion/graph-reconciliation.ts +77 -15
- package/src/ingestion/source-availability/darwin-path.ts +9 -3
- package/src/ingestion/sync.ts +27 -4
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +4 -3
- package/src/mcp/http-egress.ts +42 -3
- package/src/mcp/tools/audit.ts +11 -2
- package/src/mcp/tools/changes.ts +1 -0
- package/src/mcp/tools/links.ts +74 -93
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/expansion.ts +19 -31
- package/src/pipeline/graph-retrieval.ts +22 -2
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +10 -3
- package/src/sdk/client.ts +1 -0
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/graph.ts +3 -1
- package/src/serve/routes/links.ts +45 -50
- package/src/serve/routes/sessions.ts +41 -53
- package/src/serve/server.ts +2 -1
- package/src/serve/status.ts +1 -0
- package/src/sessions/config-refresh.ts +111 -0
- package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
- package/src/store/migrations/034-collection-link-workspace.ts +47 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +477 -337
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +259 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/graph-similarity.ts +96 -0
- package/src/store/sqlite/workspace-link-resolver.ts +742 -0
- package/src/store/types.ts +64 -5
- package/src/store/vector/stats.ts +1 -1
- package/src/store/vector/status.ts +27 -0
- package/src/store/vector/stored-vectors.ts +158 -0
- package/src/store/vector/types.ts +6 -0
- package/src/store/vector/variant-search.ts +30 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +0 -1
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
import type { Collection } from "../../../config";
|
|
6
6
|
|
|
7
7
|
import { loadConfig } from "../../../config";
|
|
8
|
+
import {
|
|
9
|
+
detectCollectionWorkspace,
|
|
10
|
+
formatLinkWorkspace,
|
|
11
|
+
} from "../../../core/link-workspace";
|
|
8
12
|
import { bold, cyan, dim } from "../../colors";
|
|
9
13
|
import { CliError } from "../../errors";
|
|
10
14
|
|
|
@@ -23,7 +27,30 @@ interface ListOptions {
|
|
|
23
27
|
md?: boolean;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
type ListedCollection = Collection & {
|
|
31
|
+
/** Effective link workspace root (detected or configured). */
|
|
32
|
+
effectiveWorkspaceRoot?: string;
|
|
33
|
+
workspaceSource: ReturnType<typeof detectCollectionWorkspace>["source"];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Resolve each collection's effective link workspace from the filesystem. */
|
|
37
|
+
const withLinkWorkspaces = (collections: Collection[]): ListedCollection[] =>
|
|
38
|
+
collections.map((collection) => {
|
|
39
|
+
const workspace = detectCollectionWorkspace(collection);
|
|
40
|
+
return {
|
|
41
|
+
...collection,
|
|
42
|
+
...(workspace.root ? { effectiveWorkspaceRoot: workspace.root } : {}),
|
|
43
|
+
workspaceSource: workspace.source,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const linkWorkspaceLine = (collection: ListedCollection): string | null =>
|
|
48
|
+
formatLinkWorkspace({
|
|
49
|
+
workspaceRoot: collection.effectiveWorkspaceRoot,
|
|
50
|
+
workspaceSource: collection.workspaceSource,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function formatMarkdown(collections: ListedCollection[]): string {
|
|
27
54
|
const lines: string[] = ["# Collections", ""];
|
|
28
55
|
if (collections.length === 0) {
|
|
29
56
|
lines.push("No collections configured.");
|
|
@@ -43,12 +70,14 @@ function formatMarkdown(collections: Collection[]): string {
|
|
|
43
70
|
if (coll.updateCmd) {
|
|
44
71
|
lines.push(`- **Update Command:** \`${coll.updateCmd}\``);
|
|
45
72
|
}
|
|
73
|
+
const workspace = linkWorkspaceLine(coll);
|
|
74
|
+
if (workspace) lines.push(`- **Link workspace:** ${workspace}`);
|
|
46
75
|
lines.push("");
|
|
47
76
|
}
|
|
48
77
|
return lines.join("\n");
|
|
49
78
|
}
|
|
50
79
|
|
|
51
|
-
function formatTerminal(collections:
|
|
80
|
+
function formatTerminal(collections: ListedCollection[]): string {
|
|
52
81
|
if (collections.length === 0) {
|
|
53
82
|
return dim("No collections configured.");
|
|
54
83
|
}
|
|
@@ -78,6 +107,10 @@ function formatTerminal(collections: Collection[]): string {
|
|
|
78
107
|
if (updateCmd) {
|
|
79
108
|
lines.push(` ${dim("Update:")} ${updateCmd}`);
|
|
80
109
|
}
|
|
110
|
+
const workspace = linkWorkspaceLine(coll);
|
|
111
|
+
if (workspace) {
|
|
112
|
+
lines.push(` ${dim("Links:")} workspace ${sanitize(workspace)}`);
|
|
113
|
+
}
|
|
81
114
|
lines.push("");
|
|
82
115
|
}
|
|
83
116
|
return lines.join("\n");
|
|
@@ -96,13 +129,14 @@ export async function collectionList(options: ListOptions): Promise<void> {
|
|
|
96
129
|
const config = result.value;
|
|
97
130
|
|
|
98
131
|
// Format and output
|
|
132
|
+
const collections = withLinkWorkspaces(config.collections);
|
|
99
133
|
let output: string;
|
|
100
134
|
if (options.json) {
|
|
101
|
-
output = JSON.stringify(
|
|
135
|
+
output = JSON.stringify(collections, null, 2);
|
|
102
136
|
} else if (options.md) {
|
|
103
|
-
output = formatMarkdown(
|
|
137
|
+
output = formatMarkdown(collections);
|
|
104
138
|
} else {
|
|
105
|
-
output = formatTerminal(
|
|
139
|
+
output = formatTerminal(collections);
|
|
106
140
|
}
|
|
107
141
|
|
|
108
142
|
process.stdout.write(`${output}\n`);
|
|
@@ -818,7 +818,7 @@ function getActiveChunkCount(
|
|
|
818
818
|
`
|
|
819
819
|
SELECT COUNT(*) as count FROM content_chunks c
|
|
820
820
|
WHERE EXISTS (
|
|
821
|
-
SELECT 1 FROM documents d
|
|
821
|
+
SELECT 1 FROM documents d INDEXED BY idx_documents_mirror_hash
|
|
822
822
|
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
|
|
823
823
|
)
|
|
824
824
|
`
|
|
@@ -851,7 +851,7 @@ function getActiveChunks(
|
|
|
851
851
|
'force' as reason
|
|
852
852
|
FROM content_chunks c
|
|
853
853
|
WHERE EXISTS (
|
|
854
|
-
SELECT 1 FROM documents d
|
|
854
|
+
SELECT 1 FROM documents d INDEXED BY idx_documents_mirror_hash
|
|
855
855
|
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
|
|
856
856
|
)
|
|
857
857
|
AND (c.mirror_hash > ? OR (c.mirror_hash = ? AND c.seq > ?))
|
|
@@ -864,7 +864,7 @@ function getActiveChunks(
|
|
|
864
864
|
'force' as reason
|
|
865
865
|
FROM content_chunks c
|
|
866
866
|
WHERE EXISTS (
|
|
867
|
-
SELECT 1 FROM documents d
|
|
867
|
+
SELECT 1 FROM documents d INDEXED BY idx_documents_mirror_hash
|
|
868
868
|
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1${collectionClause}
|
|
869
869
|
)
|
|
870
870
|
ORDER BY c.mirror_hash, c.seq
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
|
|
15
15
|
import { normalizeContentTypes } from "../../config";
|
|
16
16
|
import { diagnoseGraphQuery } from "../../core/graph-query";
|
|
17
|
+
import { getActivePreset } from "../../llm/registry";
|
|
17
18
|
import { initStore } from "./shared";
|
|
18
19
|
|
|
19
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -125,7 +126,7 @@ export async function graph(
|
|
|
125
126
|
if (!initResult.ok) {
|
|
126
127
|
return { success: false, error: initResult.error };
|
|
127
128
|
}
|
|
128
|
-
const { store } = initResult;
|
|
129
|
+
const { store, config } = initResult;
|
|
129
130
|
|
|
130
131
|
try {
|
|
131
132
|
const storeOptions: GetGraphOptions = {
|
|
@@ -136,6 +137,7 @@ export async function graph(
|
|
|
136
137
|
threshold: options.threshold,
|
|
137
138
|
linkedOnly: !options.includeIsolated,
|
|
138
139
|
similarTopK: options.similarTopK,
|
|
140
|
+
embedModel: getActivePreset(config).embed,
|
|
139
141
|
};
|
|
140
142
|
|
|
141
143
|
const result = await store.getGraph(storeOptions);
|
|
@@ -5,16 +5,8 @@
|
|
|
5
5
|
* @module src/cli/commands/links
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import type { DocEdgeRow, DocLinkRow, StorePort } from "../../store/types";
|
|
9
9
|
|
|
10
|
-
import type {
|
|
11
|
-
DocEdgeRow,
|
|
12
|
-
DocLinkRow,
|
|
13
|
-
DocumentRow,
|
|
14
|
-
StorePort,
|
|
15
|
-
} from "../../store/types";
|
|
16
|
-
|
|
17
|
-
import { normalizeWikiName } from "../../core/links";
|
|
18
10
|
import { resolveDocRef } from "../../core/ref-parser";
|
|
19
11
|
import { initStore } from "./shared";
|
|
20
12
|
|
|
@@ -51,6 +43,8 @@ export interface LinkWithResolution {
|
|
|
51
43
|
resolved: boolean;
|
|
52
44
|
resolvedDocid?: string;
|
|
53
45
|
resolvedUri?: string;
|
|
46
|
+
/** Collection of the resolved target (may differ from the source's). */
|
|
47
|
+
resolvedCollection?: string;
|
|
54
48
|
}
|
|
55
49
|
|
|
56
50
|
export interface SemanticLinkItem {
|
|
@@ -155,6 +149,8 @@ export interface BacklinkItem {
|
|
|
155
149
|
sourceDocid: string;
|
|
156
150
|
sourceUri: string;
|
|
157
151
|
sourceTitle?: string;
|
|
152
|
+
/** Collection of the linking document. */
|
|
153
|
+
sourceCollection?: string;
|
|
158
154
|
linkText?: string;
|
|
159
155
|
startLine: number;
|
|
160
156
|
startCol: number;
|
|
@@ -233,96 +229,13 @@ export type SimilarResult =
|
|
|
233
229
|
| { success: true; data: SimilarResponse }
|
|
234
230
|
| { success: false; error: string; isValidation?: boolean };
|
|
235
231
|
|
|
236
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
-
// Helper: Build resolution indexes (cached per collection)
|
|
238
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
239
|
-
|
|
240
|
-
interface ResolutionIndexes {
|
|
241
|
-
// Map: normalized wiki name -> DocumentRow
|
|
242
|
-
wikiIndex: Map<string, DocumentRow>;
|
|
243
|
-
// Map: relPath -> DocumentRow
|
|
244
|
-
pathIndex: Map<string, DocumentRow>;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Normalize markdown link path for matching (strip ./, collapse ..) */
|
|
248
|
-
function normalizeMarkdownPath(path: string): string {
|
|
249
|
-
// Strip leading ./
|
|
250
|
-
let normalized = path.replace(/^\.\//, "");
|
|
251
|
-
// Collapse simple parent refs (a/b/../c -> a/c)
|
|
252
|
-
while (normalized.includes("/../")) {
|
|
253
|
-
normalized = normalized.replace(/[^/]+\/\.\.\//, "");
|
|
254
|
-
}
|
|
255
|
-
return normalized;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function buildResolutionIndexes(
|
|
259
|
-
store: StorePort,
|
|
260
|
-
collection: string,
|
|
261
|
-
cache: Map<string, ResolutionIndexes>
|
|
262
|
-
): Promise<ResolutionIndexes> {
|
|
263
|
-
const cached = cache.get(collection);
|
|
264
|
-
if (cached) {
|
|
265
|
-
return cached;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const indexes: ResolutionIndexes = {
|
|
269
|
-
wikiIndex: new Map(),
|
|
270
|
-
pathIndex: new Map(),
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const docsResult = await store.listDocuments(collection);
|
|
274
|
-
if (!docsResult.ok) {
|
|
275
|
-
// Collection may not exist or store error - return empty indexes
|
|
276
|
-
// Links to this collection will show as unresolved
|
|
277
|
-
cache.set(collection, indexes);
|
|
278
|
-
return indexes;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
for (const d of docsResult.value) {
|
|
282
|
-
if (!d.active) continue;
|
|
283
|
-
|
|
284
|
-
// Index by relPath for markdown links (exact match)
|
|
285
|
-
indexes.pathIndex.set(d.relPath, d);
|
|
286
|
-
|
|
287
|
-
// Also index by normalized path (without ./) for common variants
|
|
288
|
-
const normalizedPath = normalizeMarkdownPath(d.relPath);
|
|
289
|
-
if (
|
|
290
|
-
normalizedPath !== d.relPath &&
|
|
291
|
-
!indexes.pathIndex.has(normalizedPath)
|
|
292
|
-
) {
|
|
293
|
-
indexes.pathIndex.set(normalizedPath, d);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Index by normalized title for wiki links
|
|
297
|
-
if (d.title) {
|
|
298
|
-
const wikiKey = normalizeWikiName(d.title);
|
|
299
|
-
indexes.wikiIndex.set(wikiKey, d);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Also index by filename stem as fallback for wiki links
|
|
303
|
-
const stem = basename(d.relPath).replace(/\.[^.]+$/, "");
|
|
304
|
-
if (stem) {
|
|
305
|
-
const stemKey = normalizeWikiName(stem);
|
|
306
|
-
// Don't overwrite title match
|
|
307
|
-
if (!indexes.wikiIndex.has(stemKey)) {
|
|
308
|
-
indexes.wikiIndex.set(stemKey, d);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
cache.set(collection, indexes);
|
|
314
|
-
return indexes;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
232
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
318
233
|
// Helper: Map DocLinkRow to output format (avoids null leakage)
|
|
319
234
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
320
235
|
|
|
321
236
|
function mapLinkToOutput(
|
|
322
237
|
link: DocLinkRow,
|
|
323
|
-
resolved:
|
|
324
|
-
resolvedDocid?: string,
|
|
325
|
-
resolvedUri?: string
|
|
238
|
+
resolved: { docid: string; uri: string; collection?: string } | null
|
|
326
239
|
): LinkWithResolution {
|
|
327
240
|
return {
|
|
328
241
|
targetRef: link.targetRef,
|
|
@@ -336,9 +249,12 @@ function mapLinkToOutput(
|
|
|
336
249
|
startCol: link.startCol,
|
|
337
250
|
endLine: link.endLine,
|
|
338
251
|
endCol: link.endCol,
|
|
339
|
-
resolved,
|
|
340
|
-
...(
|
|
341
|
-
|
|
252
|
+
resolved: resolved !== null,
|
|
253
|
+
...(resolved && {
|
|
254
|
+
resolvedDocid: resolved.docid,
|
|
255
|
+
resolvedUri: resolved.uri,
|
|
256
|
+
...(resolved.collection && { resolvedCollection: resolved.collection }),
|
|
257
|
+
}),
|
|
342
258
|
};
|
|
343
259
|
}
|
|
344
260
|
|
|
@@ -443,48 +359,29 @@ export async function linksList(
|
|
|
443
359
|
return a.startCol - b.startCol;
|
|
444
360
|
});
|
|
445
361
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
// Safe fallback for targetRefNorm
|
|
464
|
-
const targetNorm = link.targetRefNorm || link.targetRef;
|
|
465
|
-
|
|
466
|
-
if (link.linkType === "wiki") {
|
|
467
|
-
// Wiki links: match by normalized title or filename
|
|
468
|
-
const wikiKey = normalizeWikiName(targetNorm);
|
|
469
|
-
resolvedDoc = indexes.wikiIndex.get(wikiKey);
|
|
470
|
-
} else {
|
|
471
|
-
// Markdown links: match by relPath (try exact, then normalized)
|
|
472
|
-
resolvedDoc = indexes.pathIndex.get(targetNorm);
|
|
473
|
-
if (!resolvedDoc) {
|
|
474
|
-
const normalizedTarget = normalizeMarkdownPath(targetNorm);
|
|
475
|
-
resolvedDoc = indexes.pathIndex.get(normalizedTarget);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
linksWithResolution.push(
|
|
480
|
-
mapLinkToOutput(
|
|
481
|
-
link,
|
|
482
|
-
!!resolvedDoc,
|
|
483
|
-
resolvedDoc?.docid,
|
|
484
|
-
resolvedDoc?.uri
|
|
485
|
-
)
|
|
486
|
-
);
|
|
362
|
+
// Resolve with the shared link resolver (workspace-aware for plain wiki
|
|
363
|
+
// links whose source sits in a link workspace).
|
|
364
|
+
const resolvedResult = await store.resolveLinks(
|
|
365
|
+
links.map((link) => ({
|
|
366
|
+
targetRefNorm: link.targetRefNorm || link.targetRef,
|
|
367
|
+
targetCollection: link.targetCollection ?? doc.collection,
|
|
368
|
+
linkType: link.linkType,
|
|
369
|
+
source: {
|
|
370
|
+
collection: doc.collection,
|
|
371
|
+
relPath: doc.relPath,
|
|
372
|
+
explicit: Boolean(link.targetCollection),
|
|
373
|
+
},
|
|
374
|
+
}))
|
|
375
|
+
);
|
|
376
|
+
if (!resolvedResult.ok) {
|
|
377
|
+
return { success: false, error: resolvedResult.error.message };
|
|
487
378
|
}
|
|
379
|
+
const linksWithResolution: LinkWithResolution[] = links.map(
|
|
380
|
+
(link, index) => {
|
|
381
|
+
const resolved = resolvedResult.value[index] ?? null;
|
|
382
|
+
return mapLinkToOutput(link, resolved);
|
|
383
|
+
}
|
|
384
|
+
);
|
|
488
385
|
|
|
489
386
|
const resolvedCount = linksWithResolution.filter((l) => l.resolved).length;
|
|
490
387
|
|
|
@@ -606,6 +503,7 @@ export async function backlinks(
|
|
|
606
503
|
sourceDocid: bl.sourceDocid,
|
|
607
504
|
sourceUri: bl.sourceDocUri,
|
|
608
505
|
...(bl.sourceDocTitle && { sourceTitle: bl.sourceDocTitle }),
|
|
506
|
+
...(bl.sourceCollection && { sourceCollection: bl.sourceCollection }),
|
|
609
507
|
...(bl.linkText && { linkText: bl.linkText }),
|
|
610
508
|
startLine: bl.startLine,
|
|
611
509
|
startCol: bl.startCol,
|
|
@@ -689,27 +587,23 @@ export async function similar(
|
|
|
689
587
|
}
|
|
690
588
|
const db = store.getRawDb();
|
|
691
589
|
|
|
692
|
-
//
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
)
|
|
710
|
-
.get(doc.mirrorHash, embedModel);
|
|
711
|
-
|
|
712
|
-
if (!fallbackRow) {
|
|
590
|
+
// Stored vector of the document's first chunk, from the active partition
|
|
591
|
+
const {
|
|
592
|
+
readStoredDocumentVectors,
|
|
593
|
+
resolveStoredVectorSource,
|
|
594
|
+
similarityHitDocuments,
|
|
595
|
+
storedVectorSearchOptions,
|
|
596
|
+
} = await import("../../store/vector/stored-vectors.js");
|
|
597
|
+
const source = resolveStoredVectorSource(db, modelPreset.embed);
|
|
598
|
+
const [embedding] =
|
|
599
|
+
readStoredDocumentVectors(
|
|
600
|
+
db,
|
|
601
|
+
source,
|
|
602
|
+
[{ id: doc.id, mirrorHash: doc.mirrorHash }],
|
|
603
|
+
{ firstChunkOnly: true }
|
|
604
|
+
).get(doc.id) ?? [];
|
|
605
|
+
|
|
606
|
+
if (!embedding) {
|
|
713
607
|
return {
|
|
714
608
|
success: false,
|
|
715
609
|
error: "Document has no embeddings. Run: gno embed",
|
|
@@ -718,9 +612,6 @@ export async function similar(
|
|
|
718
612
|
}
|
|
719
613
|
|
|
720
614
|
// Normalize embedding for cosine similarity
|
|
721
|
-
const { decodeEmbedding } =
|
|
722
|
-
await import("../../store/vector/sqlite-vec.js");
|
|
723
|
-
const embedding = decodeEmbedding(fallbackRow.embedding);
|
|
724
615
|
const dimensions = embedding.length;
|
|
725
616
|
let norm = 0;
|
|
726
617
|
for (let i = 0; i < dimensions; i++) {
|
|
@@ -738,7 +629,7 @@ export async function similar(
|
|
|
738
629
|
const { createVectorIndexPort } =
|
|
739
630
|
await import("../../store/vector/sqlite-vec.js");
|
|
740
631
|
const vecResult = await createVectorIndexPort(db, {
|
|
741
|
-
model:
|
|
632
|
+
model: modelPreset.embed,
|
|
742
633
|
dimensions,
|
|
743
634
|
});
|
|
744
635
|
if (!vecResult.ok) {
|
|
@@ -760,13 +651,13 @@ export async function similar(
|
|
|
760
651
|
const searchResult = await vectorIndex.searchNearest(
|
|
761
652
|
embedding,
|
|
762
653
|
candidateLimit,
|
|
763
|
-
|
|
654
|
+
storedVectorSearchOptions(source)
|
|
764
655
|
);
|
|
765
656
|
if (!searchResult.ok) {
|
|
766
657
|
return { success: false, error: searchResult.error.message };
|
|
767
658
|
}
|
|
768
659
|
|
|
769
|
-
//
|
|
660
|
+
// Candidate documents from a single listDocuments call
|
|
770
661
|
const docsResult = crossCollection
|
|
771
662
|
? await store.listDocuments()
|
|
772
663
|
: await store.listDocuments(doc.collection);
|
|
@@ -775,28 +666,18 @@ export async function similar(
|
|
|
775
666
|
return { success: false, error: docsResult.error.message };
|
|
776
667
|
}
|
|
777
668
|
|
|
778
|
-
|
|
779
|
-
for (const d of docsResult.value) {
|
|
780
|
-
if (d.active && d.mirrorHash) {
|
|
781
|
-
// Only keep first doc per hash (they have same content)
|
|
782
|
-
if (!docsByHash.has(d.mirrorHash)) {
|
|
783
|
-
docsByHash.set(d.mirrorHash, d);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Map results to documents, excluding self
|
|
669
|
+
// Map hits to their owning documents, excluding self
|
|
789
670
|
const similarItems: SimilarItem[] = [];
|
|
790
671
|
const seenDocids = new Set<string>();
|
|
791
672
|
|
|
792
|
-
for (const
|
|
673
|
+
for (const { document: d, distance } of similarityHitDocuments(
|
|
674
|
+
searchResult.value,
|
|
675
|
+
docsResult.value.filter((d) => d.active && d.mirrorHash)
|
|
676
|
+
)) {
|
|
793
677
|
if (similarItems.length >= limit) {
|
|
794
678
|
break;
|
|
795
679
|
}
|
|
796
680
|
|
|
797
|
-
const d = docsByHash.get(vec.mirrorHash);
|
|
798
|
-
if (!d) continue;
|
|
799
|
-
|
|
800
681
|
// Exclude self
|
|
801
682
|
if (d.docid === doc.docid) continue;
|
|
802
683
|
|
|
@@ -805,7 +686,7 @@ export async function similar(
|
|
|
805
686
|
|
|
806
687
|
// Compute similarity score from cosine distance
|
|
807
688
|
// sqlite-vec with cosine metric returns distance where similarity = 1 - distance
|
|
808
|
-
const score = Math.max(0, Math.min(1, 1 -
|
|
689
|
+
const score = Math.max(0, Math.min(1, 1 - distance));
|
|
809
690
|
|
|
810
691
|
if (score < threshold) continue;
|
|
811
692
|
|
|
@@ -178,6 +178,13 @@ export function formatSyncResultLines(
|
|
|
178
178
|
`Rechunked ${syncResult.rechunkedMirrors} cached mirrors. Run gno embed if embedding was skipped.`
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
|
+
if (syncResult.graphRebuild) {
|
|
182
|
+
lines.push(
|
|
183
|
+
syncResult.graphRebuild === "resolver-upgrade"
|
|
184
|
+
? "Link graph rebuilt: link resolution was upgraded."
|
|
185
|
+
: "Link graph rebuilt: collection settings or link workspace membership changed."
|
|
186
|
+
);
|
|
187
|
+
}
|
|
181
188
|
|
|
182
189
|
for (const c of syncResult.collections) {
|
|
183
190
|
lines.push(`${c.collection}:`);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { isConnectorActivationComplete } from "../../core/activation-connector-health";
|
|
22
22
|
import { buildActivationStatus } from "../../core/activation-status";
|
|
23
23
|
import { formatChunkingStatus } from "../../core/chunking-status";
|
|
24
|
+
import { formatLinkWorkspace } from "../../core/link-workspace";
|
|
24
25
|
import {
|
|
25
26
|
buildMemoryStatus,
|
|
26
27
|
formatMemoryStatusLines,
|
|
@@ -140,6 +141,8 @@ function formatTerminal(
|
|
|
140
141
|
` ${c.name}: ${c.activeDocuments} docs, ${c.totalChunks} chunks` +
|
|
141
142
|
(c.embeddedChunks > 0 ? `, ${c.embeddedChunks} embedded` : "")
|
|
142
143
|
);
|
|
144
|
+
const workspace = formatLinkWorkspace(c);
|
|
145
|
+
if (workspace) lines.push(` Link workspace: ${workspace}`);
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
148
|
|
|
@@ -336,6 +339,7 @@ export async function status(
|
|
|
336
339
|
const statusResult = await store.getStatus({
|
|
337
340
|
embedModel: resolveModelUri(config, "embed"),
|
|
338
341
|
chunking: config.chunking ?? {},
|
|
342
|
+
configuredCollections: config.collections.map(({ name }) => name),
|
|
339
343
|
});
|
|
340
344
|
if (!statusResult.ok) {
|
|
341
345
|
return { success: false, error: statusResult.error.message };
|
|
@@ -395,6 +399,8 @@ export function formatStatus(
|
|
|
395
399
|
collections: s.collections.map((c) => ({
|
|
396
400
|
name: c.name,
|
|
397
401
|
path: c.path,
|
|
402
|
+
...(c.workspaceRoot ? { workspaceRoot: c.workspaceRoot } : {}),
|
|
403
|
+
workspaceSource: c.workspaceSource ?? "none",
|
|
398
404
|
documentCount: c.activeDocuments,
|
|
399
405
|
chunkCount: c.totalChunks,
|
|
400
406
|
embeddedCount: c.embeddedChunks,
|
package/src/cli/program.ts
CHANGED
|
@@ -1694,7 +1694,10 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1694
1694
|
collectRepeatableValue,
|
|
1695
1695
|
[]
|
|
1696
1696
|
)
|
|
1697
|
-
.option(
|
|
1697
|
+
.option(
|
|
1698
|
+
"--max-findings <count>",
|
|
1699
|
+
"maximum returned findings (1-100000, or all)"
|
|
1700
|
+
)
|
|
1698
1701
|
.option("--max-age-days <days>", "explicit age review signal", Number)
|
|
1699
1702
|
.option(
|
|
1700
1703
|
"--orphan-root <uri>",
|
|
@@ -1736,7 +1739,7 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1736
1739
|
collections: cmdOpts.collection as string[],
|
|
1737
1740
|
paths: cmdOpts.path as string[],
|
|
1738
1741
|
tags: cmdOpts.tag as string[],
|
|
1739
|
-
maxFindings: cmdOpts.maxFindings as
|
|
1742
|
+
maxFindings: cmdOpts.maxFindings as string | undefined,
|
|
1740
1743
|
maxAgeDays: cmdOpts.maxAgeDays as number | undefined,
|
|
1741
1744
|
orphanRoots: cmdOpts.orphanRoot as string[],
|
|
1742
1745
|
orphanIgnorePrefixes: cmdOpts.orphanIgnorePrefix as string[],
|
|
@@ -4749,6 +4752,12 @@ function wireKnowledgeDeltaCommands(program: Command): void {
|
|
|
4749
4752
|
program
|
|
4750
4753
|
.command("impact <doc>")
|
|
4751
4754
|
.description("Find bounded inbound knowledge dependencies")
|
|
4755
|
+
.option(
|
|
4756
|
+
"-c, --collection <name>",
|
|
4757
|
+
"only traverse these collections (repeatable; default all)",
|
|
4758
|
+
collectRepeatableValue,
|
|
4759
|
+
[]
|
|
4760
|
+
)
|
|
4752
4761
|
.option("--max-depth <n>", "maximum dependency depth", "3")
|
|
4753
4762
|
.option("--max-nodes <n>", "maximum returned nodes", "100")
|
|
4754
4763
|
.option("--max-edges <n>", "maximum traversed evidence edges", "250")
|
|
@@ -4764,6 +4773,7 @@ function wireKnowledgeDeltaCommands(program: Command): void {
|
|
|
4764
4773
|
const result = await impact(
|
|
4765
4774
|
doc,
|
|
4766
4775
|
{
|
|
4776
|
+
collections: cmdOpts.collection as string[],
|
|
4767
4777
|
maxDepth: parsePositiveInt("max-depth", cmdOpts.maxDepth),
|
|
4768
4778
|
maxNodes: parsePositiveInt("max-nodes", cmdOpts.maxNodes),
|
|
4769
4779
|
maxEdges: parsePositiveInt("max-edges", cmdOpts.maxEdges),
|
package/src/config/loader.ts
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ZodError } from "zod";
|
|
9
9
|
|
|
10
|
+
import {
|
|
11
|
+
validateWorkspaceRootSetting,
|
|
12
|
+
workspaceRootSettingMessage,
|
|
13
|
+
} from "../core/link-workspace";
|
|
10
14
|
import {
|
|
11
15
|
normalizeConfigContentTypes,
|
|
12
16
|
type ConfigWarning,
|
|
@@ -132,6 +136,18 @@ export async function loadConfigFromPath(
|
|
|
132
136
|
};
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
const workspaceIssues = validateCollectionWorkspaceRoots(result.data);
|
|
140
|
+
if (workspaceIssues.length > 0) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
error: {
|
|
144
|
+
code: "VALIDATION_ERROR",
|
|
145
|
+
message: `Config validation failed: ${workspaceIssues.map((issue) => issue.message).join("; ")}`,
|
|
146
|
+
issues: workspaceIssues,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
135
151
|
const normalized = normalizeConfigContentTypes(result.data);
|
|
136
152
|
return {
|
|
137
153
|
ok: true,
|
|
@@ -140,6 +156,33 @@ export async function loadConfigFromPath(
|
|
|
140
156
|
};
|
|
141
157
|
}
|
|
142
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Validate explicit `workspaceRoot` settings against the filesystem: each must
|
|
161
|
+
* be absolute, exist, and contain its collection root. Messages name the
|
|
162
|
+
* collection so the error is actionable.
|
|
163
|
+
*/
|
|
164
|
+
export function validateCollectionWorkspaceRoots(
|
|
165
|
+
config: Config
|
|
166
|
+
): ZodError["issues"] {
|
|
167
|
+
const issues: ZodError["issues"] = [];
|
|
168
|
+
for (const [index, collection] of config.collections.entries()) {
|
|
169
|
+
if (typeof collection.workspaceRoot !== "string") continue;
|
|
170
|
+
const error = validateWorkspaceRootSetting(
|
|
171
|
+
collection.path,
|
|
172
|
+
collection.workspaceRoot
|
|
173
|
+
);
|
|
174
|
+
if (error) {
|
|
175
|
+
issues.push({
|
|
176
|
+
code: "custom",
|
|
177
|
+
message: workspaceRootSettingMessage(collection.name, error),
|
|
178
|
+
path: ["collections", index, "workspaceRoot"],
|
|
179
|
+
input: collection.workspaceRoot,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return issues;
|
|
184
|
+
}
|
|
185
|
+
|
|
143
186
|
/**
|
|
144
187
|
* Load config, returning null if not found (convenience wrapper).
|
|
145
188
|
* Throws on parse/validation errors.
|
package/src/config/types.ts
CHANGED
|
@@ -166,6 +166,14 @@ export const CollectionSchema = z.object({
|
|
|
166
166
|
*/
|
|
167
167
|
sourceAvailability: SourceAvailabilitySchema.optional(),
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Link workspace for plain wiki links. Omitted: auto-detect the nearest
|
|
171
|
+
* `.obsidian/` ancestor-or-self. An absolute path joins the collection to
|
|
172
|
+
* that workspace root (it must contain the collection root); `false` keeps
|
|
173
|
+
* the collection's links collection-scoped.
|
|
174
|
+
*/
|
|
175
|
+
workspaceRoot: z.union([z.string().min(1), z.literal(false)]).optional(),
|
|
176
|
+
|
|
169
177
|
/**
|
|
170
178
|
* Declares the collection as a GNO-managed memory substrate: `remember`
|
|
171
179
|
* writes fact files here and refuses every collection without the flag.
|