@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.
Files changed (99) hide show
  1. package/README.md +26 -30
  2. package/assets/skill/SKILL.md +8 -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/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.7.0.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +2 -1
  11. package/spec/cli.md +48 -6
  12. package/spec/db/schema.sql +0 -1
  13. package/spec/mcp.md +18 -3
  14. package/spec/output-schemas/audit-report.schema.json +18 -4
  15. package/spec/output-schemas/backlinks.schema.json +4 -0
  16. package/spec/output-schemas/collection-list.schema.json +15 -2
  17. package/spec/output-schemas/graph.schema.json +2 -0
  18. package/spec/output-schemas/links-list.schema.json +4 -0
  19. package/spec/output-schemas/status.schema.json +27 -16
  20. package/src/cli/commands/ask.ts +31 -12
  21. package/src/cli/commands/audit.ts +23 -4
  22. package/src/cli/commands/collection/list.ts +39 -5
  23. package/src/cli/commands/embed.ts +3 -3
  24. package/src/cli/commands/links.ts +34 -131
  25. package/src/cli/commands/ls.ts +6 -1
  26. package/src/cli/commands/shared.ts +7 -0
  27. package/src/cli/commands/status.ts +5 -0
  28. package/src/cli/program.ts +12 -2
  29. package/src/config/loader.ts +43 -0
  30. package/src/config/types.ts +8 -0
  31. package/src/core/audit-contract.ts +16 -4
  32. package/src/core/audit-freshness.ts +11 -1
  33. package/src/core/audit-links.ts +145 -25
  34. package/src/core/audit-provenance.ts +11 -4
  35. package/src/core/audit-workspace.ts +19 -4
  36. package/src/core/audit.ts +67 -15
  37. package/src/core/context-compiler.ts +3 -0
  38. package/src/core/context-evidence.ts +11 -0
  39. package/src/core/graph-edge-confidence.ts +23 -1
  40. package/src/core/host-paths.ts +24 -5
  41. package/src/core/knowledge-impact.ts +28 -0
  42. package/src/core/link-workspace.ts +324 -0
  43. package/src/core/request-receipts.ts +63 -9
  44. package/src/core/retrieval-replay-candidate.ts +6 -0
  45. package/src/core/retrieval-trace-request.ts +3 -0
  46. package/src/core/windows-private-path.ts +136 -1
  47. package/src/embed/backlog.ts +22 -10
  48. package/src/embed/variant-backlog.ts +34 -9
  49. package/src/index.ts +14 -1
  50. package/src/ingestion/graph-reconciliation.ts +77 -15
  51. package/src/ingestion/source-availability/darwin-path.ts +9 -3
  52. package/src/ingestion/sync.ts +22 -1
  53. package/src/ingestion/types.ts +14 -0
  54. package/src/llm/inference-scope.ts +19 -0
  55. package/src/mcp/http-egress.ts +42 -3
  56. package/src/mcp/tools/audit.ts +11 -2
  57. package/src/mcp/tools/changes.ts +1 -0
  58. package/src/mcp/tools/links.ts +3 -0
  59. package/src/mcp/tools/sessions.ts +33 -4
  60. package/src/mcp/tools/status.ts +22 -6
  61. package/src/pipeline/expansion.ts +19 -31
  62. package/src/pipeline/graph-retrieval.ts +22 -2
  63. package/src/pipeline/hybrid.ts +1 -1
  64. package/src/pipeline/types.ts +6 -3
  65. package/src/serve/embed-scheduler.ts +2 -2
  66. package/src/serve/findings-pass.ts +1 -1
  67. package/src/serve/host-path-redaction.ts +51 -14
  68. package/src/serve/public/components/BootstrapStatus.tsx +5 -3
  69. package/src/serve/public/components/CaptureModal.tsx +1 -1
  70. package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
  71. package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
  72. package/src/serve/public/components/FirstRunWizard.tsx +4 -2
  73. package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
  74. package/src/serve/public/pages/Collections.tsx +16 -13
  75. package/src/serve/public/pages/Connectors.tsx +7 -4
  76. package/src/serve/public/pages/Dashboard.tsx +8 -6
  77. package/src/serve/public/pages/GraphView.tsx +2 -0
  78. package/src/serve/resident-runtime.ts +7 -0
  79. package/src/serve/routes/changes.ts +6 -1
  80. package/src/serve/routes/links.ts +13 -0
  81. package/src/serve/routes/sessions.ts +81 -37
  82. package/src/serve/server.ts +5 -3
  83. package/src/serve/status-model.ts +10 -6
  84. package/src/serve/status.ts +2 -7
  85. package/src/sessions/config-refresh.ts +111 -0
  86. package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
  87. package/src/store/migrations/034-collection-link-workspace.ts +47 -0
  88. package/src/store/migrations/index.ts +4 -0
  89. package/src/store/sqlite/adapter.ts +392 -233
  90. package/src/store/sqlite/change-journal-store.ts +1 -1
  91. package/src/store/sqlite/eligibility.ts +8 -2
  92. package/src/store/sqlite/graph-link-resolver.ts +252 -5
  93. package/src/store/sqlite/graph-neighbors.ts +147 -40
  94. package/src/store/sqlite/graph-reference-state.ts +13 -2
  95. package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
  96. package/src/store/sqlite/workspace-link-resolver.ts +654 -0
  97. package/src/store/types.ts +49 -3
  98. package/src/store/vector/stats.ts +1 -1
  99. package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +0 -1
