@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.
Files changed (96) hide show
  1. package/README.md +3 -2
  2. package/assets/skill/SKILL.md +11 -1
  3. package/assets/skill/cli-reference.md +8 -1
  4. package/assets/skill/examples.md +2 -1
  5. package/assets/skill/mcp-reference.md +3 -1
  6. package/assets/skill/recipes/memory-scoped-recall.md +10 -5
  7. package/assets/spa-production.json.gz +0 -0
  8. package/browser-extension/artifacts/{gno-browser-clipper-v2.7.1.zip → gno-browser-clipper-v2.8.1.zip} +0 -0
  9. package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +1 -0
  10. package/browser-extension/dist/manifest.json +1 -1
  11. package/package.json +1 -1
  12. package/spec/cli.md +86 -9
  13. package/spec/db/schema.sql +0 -1
  14. package/spec/mcp.md +26 -7
  15. package/spec/output-schemas/audit-report.schema.json +18 -4
  16. package/spec/output-schemas/backlinks.schema.json +4 -0
  17. package/spec/output-schemas/collection-list.schema.json +13 -0
  18. package/spec/output-schemas/graph.schema.json +2 -0
  19. package/spec/output-schemas/links-list.schema.json +4 -0
  20. package/spec/output-schemas/memory-recall.schema.json +1 -1
  21. package/spec/output-schemas/status.schema.json +18 -3
  22. package/src/cli/commands/audit.ts +23 -4
  23. package/src/cli/commands/collection/list.ts +39 -5
  24. package/src/cli/commands/embed.ts +3 -3
  25. package/src/cli/commands/graph.ts +3 -1
  26. package/src/cli/commands/links.ts +61 -180
  27. package/src/cli/commands/shared.ts +7 -0
  28. package/src/cli/commands/status.ts +6 -0
  29. package/src/cli/program.ts +12 -2
  30. package/src/config/loader.ts +43 -0
  31. package/src/config/types.ts +8 -0
  32. package/src/core/audit-contract.ts +16 -4
  33. package/src/core/audit-freshness.ts +11 -1
  34. package/src/core/audit-links.ts +197 -25
  35. package/src/core/audit-outside-index.ts +215 -0
  36. package/src/core/audit-provenance.ts +11 -4
  37. package/src/core/audit-workspace.ts +30 -9
  38. package/src/core/audit.ts +76 -16
  39. package/src/core/context-compiler.ts +3 -0
  40. package/src/core/context-evidence.ts +11 -0
  41. package/src/core/graph-edge-confidence.ts +23 -1
  42. package/src/core/host-paths.ts +1 -0
  43. package/src/core/knowledge-impact.ts +28 -0
  44. package/src/core/link-inventory-markdown.ts +2 -3
  45. package/src/core/link-workspace.ts +324 -0
  46. package/src/core/links.ts +40 -17
  47. package/src/core/memory-recall.ts +254 -15
  48. package/src/core/memory-types.ts +12 -0
  49. package/src/core/memory.ts +2 -0
  50. package/src/core/retrieval-replay-candidate.ts +6 -0
  51. package/src/core/retrieval-trace-request.ts +3 -0
  52. package/src/index.ts +14 -1
  53. package/src/ingestion/graph-reconciliation.ts +77 -15
  54. package/src/ingestion/source-availability/darwin-path.ts +9 -3
  55. package/src/ingestion/sync.ts +27 -4
  56. package/src/ingestion/types.ts +14 -0
  57. package/src/llm/inference-scope.ts +4 -3
  58. package/src/mcp/http-egress.ts +42 -3
  59. package/src/mcp/tools/audit.ts +11 -2
  60. package/src/mcp/tools/changes.ts +1 -0
  61. package/src/mcp/tools/links.ts +74 -93
  62. package/src/mcp/tools/sessions.ts +33 -4
  63. package/src/mcp/tools/status.ts +4 -0
  64. package/src/pipeline/expansion.ts +19 -31
  65. package/src/pipeline/graph-retrieval.ts +22 -2
  66. package/src/pipeline/hybrid.ts +1 -1
  67. package/src/pipeline/search.ts +2 -0
  68. package/src/pipeline/types.ts +10 -3
  69. package/src/sdk/client.ts +1 -0
  70. package/src/serve/findings-pass.ts +1 -1
  71. package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
  72. package/src/serve/public/pages/GraphView.tsx +2 -0
  73. package/src/serve/routes/changes.ts +6 -1
  74. package/src/serve/routes/graph.ts +3 -1
  75. package/src/serve/routes/links.ts +45 -50
  76. package/src/serve/routes/sessions.ts +41 -53
  77. package/src/serve/server.ts +2 -1
  78. package/src/serve/status.ts +1 -0
  79. package/src/sessions/config-refresh.ts +111 -0
  80. package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
  81. package/src/store/migrations/034-collection-link-workspace.ts +47 -0
  82. package/src/store/migrations/index.ts +4 -0
  83. package/src/store/sqlite/adapter.ts +477 -337
  84. package/src/store/sqlite/eligibility.ts +8 -2
  85. package/src/store/sqlite/graph-link-resolver.ts +259 -5
  86. package/src/store/sqlite/graph-neighbors.ts +147 -40
  87. package/src/store/sqlite/graph-reference-state.ts +13 -2
  88. package/src/store/sqlite/graph-similarity.ts +96 -0
  89. package/src/store/sqlite/workspace-link-resolver.ts +742 -0
  90. package/src/store/types.ts +64 -5
  91. package/src/store/vector/stats.ts +1 -1
  92. package/src/store/vector/status.ts +27 -0
  93. package/src/store/vector/stored-vectors.ts +158 -0
  94. package/src/store/vector/types.ts +6 -0
  95. package/src/store/vector/variant-search.ts +30 -14
  96. package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +0 -1
