@gmickel/gno 1.32.0 → 1.34.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 (54) hide show
  1. package/README.md +17 -3
  2. package/assets/skill/SKILL.md +30 -0
  3. package/assets/skill/cli-reference.md +10 -2
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.32.0.zip → gno-browser-clipper-v1.34.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.34.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +5 -1
  8. package/spec/cli.md +60 -1
  9. package/spec/mcp.md +21 -0
  10. package/spec/output-schemas/audit-report.schema.json +284 -0
  11. package/spec/output-schemas/publish-artifact.schema.json +76 -1
  12. package/src/cli/commands/audit.ts +231 -0
  13. package/src/cli/commands/publish.ts +43 -7
  14. package/src/cli/errors.ts +9 -2
  15. package/src/cli/program.ts +112 -0
  16. package/src/core/audit-contract.ts +296 -0
  17. package/src/core/audit-freshness.ts +233 -0
  18. package/src/core/audit-links.ts +222 -0
  19. package/src/core/audit-provenance.ts +154 -0
  20. package/src/core/audit-report.ts +318 -0
  21. package/src/core/audit-workspace.ts +678 -0
  22. package/src/core/audit.ts +569 -0
  23. package/src/core/capture.ts +196 -3
  24. package/src/core/document-capabilities.ts +9 -8
  25. package/src/core/record-metadata.ts +33 -0
  26. package/src/ingestion/strip.ts +152 -26
  27. package/src/mcp/http-egress.ts +8 -0
  28. package/src/mcp/tools/audit.ts +97 -0
  29. package/src/mcp/tools/index.ts +13 -0
  30. package/src/publish/artifact-asset-codec.ts +75 -0
  31. package/src/publish/artifact-asset-contract.ts +152 -0
  32. package/src/publish/artifact-asset-parse.ts +401 -0
  33. package/src/publish/artifact-asset-sniff.ts +108 -0
  34. package/src/publish/artifact-asset-validate.ts +209 -0
  35. package/src/publish/artifact-assets.ts +58 -0
  36. package/src/publish/artifact-validation.ts +32 -6
  37. package/src/publish/artifact.ts +50 -3
  38. package/src/publish/attachment-bundle.ts +145 -0
  39. package/src/publish/attachment-discover.ts +203 -0
  40. package/src/publish/attachment-load.ts +133 -0
  41. package/src/publish/attachment-obsidian.ts +45 -0
  42. package/src/publish/attachment-path.ts +334 -0
  43. package/src/publish/attachment-raster.ts +852 -0
  44. package/src/publish/attachment-resolver.ts +280 -0
  45. package/src/publish/attachment-types.ts +54 -0
  46. package/src/publish/encrypted-export.ts +121 -44
  47. package/src/publish/export-attachments.ts +224 -0
  48. package/src/publish/export-service.ts +142 -80
  49. package/src/publish/obsidian-sanitize.ts +121 -13
  50. package/src/serve/routes/api.ts +2 -1
  51. package/src/store/sqlite/adapter.ts +82 -0
  52. package/src/store/sqlite/graph-link-bulk-resolver.ts +191 -0
  53. package/src/store/sqlite/graph-link-resolver.ts +241 -2
  54. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +0 -1
@@ -7,6 +7,7 @@
7
7
  import type { Database } from "bun:sqlite";
8
8
 
9
9
  import { stripWikiMdExt } from "../../core/links";
10
+ import { resolveGraphLinkTargetsBulk } from "./graph-link-bulk-resolver";
10
11
 
