@gmickel/gno 2.7.0 → 2.8.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/README.md +26 -30
- package/assets/skill/SKILL.md +8 -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/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.0.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +48 -6
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +18 -3
- 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 +15 -2
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/status.schema.json +27 -16
- package/src/cli/commands/ask.ts +31 -12
- 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/links.ts +34 -131
- package/src/cli/commands/ls.ts +6 -1
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +5 -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 +145 -25
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +19 -4
- package/src/core/audit.ts +67 -15
- 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 +24 -5
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-workspace.ts +324 -0
- package/src/core/request-receipts.ts +63 -9
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/core/windows-private-path.ts +136 -1
- package/src/embed/backlog.ts +22 -10
- package/src/embed/variant-backlog.ts +34 -9
- 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 +22 -1
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +19 -0
- 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 +3 -0
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +22 -6
- 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/types.ts +6 -3
- package/src/serve/embed-scheduler.ts +2 -2
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/host-path-redaction.ts +51 -14
- package/src/serve/public/components/BootstrapStatus.tsx +5 -3
- package/src/serve/public/components/CaptureModal.tsx +1 -1
- package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
- package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
- package/src/serve/public/components/FirstRunWizard.tsx +4 -2
- package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
- package/src/serve/public/pages/Collections.tsx +16 -13
- package/src/serve/public/pages/Connectors.tsx +7 -4
- package/src/serve/public/pages/Dashboard.tsx +8 -6
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/resident-runtime.ts +7 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/links.ts +13 -0
- package/src/serve/routes/sessions.ts +81 -37
- package/src/serve/server.ts +5 -3
- package/src/serve/status-model.ts +10 -6
- package/src/serve/status.ts +2 -7
- 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 +392 -233
- package/src/store/sqlite/change-journal-store.ts +1 -1
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +252 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
- package/src/store/sqlite/workspace-link-resolver.ts +654 -0
- package/src/store/types.ts +49 -3
- package/src/store/vector/stats.ts +1 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +0 -1
package/src/cli/commands/ask.ts
CHANGED
|
@@ -81,6 +81,36 @@ export type AskCommandResult =
|
|
|
81
81
|
}
|
|
82
82
|
| { success: false; error: string };
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Give every document URI `?index=` on a named index, like search and
|
|
86
|
+
* Capsule results: results, citations and the answer-context selection.
|
|
87
|
+
*/
|
|
88
|
+
export function decorateAskUris(
|
|
89
|
+
result: AskResult,
|
|
90
|
+
indexName: string | undefined
|
|
91
|
+
): AskResult {
|
|
92
|
+
const decorate = <T extends { uri: string }>(item: T): T => ({
|
|
93
|
+
...item,
|
|
94
|
+
uri: decorateUriForIndex(item.uri, indexName),
|
|
95
|
+
});
|
|
96
|
+
const answerContext = result.meta.answerContext;
|
|
97
|
+
return {
|
|
98
|
+
...result,
|
|
99
|
+
results: result.results.map(decorate),
|
|
100
|
+
citations: result.citations?.map(decorate),
|
|
101
|
+
meta: answerContext
|
|
102
|
+
? {
|
|
103
|
+
...result.meta,
|
|
104
|
+
answerContext: {
|
|
105
|
+
...answerContext,
|
|
106
|
+
selected: answerContext.selected.map(decorate),
|
|
107
|
+
dropped: answerContext.dropped.map(decorate),
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
: result.meta,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
115
|
// Command Implementation
|
|
86
116
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -435,20 +465,9 @@ export async function ask(
|
|
|
435
465
|
return { success: false, error: finalized.error.message };
|
|
436
466
|
}
|
|
437
467
|
}
|
|
438
|
-
// URIs carry `?index=` like search and Capsule results on a named index.
|
|
439
468
|
return {
|
|
440
469
|
success: true,
|
|
441
|
-
data:
|
|
442
|
-
...askResult,
|
|
443
|
-
results: askResult.results.map((result) => ({
|
|
444
|
-
...result,
|
|
445
|
-
uri: decorateUriForIndex(result.uri, globals.index),
|
|
446
|
-
})),
|
|
447
|
-
citations: askResult.citations?.map((citation) => ({
|
|
448
|
-
...citation,
|
|
449
|
-
uri: decorateUriForIndex(citation.uri, globals.index),
|
|
450
|
-
})),
|
|
451
|
-
},
|
|
470
|
+
data: decorateAskUris(askResult, globals.index),
|
|
452
471
|
metadata: traceSession?.metadata(),
|
|
453
472
|
};
|
|
454
473
|
} catch (cause) {
|
|
@@ -14,8 +14,11 @@ import type { WorkspaceAuditProgress } from "../../core/audit-workspace";
|
|
|
14
14
|
import { getIndexDbPath } from "../../app/constants";
|
|
15
15
|
import { loadConfig } from "../../config";
|
|
16
16
|
import {
|
|
17
|
+
AUDIT_MAX_FINDINGS_RANGE_MESSAGE,
|
|
17
18
|
auditExitCode,
|
|
18
19
|
AUDIT_CATEGORIES,
|
|
20
|
+
parseAuditMaxFindingsInput,
|
|
21
|
+
resolveAuditMaxFindings,
|
|
19
22
|
serializeAuditReportCanonical,
|
|
20
23
|
} from "../../core/audit";
|
|
21
24
|
import { runWorkspaceAudit } from "../../core/audit-workspace";
|
|
@@ -31,7 +34,8 @@ export interface AuditCommandOptions {
|
|
|
31
34
|
collections?: string[];
|
|
32
35
|
paths?: string[];
|
|
33
36
|
tags?: string[];
|
|
34
|
-
|
|
37
|
+
/** Positive integer, `all`, or the raw CLI string for either. */
|
|
38
|
+
maxFindings?: number | string;
|
|
35
39
|
maxAgeDays?: number;
|
|
36
40
|
orphanRoots?: string[];
|
|
37
41
|
orphanIgnorePrefixes?: string[];
|
|
@@ -80,11 +84,18 @@ export const audit = async (
|
|
|
80
84
|
error: "category must be links, provenance, freshness, or all",
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
|
-
|
|
87
|
+
const maxFindings =
|
|
88
|
+
typeof options.maxFindings === "string"
|
|
89
|
+
? parseAuditMaxFindingsInput(options.maxFindings)
|
|
90
|
+
: options.maxFindings;
|
|
91
|
+
if (
|
|
92
|
+
(options.maxFindings !== undefined && maxFindings === undefined) ||
|
|
93
|
+
!resolveAuditMaxFindings(maxFindings).ok
|
|
94
|
+
) {
|
|
84
95
|
return {
|
|
85
96
|
success: false,
|
|
86
97
|
invalid: true,
|
|
87
|
-
error:
|
|
98
|
+
error: AUDIT_MAX_FINDINGS_RANGE_MESSAGE,
|
|
88
99
|
};
|
|
89
100
|
}
|
|
90
101
|
if (invalidPositiveInteger(options.maxAgeDays)) {
|
|
@@ -155,7 +166,7 @@ export const audit = async (
|
|
|
155
166
|
collectionFilters: requestedCollections,
|
|
156
167
|
pathFilters: options.paths,
|
|
157
168
|
tagFilters: requestedTags,
|
|
158
|
-
maxFindings
|
|
169
|
+
maxFindings,
|
|
159
170
|
agePolicy:
|
|
160
171
|
options.maxAgeDays === undefined
|
|
161
172
|
? undefined
|
|
@@ -188,6 +199,14 @@ export const formatAuditReport = (
|
|
|
188
199
|
`Categories: ${report.scope.categories.join(", ")}`,
|
|
189
200
|
`Rules: ${report.counts.rules.total} (${report.counts.rules.fail} failed, ${report.counts.rules.unavailable} unavailable, ${report.counts.rules.inconclusive} inconclusive)`,
|
|
190
201
|
`Findings: ${report.counts.findings.total}${report.counts.findings.truncated ? ` (${report.counts.findings.returned} shown)` : ""}`,
|
|
202
|
+
...(report.truncation.snapshotTruncated
|
|
203
|
+
? [
|
|
204
|
+
"Snapshot: truncated; totals cover the bounded audit snapshot, not the whole index",
|
|
205
|
+
]
|
|
206
|
+
: []),
|
|
207
|
+
...(report.truncation.evidenceTruncated
|
|
208
|
+
? ["Evidence: some finding evidence was shortened"]
|
|
209
|
+
: []),
|
|
191
210
|
`Examined: ${report.counts.examined.documents} document/rule observations`,
|
|
192
211
|
`Duration: ${report.durationMs}ms`,
|
|
193
212
|
];
|
|
@@ -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
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* @module src/cli/commands/links
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { basename } from "node:path";
|
|
9
|
-
|
|
10
8
|
import type {
|
|
11
9
|
DocEdgeRow,
|
|
12
10
|
DocLinkRow,
|
|
@@ -14,7 +12,6 @@ import type {
|
|
|
14
12
|
StorePort,
|
|
15
13
|
} from "../../store/types";
|
|
16
14
|
|
|
17
|
-
import { normalizeWikiName } from "../../core/links";
|
|
18
15
|
import { resolveDocRef } from "../../core/ref-parser";
|
|
19
16
|
import { initStore } from "./shared";
|
|
20
17
|
|
|
@@ -51,6 +48,8 @@ export interface LinkWithResolution {
|
|
|
51
48
|
resolved: boolean;
|
|
52
49
|
resolvedDocid?: string;
|
|
53
50
|
resolvedUri?: string;
|
|
51
|
+
/** Collection of the resolved target (may differ from the source's). */
|
|
52
|
+
resolvedCollection?: string;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
export interface SemanticLinkItem {
|
|
@@ -155,6 +154,8 @@ export interface BacklinkItem {
|
|
|
155
154
|
sourceDocid: string;
|
|
156
155
|
sourceUri: string;
|
|
157
156
|
sourceTitle?: string;
|
|
157
|
+
/** Collection of the linking document. */
|
|
158
|
+
sourceCollection?: string;
|
|
158
159
|
linkText?: string;
|
|
159
160
|
startLine: number;
|
|
160
161
|
startCol: number;
|
|
@@ -233,96 +234,13 @@ export type SimilarResult =
|
|
|
233
234
|
| { success: true; data: SimilarResponse }
|
|
234
235
|
| { success: false; error: string; isValidation?: boolean };
|
|
235
236
|
|
|
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
237
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
318
238
|
// Helper: Map DocLinkRow to output format (avoids null leakage)
|
|
319
239
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
320
240
|
|
|
321
241
|
function mapLinkToOutput(
|
|
322
242
|
link: DocLinkRow,
|
|
323
|
-
resolved:
|
|
324
|
-
resolvedDocid?: string,
|
|
325
|
-
resolvedUri?: string
|
|
243
|
+
resolved: { docid: string; uri: string; collection?: string } | null
|
|
326
244
|
): LinkWithResolution {
|
|
327
245
|
return {
|
|
328
246
|
targetRef: link.targetRef,
|
|
@@ -336,9 +254,12 @@ function mapLinkToOutput(
|
|
|
336
254
|
startCol: link.startCol,
|
|
337
255
|
endLine: link.endLine,
|
|
338
256
|
endCol: link.endCol,
|
|
339
|
-
resolved,
|
|
340
|
-
...(
|
|
341
|
-
|
|
257
|
+
resolved: resolved !== null,
|
|
258
|
+
...(resolved && {
|
|
259
|
+
resolvedDocid: resolved.docid,
|
|
260
|
+
resolvedUri: resolved.uri,
|
|
261
|
+
...(resolved.collection && { resolvedCollection: resolved.collection }),
|
|
262
|
+
}),
|
|
342
263
|
};
|
|
343
264
|
}
|
|
344
265
|
|
|
@@ -443,48 +364,29 @@ export async function linksList(
|
|
|
443
364
|
return a.startCol - b.startCol;
|
|
444
365
|
});
|
|
445
366
|
|
|
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
|
-
);
|
|
367
|
+
// Resolve with the shared link resolver (workspace-aware for plain wiki
|
|
368
|
+
// links whose source sits in a link workspace).
|
|
369
|
+
const resolvedResult = await store.resolveLinks(
|
|
370
|
+
links.map((link) => ({
|
|
371
|
+
targetRefNorm: link.targetRefNorm || link.targetRef,
|
|
372
|
+
targetCollection: link.targetCollection ?? doc.collection,
|
|
373
|
+
linkType: link.linkType,
|
|
374
|
+
source: {
|
|
375
|
+
collection: doc.collection,
|
|
376
|
+
relPath: doc.relPath,
|
|
377
|
+
explicit: Boolean(link.targetCollection),
|
|
378
|
+
},
|
|
379
|
+
}))
|
|
380
|
+
);
|
|
381
|
+
if (!resolvedResult.ok) {
|
|
382
|
+
return { success: false, error: resolvedResult.error.message };
|
|
487
383
|
}
|
|
384
|
+
const linksWithResolution: LinkWithResolution[] = links.map(
|
|
385
|
+
(link, index) => {
|
|
386
|
+
const resolved = resolvedResult.value[index] ?? null;
|
|
387
|
+
return mapLinkToOutput(link, resolved);
|
|
388
|
+
}
|
|
389
|
+
);
|
|
488
390
|
|
|
489
391
|
const resolvedCount = linksWithResolution.filter((l) => l.resolved).length;
|
|
490
392
|
|
|
@@ -606,6 +508,7 @@ export async function backlinks(
|
|
|
606
508
|
sourceDocid: bl.sourceDocid,
|
|
607
509
|
sourceUri: bl.sourceDocUri,
|
|
608
510
|
...(bl.sourceDocTitle && { sourceTitle: bl.sourceDocTitle }),
|
|
511
|
+
...(bl.sourceCollection && { sourceCollection: bl.sourceCollection }),
|
|
609
512
|
...(bl.linkText && { linkText: bl.linkText }),
|
|
610
513
|
startLine: bl.startLine,
|
|
611
514
|
startCol: bl.startCol,
|
package/src/cli/commands/ls.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { DocumentRow, StorePort, StoreResult } from "../../store/types";
|
|
9
9
|
|
|
10
|
+
import { decorateUriForIndex } from "../../app/constants";
|
|
10
11
|
import { initStore } from "./shared";
|
|
11
12
|
|
|
12
13
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -145,7 +146,11 @@ export async function ls(
|
|
|
145
146
|
// Apply offset and limit
|
|
146
147
|
const offset = options.offset ?? 0;
|
|
147
148
|
const limit = options.limit ?? 20;
|
|
148
|
-
|
|
149
|
+
// Sorted on canonical URIs; the output URIs carry `?index=` like search.
|
|
150
|
+
const paged = allActive.slice(offset, offset + limit).map((doc) => ({
|
|
151
|
+
...doc,
|
|
152
|
+
uri: decorateUriForIndex(doc.uri, options.indexName),
|
|
153
|
+
}));
|
|
149
154
|
|
|
150
155
|
return {
|
|
151
156
|
success: true,
|
|
@@ -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
|
|
|
@@ -395,6 +398,8 @@ export function formatStatus(
|
|
|
395
398
|
collections: s.collections.map((c) => ({
|
|
396
399
|
name: c.name,
|
|
397
400
|
path: c.path,
|
|
401
|
+
...(c.workspaceRoot ? { workspaceRoot: c.workspaceRoot } : {}),
|
|
402
|
+
workspaceSource: c.workspaceSource ?? "none",
|
|
398
403
|
documentCount: c.activeDocuments,
|
|
399
404
|
chunkCount: c.totalChunks,
|
|
400
405
|
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.
|