@@ -9,6 +9,7 @@ import type { DocumentRow } from "../store/types";
9
9
  import type {
10
10
  AuditCategory,
11
11
  AuditFingerprints,
12
+ AuditMaxFindings,
12
13
  AuditRunResult,
13
14
  AuditScope,
14
15
  } from "./audit";
@@ -26,10 +27,12 @@ import {
26
27
  AUDIT_RULE_SET_VERSION,
27
28
  canonicalAuditJson,
28
29
  hashAuditCanonical,
30
+ resolveAuditMaxFindings,
29
31
  runAudit,
30
32
  } from "./audit";
31
33
  import { evaluateFreshnessAudit } from "./audit-freshness";
32
34
  import { evaluateLinkAudit } from "./audit-links";
35
+ import { markOutsideIndexLinks } from "./audit-outside-index";
33
36
  import { evaluateProvenanceAudit } from "./audit-provenance";
34
37
  import {
35
38
  extractCaptureSourceFromFrontmatter,
@@ -55,7 +58,7 @@ export interface WorkspaceAuditOptions {
55
58
  collectionFilters?: readonly string[];
56
59
  pathFilters?: readonly string[];
57
60
  tagFilters?: readonly string[];
58
- maxFindings?: number;
61
+ maxFindings?: AuditMaxFindings;
59
62
  agePolicy?: AuditFreshnessOptions["agePolicy"];
60
63
  orphanRoots?: readonly string[];
61
64
  orphanIgnorePrefixes?: readonly string[];
@@ -347,7 +350,8 @@ const filterLinkSnapshot = (
347
350
  snapshot: AuditLinkSnapshot,
348
351
  selectedIds: ReadonlySet<number>,
349
352
  selectedDocuments: readonly DocumentRow[],
350
- selectionTruncated: boolean
353
+ selectionTruncated: boolean,
354
+ scopeCollections: readonly string[]
351
355
  ): AuditLinkSnapshot => {
352
356
  const links = snapshot.links.filter(
353
357
  (link) =>
@@ -362,6 +366,9 @@ const filterLinkSnapshot = (
362
366
  // Preserve graph-wide documents as duplicate-mirror evidence while the
363
367
  // explicit id set prevents findings outside the requested audit scope.
364
368
  auditedDocumentIds: [...selectedIds],
369
+ ...(scopeCollections.length > 0
370
+ ? { scopeCollections: [...scopeCollections] }
371
+ : {}),
365
372
  links,
366
373
  totals: { documents: selectedDocuments.length, links: outgoingTotal },
367
374
  truncated: {
@@ -423,14 +430,20 @@ const loadWorkspaceSnapshot = async (
423
430
  ? captureAuditLinkSnapshot(options.store.getRawDb())
424
431
  : emptyLinkSnapshot();
425
432
  const selectedIds = new Set(selected.documents.map(({ id }) => id));
433
+ const links = filterLinkSnapshot(
434
+ rawLinks,
435
+ selectedIds,
436
+ selected.documents,
437
+ selected.truncated,
438
+ filters.collections
439
+ );
426
440
  return {
427
441
  documents: observed,
428
- links: filterLinkSnapshot(
429
- rawLinks,
430
- selectedIds,
431
- selected.documents,
432
- selected.truncated
433
- ),
442
+ links: options.categories.includes("links")
443
+ ? await markOutsideIndexLinks(options.store.getRawDb(), links, {
444
+ signal: options.signal,
445
+ })
446
+ : links,
434
447
  truncated: selected.truncated,
435
448
  };
436
449
  };
@@ -596,6 +609,12 @@ export const runWorkspaceAudit = async (
596
609
  snapshots.set(attempt, pending);
597
610
  return pending;
598
611
  };
612
+ // Per-family caps follow the effective report cap; runAudit rejects an
613
+ // invalid value before any rule runs.
614
+ const resolvedMaxFindings = resolveAuditMaxFindings(options.maxFindings);
615
+ const maxFindingsPerRule = resolvedMaxFindings.ok
616
+ ? resolvedMaxFindings.limit
617
+ : 0;
599
618
  const result = await runAudit({
600
619
  scope,
601
620
  capabilities: {
@@ -645,6 +664,7 @@ export const runWorkspaceAudit = async (
645
664
  ...evaluateLinkAudit(snapshot.links, {
646
665
  rootUris: options.orphanRoots ?? [],
647
666
  ignorePathPrefixes: options.orphanIgnorePrefixes ?? [],
667
+ maxFindingsPerRule,
648
668
  })
649
669
  );
650
670
  await options.onProgress?.({
@@ -657,7 +677,7 @@ export const runWorkspaceAudit = async (
657
677
  contributions.push(
658
678
  ...evaluateProvenanceAudit(
659
679
  snapshot.documents.map(({ provenance }) => provenance),
660
- { truncated: snapshot.truncated }
680
+ { truncated: snapshot.truncated, maxFindingsPerRule }
661
681
  )
662
682
  );
663
683
  await options.onProgress?.({
@@ -674,6 +694,7 @@ export const runWorkspaceAudit = async (
674
694
  now: options.now ?? new Date(),
675
695
  agePolicy: options.agePolicy,
676
696
  truncated: snapshot.truncated,
697
+ maxFindingsPerRule,
677
698
  }
678
699
  )
679
700
  );
package/src/core/audit.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  AuditFinding,
7
7
  AuditFingerprints,
8
8
  AuditForbiddenStoreMethod,
9
+ AuditMaxFindings,
9
10
  AuditReport,
10
11
  AuditReportStatus,
11
12
  AuditRuleContribution,
@@ -24,6 +25,9 @@ import {
24
25
  AUDIT_DEFAULT_MAX_FINDINGS,
25
26
  AUDIT_FORBIDDEN_STORE_METHODS,
26
27
  AUDIT_MAX_CODE_CHARS,
28
+ AUDIT_MAX_EVIDENCE_DETAIL_CHARS,
29
+ AUDIT_MAX_EVIDENCE_PER_FINDING,
30
+ AUDIT_MAX_FINDINGS_ALL,
27
31
  AUDIT_MAX_FINDINGS_LIMIT,
28
32
  AUDIT_MAX_IDENTIFIER_CHARS,
29
33
  AUDIT_MAX_MESSAGE_CHARS,
@@ -127,23 +131,61 @@ export const normalizeAuditScope = (
127
131
  };
128
132
  };
129
133
 
130
- const resolveMaxFindings = (
131
- value: number | undefined
132
- ): { ok: true; maxFindings: number } | { ok: false; error: string } => {
134
+ export const AUDIT_MAX_FINDINGS_RANGE_MESSAGE = `maxFindings must be an integer between 1 and ${AUDIT_MAX_FINDINGS_LIMIT}, or "${AUDIT_MAX_FINDINGS_ALL}"`;
135
+
136
+ /**
137
+ * Validate `maxFindings`. `limit` is the effective numeric cap (Infinity for
138
+ * `all`); `reported` is the value echoed in the report's truncation block.
139
+ */
140
+ export const resolveAuditMaxFindings = (
141
+ value: AuditMaxFindings | undefined
142
+ ):
143
+ | { ok: true; limit: number; reported: AuditMaxFindings }
144
+ | { ok: false; error: string } => {
145
+ if (value === AUDIT_MAX_FINDINGS_ALL) {
146
+ return {
147
+ ok: true,
148
+ limit: Number.POSITIVE_INFINITY,
149
+ reported: AUDIT_MAX_FINDINGS_ALL,
150
+ };
151
+ }
133
152
  const maxFindings = value ?? AUDIT_DEFAULT_MAX_FINDINGS;
134
153
  if (
135
154
  !Number.isSafeInteger(maxFindings) ||
136
155
  maxFindings < 1 ||
137
156
  maxFindings > AUDIT_MAX_FINDINGS_LIMIT
138
157
  ) {
139
- return {
140
- ok: false,
141
- error: `maxFindings must be an integer between 1 and ${AUDIT_MAX_FINDINGS_LIMIT}`,
142
- };
158
+ return { ok: false, error: AUDIT_MAX_FINDINGS_RANGE_MESSAGE };
143
159
  }
144
- return { ok: true, maxFindings };
160
+ return { ok: true, limit: maxFindings, reported: maxFindings };
145
161
  };
146
162
 
163
+ /** Parse a CLI `--max-findings` value: a positive integer or `all`. */
164
+ export const parseAuditMaxFindingsInput = (
165
+ raw: string
166
+ ): AuditMaxFindings | undefined => {
167
+ const value = raw.trim();
168
+ if (value.toLowerCase() === AUDIT_MAX_FINDINGS_ALL) {
169
+ return AUDIT_MAX_FINDINGS_ALL;
170
+ }
171
+ if (!/^\d+$/.test(value)) return undefined;
172
+ const parsed = Number(value);
173
+ return resolveAuditMaxFindings(parsed).ok ? parsed : undefined;
174
+ };
175
+
176
+ const draftEvidenceTruncated = (contribution: AuditRuleContribution): boolean =>
177
+ contribution.evidenceTruncated === true ||
178
+ (contribution.findings ?? []).some(
179
+ (draft) =>
180
+ draft.evidence.length > AUDIT_MAX_EVIDENCE_PER_FINDING ||
181
+ draft.evidence.some(
182
+ (item) =>
183
+ item.detail !== undefined &&
184
+ Array.from(normalizeAuditText(item.detail)).length >
185
+ AUDIT_MAX_EVIDENCE_DETAIL_CHARS
186
+ )
187
+ );
188
+
147
189
  const fingerprintsEqual = (
148
190
  left: AuditFingerprints,
149
191
  right: AuditFingerprints
@@ -184,8 +226,16 @@ const materializeRule = (
184
226
  )
185
227
  .sort(compareAuditFindings);
186
228
 
229
+ // Only warning/error findings fail a passing rule; info findings of an
230
+ // informational rule (links.outside-index) leave it passing.
187
231
  let status = contribution.status;
188
- if (findings.length > 0 && status === "pass") {
232
+ if (
233
+ status === "pass" &&
234
+ findings.some(
235
+ (finding) =>
236
+ finding.severity === "error" || finding.severity === "warning"
237
+ )
238
+ ) {
189
239
  status = "fail";
190
240
  }
191
241
 
@@ -231,7 +281,8 @@ const buildReportFromRules = (input: {
231
281
  versions: AuditVersions;
232
282
  rules: AuditRuleResult[];
233
283
  status: AuditReportStatus;
234
- maxFindings: number;
284
+ maxFindings: { limit: number; reported: AuditMaxFindings };
285
+ evidenceTruncated: boolean;
235
286
  startedAt: string;
236
287
  completedAt: string;
237
288
  snapshotMs: number;
@@ -261,9 +312,9 @@ const buildReportFromRules = (input: {
261
312
  (sum, rule) => sum + rule.findingCount,
262
313
  0
263
314
  );
264
- const truncated = exactFindingCount > input.maxFindings;
315
+ const truncated = exactFindingCount > input.maxFindings.limit;
265
316
  const returnedFindings = truncated
266
- ? uniqueFindings.slice(0, input.maxFindings)
317
+ ? uniqueFindings.slice(0, input.maxFindings.limit)
267
318
  : uniqueFindings;
268
319
  const returnedFindingIds = new Set(
269
320
  returnedFindings.map((finding) => finding.id)
@@ -308,7 +359,11 @@ const buildReportFromRules = (input: {
308
359
  },
309
360
  truncation: {
310
361
  findingsTruncated: truncated,
311
- maxFindings: input.maxFindings,
362
+ maxFindings: input.maxFindings.reported,
363
+ snapshotTruncated: rules.some(
364
+ (rule) => rule.skipReason === "snapshot_truncated"
365
+ ),
366
+ evidenceTruncated: input.evidenceTruncated,
312
367
  },
313
368
  timing: {
314
369
  snapshotMs: Math.max(0, Math.round(input.snapshotMs)),
@@ -329,7 +384,7 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
329
384
  if (!scopeResult.ok) {
330
385
  return { ok: false, exit: "invalid", error: scopeResult.error };
331
386
  }
332
- const maxFindingsResult = resolveMaxFindings(input.maxFindings);
387
+ const maxFindingsResult = resolveAuditMaxFindings(input.maxFindings);
333
388
  if (!maxFindingsResult.ok) {
334
389
  return { ok: false, exit: "invalid", error: maxFindingsResult.error };
335
390
  }
@@ -355,6 +410,7 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
355
410
  let snapshotMs = 0;
356
411
  let rulesMs = 0;
357
412
  let lastRules: AuditRuleResult[] = [];
413
+ let lastEvidenceTruncated = false;
358
414
  let lastFingerprints: AuditFingerprints | null = null;
359
415
  let snapshotChanged = false;
360
416
  let failed = false;
@@ -405,6 +461,7 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
405
461
  try {
406
462
  const contributions = await collectContributions(input.rules, ruleCtx);
407
463
  lastRules = contributions.map(materializeRule);
464
+ lastEvidenceTruncated = contributions.some(draftEvidenceTruncated);
408
465
  } catch (cause) {
409
466
  if (input.signal?.aborted) {
410
467
  lastRules = [cancellationRule()];
@@ -453,6 +510,7 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
453
510
  }
454
511
  // Discard rule results from a drifted attempt; retry with a fresh snapshot.
455
512
  lastRules = [];
513
+ lastEvidenceTruncated = false;
456
514
  }
457
515
 
458
516
  const completedAt = clock();
@@ -484,7 +542,8 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
484
542
  },
485
543
  ],
486
544
  status: "failed",
487
- maxFindings: maxFindingsResult.maxFindings,
545
+ maxFindings: maxFindingsResult,
546
+ evidenceTruncated: false,
488
547
  startedAt: runStartedAt.toISOString(),
489
548
  completedAt: completedAt.toISOString(),
490
549
  snapshotMs,
@@ -531,7 +590,8 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
531
590
  rules: rulesForReport,
532
591
  snapshotChanged: false,
533
592
  }),
534
- maxFindings: maxFindingsResult.maxFindings,
593
+ maxFindings: maxFindingsResult,
594
+ evidenceTruncated: lastEvidenceTruncated,
535
595
  startedAt: runStartedAt.toISOString(),
536
596
  completedAt: completedAt.toISOString(),
537
597
  snapshotMs,
@@ -355,6 +355,9 @@ export const planContextEvidence = async <T, P>(
355
355
  await deps.retrieve({
356
356
  query,
357
357
  collection,
358
+ // The primary search is partitioned per collection; graph expansion
359
+ // may bridge between the requested collections, never beyond them.
360
+ ...(collections.length > 0 ? { graphCollections: collections } : {}),
358
361
  noExpand: true,
359
362
  queryModes,
360
363
  tagsAll: input.tagsAll,
@@ -40,6 +40,7 @@ import {
40
40
  import { planContextEvidence } from "./context-compiler";
41
41
  import { projectContextEvidenceMetadata } from "./context-evidence-metadata";
42
42
  import { createEgressLineage, resolveEgressLineage } from "./egress-provenance";
43
+ import { linkResolutionFingerprintInput } from "./link-workspace";
43
44
  import { projectRecordEvidenceMetadata } from "./record-metadata";
44
45
  import {
45
46
  extractInclusiveLines,
@@ -205,6 +206,13 @@ const canonicalIndexSnapshot = (
205
206
  ),
206
207
  });
207
208
 
209
+ const linkResolutionEntry = (
210
+ rows: Parameters<typeof linkResolutionFingerprintInput>[0]
211
+ ): { linkResolution?: ReturnType<typeof linkResolutionFingerprintInput> } => {
212
+ const linkResolution = linkResolutionFingerprintInput(rows);
213
+ return linkResolution ? { linkResolution } : {};
214
+ };
215
+
208
216
  /** Capture one strict, content-free index/context snapshot before or after work. */
209
217
  export const captureContextEvidenceSnapshot = async (
210
218
  store: ContextEvidenceStore,
@@ -284,6 +292,9 @@ export const captureContextEvidenceSnapshot = async (
284
292
  indexFingerprint: hashJson({
285
293
  snapshots: indexSnapshots,
286
294
  egressLineage,
295
+ // Link workspace membership decides graph neighbours; a membership or
296
+ // resolver change is index drift. Absent without any workspace.
297
+ ...linkResolutionEntry(collectionRows),
287
298
  }),
288
299
  };
289
300
  };
@@ -17,8 +17,30 @@ export const GRAPH_EDGE_CONFIDENCE_RANK: Record<GraphEdgeConfidence, number> = {
17
17
  export function classifyResolvedGraphEdge(
18
18
  linkType: "wiki" | "markdown",
19
19
  matchRank: number | null,
20
- matchCount: number | null
20
+ matchCount: number | null,
21
+ /** Workspace resolution reason; semantic, independent of legacy ranks. */
22
+ reason?: string
21
23
  ): { confidence: GraphEdgeConfidence; audit: GraphEdgeAudit } {
24
+ if (linkType === "wiki" && reason !== undefined && reason !== "title") {
25
+ switch (reason) {
26
+ case "workspace-path":
27
+ case "collection-path":
28
+ return {
29
+ confidence: "explicit",
30
+ audit: { resolution: "exact-path", matchCount: 1 },
31
+ };
32
+ case "exact-name":
33
+ return {
34
+ confidence: "explicit",
35
+ audit: { resolution: "exact-name", matchCount: 1 },
36
+ };
37
+ default:
38
+ return {
39
+ confidence: "inferred",
40
+ audit: { resolution: "tie-break", matchCount: 1 },
41
+ };
42
+ }
43
+ }
22
44
  if (linkType === "markdown") {
23
45
  return {
24
46
  confidence: "explicit",
@@ -22,6 +22,7 @@ export const OWNER_CONFIG_PATH_FIELDS: ReadonlySet<string> = new Set([
22
22
  "configPath",
23
23
  "dbPath",
24
24
  "path",
25
+ "workspaceRoot",
25
26
  ]);
26
27
 
27
28
  const isPlainObject = (value: object): boolean => {
@@ -41,6 +41,12 @@ export interface KnowledgeImpactResult {
41
41
  }
42
42
 
43
43
  export interface KnowledgeImpactInput {
44
+ /**
45
+ * Collection scope: only documents in these collections are traversed or
46
+ * returned, and a document outside them is never used as a bridge.
47
+ * Omitted or empty means every indexed collection.
48
+ */
49
+ collections?: string[];
44
50
  maxDepth?: number;
45
51
  maxNodes?: number;
46
52
  maxEdges?: number;
@@ -118,8 +124,30 @@ export async function analyzeKnowledgeImpact(
118
124
  isValidation: true,
119
125
  };
120
126
  }
127
+ const scope = [...new Set(input.collections ?? [])].sort();
128
+ if (scope.length > 0) {
129
+ const known = await store.getCollections();
130
+ if (!known.ok) return { success: false, error: known.error.message };
131
+ const names = new Set(known.value.map((row) => row.name));
132
+ const missing = scope.find((name) => !names.has(name));
133
+ if (missing) {
134
+ return {
135
+ success: false,
136
+ error: `Collection not found: ${missing}`,
137
+ isValidation: true,
138
+ };
139
+ }
140
+ if (!scope.includes(resolved.doc.collection)) {
141
+ return {
142
+ success: false,
143
+ error: `Document is outside the requested collections: ${ref}`,
144
+ isValidation: true,
145
+ };
146
+ }
147
+ }
121
148
  const traversal = await store.queryGraphTraversal(resolved.doc.id, {
122
149
  direction: "in",
150
+ ...(scope.length > 0 ? { collections: scope } : {}),
123
151
  maxDepth: values.maxDepth,
124
152
  maxNodes: values.maxNodes,
125
153
  frontierLimit: values.frontierLimit,
@@ -23,7 +23,7 @@ import {
23
23
  pushInventoryToken,
24
24
  } from "./link-inventory-opaque";
25
25
  import { isRelevantDestination } from "./link-relevance";
26
- import { parseTargetParts } from "./links";
26
+ import { parseTargetParts, splitWikiLinkContent } from "./links";
27
27
 
28
28
  const EXTERNAL_URL_REGEX = /^[a-z][a-z0-9+.-]*:/i;
29
29
 
@@ -51,8 +51,7 @@ export function inventoryWikiLinks(
51
51
  }
52
52
 
53
53
  const content = match[1] ?? "";
54
- const pipeIndex = content.indexOf("|");
55
- const targetPart = pipeIndex >= 0 ? content.slice(0, pipeIndex) : content;
54
+ const targetPart = splitWikiLinkContent(content).target;
56
55
  const trimmedTarget = targetPart.trim();
57
56
  if (!trimmedTarget) continue;
58
57
  const leadingWs = targetPart.length - targetPart.trimStart().length;