@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
@@ -76,6 +76,12 @@ export function buildEligibleDocumentQuery(
76
76
  params.push(`%${options.author.toLowerCase()}%`);
77
77
  }
78
78
 
79
+ // Without planner statistics a collection filter can outrank the hash
80
+ // index; a hash allowlist is always the selective predicate.
81
+ const hashPin =
82
+ options.allowedMirrorHashes === undefined
83
+ ? ""
84
+ : " INDEXED BY idx_documents_mirror_hash";
79
85
  if (options.allowedMirrorHashes !== undefined) {
80
86
  conditions.push("d.mirror_hash IN (SELECT value FROM json_each(?))");
81
87
  params.push(JSON.stringify(options.allowedMirrorHashes));
@@ -128,7 +134,7 @@ export function buildEligibleDocumentQuery(
128
134
  (string | number)[]
129
135
  >(`
130
136
  SELECT d.id, d.title, d.author, d.content_type, d.categories, COALESCE(d.record_source_path, d.rel_path) AS path, cc.text
131
- FROM documents d LEFT JOIN content_chunks cc ON cc.mirror_hash = d.mirror_hash
137
+ FROM documents d${hashPin} LEFT JOIN content_chunks cc ON cc.mirror_hash = d.mirror_hash
132
138
  WHERE ${conditions.join(" AND ")}
133
139
  `)
134
140
  .iterate(...params);
@@ -180,7 +186,7 @@ export function buildEligibleDocumentQuery(
180
186
  params.push(JSON.stringify([...denied]));
181
187
  }
182
188
  return {
183
- sql: `SELECT d.id, d.mirror_hash FROM documents d WHERE ${conditions.join(" AND ")}`,
189
+ sql: `SELECT d.id, d.mirror_hash FROM documents d${hashPin} WHERE ${conditions.join(" AND ")}`,
184
190
  params,
185
191
  };
186
192
  }
@@ -6,22 +6,82 @@
6
6
 
7
7
  import type { Database } from "bun:sqlite";
8
8
 
9
+ import type { WorkspaceResolutionReason } from "./workspace-link-resolver";
10
+
9
11
  import { stripWikiMdExt } from "../../core/links";
10
- import { resolveGraphLinkTargetsBulk } from "./graph-link-bulk-resolver";
12
+ import {
13
+ GRAPH_LINK_BULK_MAX_DOCUMENTS,
14
+ resolveGraphLinkTargetsBulk,
15
+ } from "./graph-link-bulk-resolver";
16
+ import {
17
+ loadLinkWorkspaceMemberships,
18
+ resolveWorkspaceTargets,
19
+ workspaceKeyForDocument,
20
+ workspaceMemberCollections,
21
+ type WorkspaceTargetInput,
22
+ } from "./workspace-link-resolver";
23
+
24
+ export interface GraphLinkSourceIdentity {
25
+ collection: string;
26
+ relPath: string;
27
+ /** The link carried an explicit `collection:` prefix. */
28
+ explicit: boolean;
29
+ }
11
30
 
12
31
  export interface GraphLinkTarget {
13
32
  targetRefNorm: string;
33
+ /** Explicit prefix collection, else the source collection. */
14
34
  targetCollection: string;
15
35
  linkType: "wiki" | "markdown";
36
+ /**
37
+ * Source document identity. Plain wiki links from a document inside a link
38
+ * workspace resolve across the workspace; without it (or for explicit,
39
+ * markdown and non-workspace links) resolution stays collection-scoped.
40
+ */
41
+ source?: GraphLinkSourceIdentity;
16
42
  }
17
43
 
44
+ /** Where a resolved link landed relative to its source collection. */
45
+ export type LinkResolutionScope =
46
+ | "same-collection"
47
+ | "cross-collection"
48
+ | "explicit-collection";
49
+
18
50
  export interface ResolvedGraphLinkTarget {
19
51
  targetId: number;
20
52
  targetDocid: string;
21
53
  matchRank: number;
22
54
  matchCount: number;
55
+ /** Collection of the resolved target document. */
56
+ targetCollection?: string;
57
+ scope?: LinkResolutionScope;
58
+ /** Workspace resolution reason; absent for collection-scoped resolution. */
59
+ reason?: WorkspaceResolutionReason;
60
+ /** False for a tied workspace link: audit evidence only, never an edge. */
61
+ traversable?: boolean;
62
+ /** Tied candidates of a non-traversable link, canonical path order. */
63
+ candidates?: Array<{ id: number; docid: string; collection: string }>;
23
64
  }
24
65
 
66
+ /** Resolved and allowed to become a graph edge. */
67
+ export const isTraversableResolution = (
68
+ resolution: ResolvedGraphLinkTarget | null | undefined
69
+ ): resolution is ResolvedGraphLinkTarget =>
70
+ resolution !== null &&
71
+ resolution !== undefined &&
72
+ resolution.traversable !== false;
73
+
74
+ /** Source identity for a raw doc_links row. */
75
+ export const linkSourceIdentity = (row: {
76
+ source_collection: string;
77
+ source_rel_path: string;
78
+ target_collection: string | null;
79
+ }): GraphLinkSourceIdentity => ({
80
+ collection: row.source_collection,
81
+ relPath: row.source_rel_path,
82
+ explicit: row.target_collection !== null && row.target_collection !== "",
83
+ });
84
+
25
85
  const MAX_SQL_PARAMS = 900;
26
86
  const BULK_RESOLUTION_THRESHOLD = 128;
27
87
  export const AUDIT_LINK_SNAPSHOT_MAX_DOCUMENTS = 50_000;
@@ -48,19 +108,33 @@ export interface AuditLinkSnapshotLink {
48
108
  targetRefNorm: string;
49
109
  targetAnchor: string | null;
50
110
  targetCollection: string;
111
+ /** The link carried an explicit `collection:` prefix. */
112
+ explicitCollection?: boolean;
51
113
  linkType: "wiki" | "markdown";
52
114
  startLine: number;
53
115
  startCol: number;
54
116
  endLine: number;
55
117
  endCol: number;
56
118
  resolved: ResolvedGraphLinkTarget | null;
119
+ /**
120
+ * Unresolved workspace wiki link whose target exists as a file in the
121
+ * workspace but is not an indexed document. Existence only: no edge.
122
+ */
123
+ outsideIndex?: boolean;
57
124
  }
58
125
 
59
126
  export interface AuditLinkSnapshot {
60
127
  documents: AuditLinkSnapshotDocument[];
61
128
  /** Optional finding scope when documents also include graph-wide evidence. */
62
129
  auditedDocumentIds?: number[];
130
+ /**
131
+ * Requested collection scope of a scoped audit. Connectivity still uses
132
+ * the whole graph, but candidate identities outside it are withheld.
133
+ */
134
+ scopeCollections?: string[];
63
135
  links: AuditLinkSnapshotLink[];
136
+ /** Why some workspace file listings were incomplete, when they were. */
137
+ outsideIndexDiagnostic?: string;
64
138
  totals: { documents: number; links: number };
65
139
  truncated: { documents: boolean; links: boolean };
66
140
  metrics: {
@@ -265,10 +339,179 @@ export const resolveGraphLinkTargetsSql = (
265
339
  return results;
266
340
  };
267
341
 
268
- /** Resolve all targets in bounded SQL batches while retaining confidence inputs. */
342
+ export type GraphLinkResolutionStrategy = "auto" | "sql" | "bulk";
343
+
344
+ /**
345
+ * Resolve all targets with one contract for every graph consumer: plain wiki
346
+ * links inside a link workspace use workspace ranking; explicit, markdown and
347
+ * non-workspace links keep collection-scoped resolution. `strategy` forces
348
+ * the SQL or bulk candidate path (parity tests); results are identical.
349
+ */
269
350
  export function resolveGraphLinkTargets(
270
351
  db: Database,
271
- targets: GraphLinkTarget[]
352
+ targets: GraphLinkTarget[],
353
+ strategy: GraphLinkResolutionStrategy = "auto"
354
+ ): Array<ResolvedGraphLinkTarget | null> {
355
+ const results: Array<ResolvedGraphLinkTarget | null> = Array.from(
356
+ { length: targets.length },
357
+ () => null
358
+ );
359
+ const memberships = targets.some(
360
+ (target) =>
361
+ target.linkType === "wiki" && target.source && !target.source.explicit
362
+ )
363
+ ? loadLinkWorkspaceMemberships(db)
364
+ : new Map();
365
+ const workspaceInputs: WorkspaceTargetInput[] = [];
366
+ const workspaceIndexes: number[] = [];
367
+ const legacyTargets: GraphLinkTarget[] = [];
368
+ const legacyIndexes: number[] = [];
369
+ for (const [index, target] of targets.entries()) {
370
+ const source = target.source;
371
+ const wsKey =
372
+ target.linkType === "wiki" && source && !source.explicit
373
+ ? workspaceKeyForDocument(
374
+ memberships,
375
+ source.collection,
376
+ source.relPath
377
+ )
378
+ : null;
379
+ if (wsKey !== null && source) {
380
+ workspaceInputs.push({
381
+ targetRefNorm: target.targetRefNorm,
382
+ source: { collection: source.collection, relPath: source.relPath },
383
+ wsKey,
384
+ });
385
+ workspaceIndexes.push(index);
386
+ } else {
387
+ legacyTargets.push(target);
388
+ legacyIndexes.push(index);
389
+ }
390
+ }
391
+
392
+ const legacyResults = resolveCollectionScopedTargets(
393
+ db,
394
+ legacyTargets,
395
+ strategy
396
+ );
397
+ // Results are shared per unique resolution (as the resolvers dedupe
398
+ // targets) so large inventories do not allocate one object per link.
399
+ const legacyShared = new Map<
400
+ ResolvedGraphLinkTarget,
401
+ Map<string, ResolvedGraphLinkTarget>
402
+ >();
403
+ for (const [position, index] of legacyIndexes.entries()) {
404
+ const resolution = legacyResults[position] ?? null;
405
+ if (!resolution) continue;
406
+ const target = targets[index]!;
407
+ const scope = target.source
408
+ ? target.source.explicit
409
+ ? ("explicit-collection" as const)
410
+ : ("same-collection" as const)
411
+ : undefined;
412
+ const variantKey = `${target.targetCollection}\0${scope ?? ""}`;
413
+ let variants = legacyShared.get(resolution);
414
+ if (!variants) {
415
+ variants = new Map();
416
+ legacyShared.set(resolution, variants);
417
+ }
418
+ let shared = variants.get(variantKey);
419
+ if (!shared) {
420
+ shared = {
421
+ ...resolution,
422
+ targetCollection: target.targetCollection,
423
+ ...(scope ? { scope } : {}),
424
+ };
425
+ variants.set(variantKey, shared);
426
+ }
427
+ results[index] = shared;
428
+ }
429
+
430
+ if (workspaceInputs.length > 0) {
431
+ // Only whether the unique count passes the threshold matters.
432
+ const unique = new Set<string>();
433
+ for (const input of workspaceInputs) {
434
+ unique.add(
435
+ `${input.wsKey}\0${input.source.collection}\0${input.source.relPath}\0${input.targetRefNorm}`
436
+ );
437
+ if (unique.size > BULK_RESOLUTION_THRESHOLD) break;
438
+ }
439
+ const uniqueCount = unique.size;
440
+ const bulk =
441
+ strategy === "bulk" ||
442
+ (strategy === "auto" &&
443
+ uniqueCount > BULK_RESOLUTION_THRESHOLD &&
444
+ countActiveDocuments(
445
+ db,
446
+ workspaceMemberCollections(
447
+ memberships,
448
+ new Set(workspaceInputs.map((input) => input.wsKey))
449
+ )
450
+ ) <= GRAPH_LINK_BULK_MAX_DOCUMENTS);
451
+ const workspaceResults = resolveWorkspaceTargets(
452
+ db,
453
+ memberships,
454
+ workspaceInputs,
455
+ { bulk }
456
+ );
457
+ const workspaceShared = new Map<object, ResolvedGraphLinkTarget>();
458
+ for (const [position, index] of workspaceIndexes.entries()) {
459
+ const resolution = workspaceResults[position] ?? null;
460
+ const source = targets[index]!.source!;
461
+ // One resolution object per (workspace, source collection, folder,
462
+ // target), so its scope is fixed and the converted result is shared.
463
+ const cached = resolution ? workspaceShared.get(resolution) : undefined;
464
+ if (cached) {
465
+ results[index] = cached;
466
+ continue;
467
+ }
468
+ results[index] = resolution
469
+ ? {
470
+ targetId: resolution.target.id,
471
+ targetDocid: resolution.target.docid,
472
+ matchRank: resolution.titleRank ?? 0,
473
+ matchCount: resolution.traversable ? 1 : resolution.tied.length,
474
+ targetCollection: resolution.target.collection,
475
+ scope:
476
+ resolution.target.collection === source.collection
477
+ ? "same-collection"
478
+ : "cross-collection",
479
+ reason: resolution.reason,
480
+ traversable: resolution.traversable,
481
+ ...(resolution.traversable
482
+ ? {}
483
+ : {
484
+ candidates: resolution.tied.map(
485
+ ({ id, docid, collection }) => ({ id, docid, collection })
486
+ ),
487
+ }),
488
+ }
489
+ : null;
490
+ if (resolution && results[index]) {
491
+ workspaceShared.set(resolution, results[index]!);
492
+ }
493
+ }
494
+ }
495
+ return results;
496
+ }
497
+
498
+ const countActiveDocuments = (
499
+ db: Database,
500
+ collections: readonly string[]
501
+ ): number =>
502
+ collections.length === 0
503
+ ? 0
504
+ : (db
505
+ .query<{ count: number }, string[]>(
506
+ `SELECT COUNT(*) AS count FROM documents WHERE active = 1 AND collection IN (${collections.map(() => "?").join(",")})`
507
+ )
508
+ .get(...collections)?.count ?? 0);
509
+
510
+ /** Collection-scoped (pre-workspace) resolution, deduplicated per target. */
511
+ function resolveCollectionScopedTargets(
512
+ db: Database,
513
+ targets: GraphLinkTarget[],
514
+ strategy: GraphLinkResolutionStrategy
272
515
  ): Array<ResolvedGraphLinkTarget | null> {
273
516
  const uniqueTargets: GraphLinkTarget[] = [];
274
517
  const uniqueIndexByKey = new Map<string, number>();
@@ -289,7 +532,10 @@ export function resolveGraphLinkTargets(
289
532
  originalToUniqueIndex.push(uniqueIndex);
290
533
  }
291
534
 
292
- if (uniqueTargets.length > BULK_RESOLUTION_THRESHOLD) {
535
+ if (
536
+ strategy === "bulk" ||
537
+ (strategy === "auto" && uniqueTargets.length > BULK_RESOLUTION_THRESHOLD)
538
+ ) {
293
539
  const bulkResults = resolveGraphLinkTargetsBulk(db, uniqueTargets);
294
540
  if (bulkResults) {
295
541
  return originalToUniqueIndex.map(
@@ -396,6 +642,7 @@ export function captureAuditLinkSnapshot(
396
642
  source_uri: string;
397
643
  source_collection: string;
398
644
  source_rel_path: string;
645
+ source_doc_rel_path: string;
399
646
  target_ref: string;
400
647
  target_ref_norm: string;
401
648
  target_anchor: string | null;
@@ -410,7 +657,8 @@ export function captureAuditLinkSnapshot(
410
657
  >(
411
658
  `SELECT d.id AS source_id, d.docid AS source_docid,
412
659
  d.uri AS source_uri, d.collection AS source_collection,
413
- COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) AS source_rel_path, dl.target_ref,
660
+ COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) AS source_rel_path,
661
+ d.rel_path AS source_doc_rel_path, dl.target_ref,
414
662
  dl.target_ref_norm, dl.target_anchor, dl.target_collection,
415
663
  dl.link_type, dl.start_line, dl.start_col,
416
664
  dl.end_line, dl.end_col
@@ -425,6 +673,11 @@ export function captureAuditLinkSnapshot(
425
673
  targetRefNorm: row.target_ref_norm,
426
674
  targetCollection: row.target_collection ?? row.source_collection,
427
675
  linkType: row.link_type,
676
+ source: linkSourceIdentity({
677
+ source_collection: row.source_collection,
678
+ source_rel_path: row.source_doc_rel_path,
679
+ target_collection: row.target_collection,
680
+ }),
428
681
  }));
429
682
  const resolutions = resolveGraphLinkTargets(db, targets);
430
683
  const uniqueTargetsResolved = new Set(
@@ -458,6 +711,7 @@ export function captureAuditLinkSnapshot(
458
711
  targetRefNorm: row.target_ref_norm,
459
712
  targetAnchor: row.target_anchor,
460
713
  targetCollection: row.target_collection ?? row.source_collection,
714
+ explicitCollection: row.target_collection !== null,
461
715
  linkType: row.link_type,
462
716
  startLine: row.start_line,
463
717
  startCol: row.start_col,