@@ -180,7 +180,7 @@ const enforceInTransaction = (
180
180
  db
181
181
  .query<{ sequence: number | null }, [number]>(
182
182
  `SELECT MAX(sequence) AS sequence
183
- FROM document_changes
183
+ FROM document_changes INDEXED BY idx_document_changes_retention
184
184
  WHERE observed_at_ms <= ?`
185
185
  )
186
186
  .get(ageBoundary)?.sequence ?? state.retention_floor;
@@ -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,6 +108,8 @@ 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;
@@ -60,6 +122,11 @@ export interface AuditLinkSnapshot {
60
122
  documents: AuditLinkSnapshotDocument[];
61
123
  /** Optional finding scope when documents also include graph-wide evidence. */
62
124
  auditedDocumentIds?: number[];
125
+ /**
126
+ * Requested collection scope of a scoped audit. Connectivity still uses
127
+ * the whole graph, but candidate identities outside it are withheld.
128
+ */
129
+ scopeCollections?: string[];
63
130
  links: AuditLinkSnapshotLink[];
64
131
  totals: { documents: number; links: number };
65
132
  truncated: { documents: boolean; links: boolean };
@@ -265,10 +332,179 @@ export const resolveGraphLinkTargetsSql = (
265
332
  return results;
266
333
  };
267
334
 
268
- /** Resolve all targets in bounded SQL batches while retaining confidence inputs. */
335
+ export type GraphLinkResolutionStrategy = "auto" | "sql" | "bulk";
336
+
337
+ /**
338
+ * Resolve all targets with one contract for every graph consumer: plain wiki
339
+ * links inside a link workspace use workspace ranking; explicit, markdown and
340
+ * non-workspace links keep collection-scoped resolution. `strategy` forces
341
+ * the SQL or bulk candidate path (parity tests); results are identical.
342
+ */
269
343
  export function resolveGraphLinkTargets(
270
344
  db: Database,
271
- targets: GraphLinkTarget[]
345
+ targets: GraphLinkTarget[],
346
+ strategy: GraphLinkResolutionStrategy = "auto"
347
+ ): Array<ResolvedGraphLinkTarget | null> {
348
+ const results: Array<ResolvedGraphLinkTarget | null> = Array.from(
349
+ { length: targets.length },
350
+ () => null
351
+ );
352
+ const memberships = targets.some(
353
+ (target) =>
354
+ target.linkType === "wiki" && target.source && !target.source.explicit
355
+ )
356
+ ? loadLinkWorkspaceMemberships(db)
357
+ : new Map();
358
+ const workspaceInputs: WorkspaceTargetInput[] = [];
359
+ const workspaceIndexes: number[] = [];
360
+ const legacyTargets: GraphLinkTarget[] = [];
361
+ const legacyIndexes: number[] = [];
362
+ for (const [index, target] of targets.entries()) {
363
+ const source = target.source;
364
+ const wsKey =
365
+ target.linkType === "wiki" && source && !source.explicit
366
+ ? workspaceKeyForDocument(
367
+ memberships,
368
+ source.collection,
369
+ source.relPath
370
+ )
371
+ : null;
372
+ if (wsKey !== null && source) {
373
+ workspaceInputs.push({
374
+ targetRefNorm: target.targetRefNorm,
375
+ source: { collection: source.collection, relPath: source.relPath },
376
+ wsKey,
377
+ });
378
+ workspaceIndexes.push(index);
379
+ } else {
380
+ legacyTargets.push(target);
381
+ legacyIndexes.push(index);
382
+ }
383
+ }
384
+
385
+ const legacyResults = resolveCollectionScopedTargets(
386
+ db,
387
+ legacyTargets,
388
+ strategy
389
+ );
390
+ // Results are shared per unique resolution (as the resolvers dedupe
391
+ // targets) so large inventories do not allocate one object per link.
392
+ const legacyShared = new Map<
393
+ ResolvedGraphLinkTarget,
394
+ Map<string, ResolvedGraphLinkTarget>
395
+ >();
396
+ for (const [position, index] of legacyIndexes.entries()) {
397
+ const resolution = legacyResults[position] ?? null;
398
+ if (!resolution) continue;
399
+ const target = targets[index]!;
400
+ const scope = target.source
401
+ ? target.source.explicit
402
+ ? ("explicit-collection" as const)
403
+ : ("same-collection" as const)
404
+ : undefined;
405
+ const variantKey = `${target.targetCollection}\0${scope ?? ""}`;
406
+ let variants = legacyShared.get(resolution);
407
+ if (!variants) {
408
+ variants = new Map();
409
+ legacyShared.set(resolution, variants);
410
+ }
411
+ let shared = variants.get(variantKey);
412
+ if (!shared) {
413
+ shared = {
414
+ ...resolution,
415
+ targetCollection: target.targetCollection,
416
+ ...(scope ? { scope } : {}),
417
+ };
418
+ variants.set(variantKey, shared);
419
+ }
420
+ results[index] = shared;
421
+ }
422
+
423
+ if (workspaceInputs.length > 0) {
424
+ // Only whether the unique count passes the threshold matters.
425
+ const unique = new Set<string>();
426
+ for (const input of workspaceInputs) {
427
+ unique.add(
428
+ `${input.wsKey}\0${input.source.collection}\0${input.source.relPath}\0${input.targetRefNorm}`
429
+ );
430
+ if (unique.size > BULK_RESOLUTION_THRESHOLD) break;
431
+ }
432
+ const uniqueCount = unique.size;
433
+ const bulk =
434
+ strategy === "bulk" ||
435
+ (strategy === "auto" &&
436
+ uniqueCount > BULK_RESOLUTION_THRESHOLD &&
437
+ countActiveDocuments(
438
+ db,
439
+ workspaceMemberCollections(
440
+ memberships,
441
+ new Set(workspaceInputs.map((input) => input.wsKey))
442
+ )
443
+ ) <= GRAPH_LINK_BULK_MAX_DOCUMENTS);
444
+ const workspaceResults = resolveWorkspaceTargets(
445
+ db,
446
+ memberships,
447
+ workspaceInputs,
448
+ { bulk }
449
+ );
450
+ const workspaceShared = new Map<object, ResolvedGraphLinkTarget>();
451
+ for (const [position, index] of workspaceIndexes.entries()) {
452
+ const resolution = workspaceResults[position] ?? null;
453
+ const source = targets[index]!.source!;
454
+ // One resolution object per (workspace, source collection, folder,
455
+ // target), so its scope is fixed and the converted result is shared.
456
+ const cached = resolution ? workspaceShared.get(resolution) : undefined;
457
+ if (cached) {
458
+ results[index] = cached;
459
+ continue;
460
+ }
461
+ results[index] = resolution
462
+ ? {
463
+ targetId: resolution.target.id,
464
+ targetDocid: resolution.target.docid,
465
+ matchRank: resolution.titleRank ?? 0,
466
+ matchCount: resolution.traversable ? 1 : resolution.tied.length,
467
+ targetCollection: resolution.target.collection,
468
+ scope:
469
+ resolution.target.collection === source.collection
470
+ ? "same-collection"
471
+ : "cross-collection",
472
+ reason: resolution.reason,
473
+ traversable: resolution.traversable,
474
+ ...(resolution.traversable
475
+ ? {}
476
+ : {
477
+ candidates: resolution.tied.map(
478
+ ({ id, docid, collection }) => ({ id, docid, collection })
479
+ ),
480
+ }),
481
+ }
482
+ : null;
483
+ if (resolution && results[index]) {
484
+ workspaceShared.set(resolution, results[index]!);
485
+ }
486
+ }
487
+ }
488
+ return results;
489
+ }
490
+
491
+ const countActiveDocuments = (
492
+ db: Database,
493
+ collections: readonly string[]
494
+ ): number =>
495
+ collections.length === 0
496
+ ? 0
497
+ : (db
498
+ .query<{ count: number }, string[]>(
499
+ `SELECT COUNT(*) AS count FROM documents WHERE active = 1 AND collection IN (${collections.map(() => "?").join(",")})`
500
+ )
501
+ .get(...collections)?.count ?? 0);
502
+
503
+ /** Collection-scoped (pre-workspace) resolution, deduplicated per target. */
504
+ function resolveCollectionScopedTargets(
505
+ db: Database,
506
+ targets: GraphLinkTarget[],
507
+ strategy: GraphLinkResolutionStrategy
272
508
  ): Array<ResolvedGraphLinkTarget | null> {
273
509
  const uniqueTargets: GraphLinkTarget[] = [];
274
510
  const uniqueIndexByKey = new Map<string, number>();
@@ -289,7 +525,10 @@ export function resolveGraphLinkTargets(
289
525
  originalToUniqueIndex.push(uniqueIndex);
290
526
  }
291
527
 
292
- if (uniqueTargets.length > BULK_RESOLUTION_THRESHOLD) {
528
+ if (
529
+ strategy === "bulk" ||
530
+ (strategy === "auto" && uniqueTargets.length > BULK_RESOLUTION_THRESHOLD)
531
+ ) {
293
532
  const bulkResults = resolveGraphLinkTargetsBulk(db, uniqueTargets);
294
533
  if (bulkResults) {
295
534
  return originalToUniqueIndex.map(
@@ -396,6 +635,7 @@ export function captureAuditLinkSnapshot(
396
635
  source_uri: string;
397
636
  source_collection: string;
398
637
  source_rel_path: string;
638
+ source_doc_rel_path: string;
399
639
  target_ref: string;
400
640
  target_ref_norm: string;
401
641
  target_anchor: string | null;
@@ -410,7 +650,8 @@ export function captureAuditLinkSnapshot(
410
650
  >(
411
651
  `SELECT d.id AS source_id, d.docid AS source_docid,
412
652
  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,
653
+ COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) AS source_rel_path,
654
+ d.rel_path AS source_doc_rel_path, dl.target_ref,
414
655
  dl.target_ref_norm, dl.target_anchor, dl.target_collection,
415
656
  dl.link_type, dl.start_line, dl.start_col,
416
657
  dl.end_line, dl.end_col
@@ -425,6 +666,11 @@ export function captureAuditLinkSnapshot(
425
666
  targetRefNorm: row.target_ref_norm,
426
667
  targetCollection: row.target_collection ?? row.source_collection,
427
668
  linkType: row.link_type,
669
+ source: linkSourceIdentity({
670
+ source_collection: row.source_collection,
671
+ source_rel_path: row.source_doc_rel_path,
672
+ target_collection: row.target_collection,
673
+ }),
428
674
  }));
429
675
  const resolutions = resolveGraphLinkTargets(db, targets);
430
676
  const uniqueTargetsResolved = new Set(
@@ -458,6 +704,7 @@ export function captureAuditLinkSnapshot(
458
704
  targetRefNorm: row.target_ref_norm,
459
705
  targetAnchor: row.target_anchor,
460
706
  targetCollection: row.target_collection ?? row.source_collection,
707
+ explicitCollection: row.target_collection !== null,
461
708
  linkType: row.link_type,
462
709
  startLine: row.start_line,
463
710
  startCol: row.start_col,