11
12
  export interface GraphLinkTarget {
12
13
  targetRefNorm: string;
@@ -22,6 +23,60 @@ export interface ResolvedGraphLinkTarget {
22
23
  }
23
24
 
24
25
  const MAX_SQL_PARAMS = 900;
26
+ const BULK_RESOLUTION_THRESHOLD = 128;
27
+ export const AUDIT_LINK_SNAPSHOT_MAX_DOCUMENTS = 50_000;
28
+ export const AUDIT_LINK_SNAPSHOT_MAX_LINKS = 50_000;
29
+
30
+ export interface AuditLinkSnapshotDocument {
31
+ id: number;
32
+ docid: string;
33
+ uri: string;
34
+ collection: string;
35
+ relPath: string;
36
+ recordSourcePath: string | null;
37
+ title: string | null;
38
+ mirrorHash: string | null;
39
+ }
40
+
41
+ export interface AuditLinkSnapshotLink {
42
+ sourceId: number;
43
+ sourceDocid: string;
44
+ sourceUri: string;
45
+ sourceCollection: string;
46
+ sourceRelPath: string;
47
+ targetRef: string;
48
+ targetRefNorm: string;
49
+ targetAnchor: string | null;
50
+ targetCollection: string;
51
+ linkType: "wiki" | "markdown";
52
+ startLine: number;
53
+ startCol: number;
54
+ endLine: number;
55
+ endCol: number;
56
+ resolved: ResolvedGraphLinkTarget | null;
57
+ }
58
+
59
+ export interface AuditLinkSnapshot {
60
+ documents: AuditLinkSnapshotDocument[];
61
+ /** Optional finding scope when documents also include graph-wide evidence. */
62
+ auditedDocumentIds?: number[];
63
+ links: AuditLinkSnapshotLink[];
64
+ totals: { documents: number; links: number };
65
+ truncated: { documents: boolean; links: boolean };
66
+ metrics: {
67
+ documentRowsExamined: number;
68
+ linkRowsExamined: number;
69
+ uniqueTargetsResolved: number;
70
+ batchedResolution: true;
71
+ };
72
+ }
73
+
74
+ export interface AuditLinkSnapshotOptions {
75
+ collections?: readonly string[];
76
+ pathPrefixes?: readonly string[];
77
+ maxDocuments?: number;
78
+ maxLinks?: number;
79
+ }
25
80
 
26
81
  const chunkArray = <T>(items: T[], chunkSize: number): T[][] => {
27
82
  const chunks: T[][] = [];
@@ -36,7 +91,7 @@ const suffixMatch = (targetExpr: string, valueExpr: string): string =>
36
91
  AND (length(${targetExpr}) = length(${valueExpr})
37
92
  OR substr(${targetExpr}, -length(${valueExpr}) - 1, 1) = '/'))`;
38
93
 
39
- const resolveUniqueGraphLinkTargets = (
94
+ export const resolveGraphLinkTargetsSql = (
40
95
  db: Database,
41
96
  targets: GraphLinkTarget[]
42
97
  ): Array<ResolvedGraphLinkTarget | null> => {
@@ -234,8 +289,192 @@ export function resolveGraphLinkTargets(
234
289
  originalToUniqueIndex.push(uniqueIndex);
235
290
  }
236
291
 
237
- const uniqueResults = resolveUniqueGraphLinkTargets(db, uniqueTargets);
292
+ if (uniqueTargets.length > BULK_RESOLUTION_THRESHOLD) {
293
+ const bulkResults = resolveGraphLinkTargetsBulk(db, uniqueTargets);
294
+ if (bulkResults) {
295
+ return originalToUniqueIndex.map(
296
+ (uniqueIndex) => bulkResults[uniqueIndex] ?? null
297
+ );
298
+ }
299
+ }
300
+
301
+ const uniqueResults = resolveGraphLinkTargetsSql(db, uniqueTargets);
238
302
  return originalToUniqueIndex.map(
239
303
  (uniqueIndex) => uniqueResults[uniqueIndex] ?? null
240
304
  );
241
305
  }
306
+
307
+ /**
308
+ * Capture one bounded, read-only link inventory using set-oriented SQL and the
309
+ * same target resolver as graph expansion. No query is issued per finding.
310
+ */
311
+ export function captureAuditLinkSnapshot(
312
+ db: Database,
313
+ options: AuditLinkSnapshotOptions = {}
314
+ ): AuditLinkSnapshot {
315
+ const maxDocuments = Math.max(
316
+ 1,
317
+ Math.min(
318
+ AUDIT_LINK_SNAPSHOT_MAX_DOCUMENTS,
319
+ options.maxDocuments ?? AUDIT_LINK_SNAPSHOT_MAX_DOCUMENTS
320
+ )
321
+ );
322
+ const maxLinks = Math.max(
323
+ 1,
324
+ Math.min(
325
+ AUDIT_LINK_SNAPSHOT_MAX_LINKS,
326
+ options.maxLinks ?? AUDIT_LINK_SNAPSHOT_MAX_LINKS
327
+ )
328
+ );
329
+ const conditions = ["d.active = 1"];
330
+ const params: string[] = [];
331
+ const collections = [...new Set(options.collections ?? [])]
332
+ .map((value) => value.normalize("NFC").trim())
333
+ .filter(Boolean)
334
+ .sort();
335
+ if (collections.length > 0) {
336
+ conditions.push(
337
+ `d.collection IN (${collections.map(() => "?").join(",")})`
338
+ );
339
+ params.push(...collections);
340
+ }
341
+ const prefixes = [...new Set(options.pathPrefixes ?? [])]
342
+ .map((value) => value.normalize("NFC").trim().replace(/^\/+/, ""))
343
+ .filter(Boolean)
344
+ .sort();
345
+ if (prefixes.length > 0) {
346
+ conditions.push(
347
+ `(${prefixes.map(() => "COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) = ? OR COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) LIKE ? ESCAPE '\\'").join(" OR ")})`
348
+ );
349
+ for (const prefix of prefixes) {
350
+ const escaped = prefix
351
+ .replaceAll("\\", "\\\\")
352
+ .replaceAll("%", "\\%")
353
+ .replaceAll("_", "\\_");
354
+ params.push(prefix, `${escaped}/%`);
355
+ }
356
+ }
357
+ const where = conditions.join(" AND ");
358
+ const totalDocuments =
359
+ db
360
+ .query<{ count: number }, string[]>(
361
+ `SELECT COUNT(*) AS count FROM documents d WHERE ${where}`
362
+ )
363
+ .get(...params)?.count ?? 0;
364
+ const documentRows = db
365
+ .query<
366
+ {
367
+ id: number;
368
+ docid: string;
369
+ uri: string;
370
+ collection: string;
371
+ rel_path: string;
372
+ record_source_path: string | null;
373
+ title: string | null;
374
+ mirror_hash: string | null;
375
+ },
376
+ (string | number)[]
377
+ >(
378
+ `SELECT d.id, d.docid, d.uri, d.collection, d.rel_path, d.record_source_path, d.title, d.mirror_hash
379
+ FROM documents d WHERE ${where} ORDER BY d.id LIMIT ?`
380
+ )
381
+ .all(...params, maxDocuments);
382
+ const totalLinks =
383
+ db
384
+ .query<{ count: number }, string[]>(
385
+ `SELECT COUNT(*) AS count
386
+ FROM doc_links dl
387
+ JOIN documents d ON d.id = dl.source_doc_id
388
+ WHERE ${where} AND dl.source = 'parsed'`
389
+ )
390
+ .get(...params)?.count ?? 0;
391
+ const rawLinks = db
392
+ .query<
393
+ {
394
+ source_id: number;
395
+ source_docid: string;
396
+ source_uri: string;
397
+ source_collection: string;
398
+ source_rel_path: string;
399
+ target_ref: string;
400
+ target_ref_norm: string;
401
+ target_anchor: string | null;
402
+ target_collection: string | null;
403
+ link_type: "wiki" | "markdown";
404
+ start_line: number;
405
+ start_col: number;
406
+ end_line: number;
407
+ end_col: number;
408
+ },
409
+ (string | number)[]
410
+ >(
411
+ `SELECT d.id AS source_id, d.docid AS source_docid,
412
+ 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,
414
+ dl.target_ref_norm, dl.target_anchor, dl.target_collection,
415
+ dl.link_type, dl.start_line, dl.start_col,
416
+ dl.end_line, dl.end_col
417
+ FROM doc_links dl
418
+ JOIN documents d ON d.id = dl.source_doc_id
419
+ WHERE ${where} AND dl.source = 'parsed'
420
+ ORDER BY d.id, dl.start_line, dl.start_col, dl.id
421
+ LIMIT ?`
422
+ )
423
+ .all(...params, maxLinks);
424
+ const targets = rawLinks.map((row) => ({
425
+ targetRefNorm: row.target_ref_norm,
426
+ targetCollection: row.target_collection ?? row.source_collection,
427
+ linkType: row.link_type,
428
+ }));
429
+ const resolutions = resolveGraphLinkTargets(db, targets);
430
+ const uniqueTargetsResolved = new Set(
431
+ targets.map((target) =>
432
+ JSON.stringify([
433
+ target.linkType,
434
+ target.targetCollection,
435
+ target.targetRefNorm,
436
+ ])
437
+ )
438
+ ).size;
439
+
440
+ return {
441
+ documents: documentRows.map((row) => ({
442
+ id: row.id,
443
+ docid: row.docid,
444
+ uri: row.uri,
445
+ collection: row.collection,
446
+ relPath: row.rel_path,
447
+ recordSourcePath: row.record_source_path,
448
+ title: row.title,
449
+ mirrorHash: row.mirror_hash,
450
+ })),
451
+ links: rawLinks.map((row, index) => ({
452
+ sourceId: row.source_id,
453
+ sourceDocid: row.source_docid,
454
+ sourceUri: row.source_uri,
455
+ sourceCollection: row.source_collection,
456
+ sourceRelPath: row.source_rel_path,
457
+ targetRef: row.target_ref,
458
+ targetRefNorm: row.target_ref_norm,
459
+ targetAnchor: row.target_anchor,
460
+ targetCollection: row.target_collection ?? row.source_collection,
461
+ linkType: row.link_type,
462
+ startLine: row.start_line,
463
+ startCol: row.start_col,
464
+ endLine: row.end_line,
465
+ endCol: row.end_col,
466
+ resolved: resolutions[index] ?? null,
467
+ })),
468
+ totals: { documents: totalDocuments, links: totalLinks },
469
+ truncated: {
470
+ documents: totalDocuments > documentRows.length,
471
+ links: totalLinks > rawLinks.length,
472
+ },
473
+ metrics: {
474
+ documentRowsExamined: documentRows.length,
475
+ linkRowsExamined: rawLinks.length,
476
+ uniqueTargetsResolved,
477
+ batchedResolution: true,
478
+ },
479
+ };
480
+ }
@@ -1 +0,0 @@
1
- cb5c4bba8cf1252da4e4fc89f1dc01a8df306851cccb50f636d89a6ef3133cff gno-browser-clipper-v1.32.0.zip