@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
@@ -0,0 +1,296 @@
1
+ /** Versioned types and bounds for deterministic knowledge integrity audits. */
2
+
3
+ import type { StorePort } from "../store/types";
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // Versions & bounds
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ export const AUDIT_SCHEMA_VERSION = "1.0" as const;
10
+ export const AUDIT_RULE_SET_VERSION = "1.0" as const;
11
+
12
+ /** Default returned finding cap; totals remain exact when truncated. */
13
+ export const AUDIT_DEFAULT_MAX_FINDINGS = 100;
14
+ /** Hard upper bound for `--max-findings` / MCP maxFindings. */
15
+ export const AUDIT_MAX_FINDINGS_LIMIT = 1000;
16
+ export const AUDIT_MAX_EVIDENCE_PER_FINDING = 8;
17
+ export const AUDIT_MAX_GUIDANCE_PER_FINDING = 4;
18
+ export const AUDIT_MAX_EVIDENCE_DETAIL_CHARS = 512;
19
+ export const AUDIT_MAX_MESSAGE_CHARS = 512;
20
+ export const AUDIT_MAX_GUIDANCE_CHARS = 256;
21
+ export const AUDIT_MAX_CODE_CHARS = 128;
22
+ export const AUDIT_MAX_IDENTIFIER_CHARS = 2048;
23
+ export const AUDIT_MAX_SCOPE_ITEMS = 256;
24
+ export const AUDIT_MAX_SCOPE_VALUE_CHARS = 256;
25
+ /** Bounded snapshot retries before `changed_during_audit`. */
26
+ export const AUDIT_MAX_SNAPSHOT_ATTEMPTS = 2;
27
+
28
+ export const AUDIT_CATEGORIES = ["links", "provenance", "freshness"] as const;
29
+ export type AuditCategory = (typeof AUDIT_CATEGORIES)[number];
30
+
31
+ export const AUDIT_RULE_STATUSES = [
32
+ "pass",
33
+ "fail",
34
+ "skip",
35
+ "unavailable",
36
+ "inconclusive",
37
+ ] as const;
38
+ export type AuditRuleStatus = (typeof AUDIT_RULE_STATUSES)[number];
39
+
40
+ export const AUDIT_REPORT_STATUSES = [
41
+ "complete",
42
+ "partial",
43
+ "changed_during_audit",
44
+ "failed",
45
+ ] as const;
46
+ export type AuditReportStatus = (typeof AUDIT_REPORT_STATUSES)[number];
47
+
48
+ export const AUDIT_EXIT_KINDS = [
49
+ "clean",
50
+ "findings",
51
+ "invalid",
52
+ "partial",
53
+ "runtime",
54
+ ] as const;
55
+ export type AuditExitKind = (typeof AUDIT_EXIT_KINDS)[number];
56
+
57
+ /**
58
+ * Process exit codes for `gno audit` / MCP surfaces.
59
+ * 0/1/2 align with global SUCCESS/VALIDATION/RUNTIME; 4/5 are audit-specific.
60
+ */
61
+ export const AUDIT_EXIT_CODES = {
62
+ clean: 0,
63
+ invalid: 1,
64
+ runtime: 2,
65
+ findings: 4,
66
+ partial: 5,
67
+ } as const satisfies Record<AuditExitKind, number>;
68
+
69
+ export type AuditFindingSeverity = "error" | "warning" | "info";
70
+
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+ // No-write port boundary
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Read-only store methods permitted for audit snapshot/rule evaluation.
77
+ * Category tasks may narrow further; mutating StorePort methods are forbidden.
78
+ */
79
+ export type AuditReadStorePort = Pick<
80
+ StorePort,
81
+ | "getActivationIndexSnapshot"
82
+ | "getCollections"
83
+ | "getStatus"
84
+ | "listDocuments"
85
+ | "listDocumentsPaginated"
86
+ | "getDocumentsByDocids"
87
+ | "getChunksBatch"
88
+ | "getContentBatch"
89
+ >;
90
+
91
+ /** Mutating StorePort method names that audits must never invoke. */
92
+ export const AUDIT_FORBIDDEN_STORE_METHODS = [
93
+ "upsertDocument",
94
+ "deactivateDocument",
95
+ "deleteDocument",
96
+ "upsertChunks",
97
+ "deleteChunks",
98
+ "recordError",
99
+ "upsertActivationReceipt",
100
+ "createRetrievalTrace",
101
+ "withTransaction",
102
+ "syncCollections",
103
+ "cleanup",
104
+ ] as const;
105
+
106
+ export type AuditForbiddenStoreMethod =
107
+ (typeof AUDIT_FORBIDDEN_STORE_METHODS)[number];
108
+
109
+ // ─────────────────────────────────────────────────────────────────────────────
110
+ // Report / finding shapes
111
+ // ─────────────────────────────────────────────────────────────────────────────
112
+
113
+ export interface AuditScope {
114
+ /** Categories requested for this run. Empty means all categories. */
115
+ categories: AuditCategory[];
116
+ collections: string[];
117
+ /** Optional path/prefix filters (normalized, sorted). */
118
+ paths: string[];
119
+ /** Optional tag filters (normalized, sorted). */
120
+ tags: string[];
121
+ indexName: string;
122
+ }
123
+
124
+ export interface AuditCapabilitySnapshot {
125
+ indexReadable: boolean;
126
+ sourcesReadable: boolean;
127
+ linksGraphAvailable: boolean;
128
+ provenanceSchemaAvailable: boolean;
129
+ offline: true;
130
+ llmDisabled: true;
131
+ }
132
+
133
+ export interface AuditFingerprints {
134
+ /** Canonical config / rule-input fingerprint. */
135
+ config: string;
136
+ /** Source tree revision evidence fingerprint. */
137
+ source: string;
138
+ /** Index revision evidence fingerprint. */
139
+ index: string;
140
+ /** Active rule-set identity fingerprint. */
141
+ rules: string;
142
+ }
143
+
144
+ export interface AuditEvidence {
145
+ kind: string;
146
+ summary: string;
147
+ uri?: string;
148
+ path?: string;
149
+ detail?: string;
150
+ }
151
+
152
+ export interface AuditFinding {
153
+ /** Stable SHA-256 identity; survives traversal order. */
154
+ id: string;
155
+ ruleId: string;
156
+ category: AuditCategory;
157
+ severity: AuditFindingSeverity;
158
+ /** Normalized subject (typically gno:// URI or collection-relative path). */
159
+ subject: string;
160
+ /** Normalized location within the subject, or null. */
161
+ location: string | null;
162
+ message: string;
163
+ evidence: AuditEvidence[];
164
+ guidance: string[];
165
+ evidenceFingerprint: string;
166
+ }
167
+
168
+ export interface AuditRuleResult {
169
+ ruleId: string;
170
+ category: AuditCategory;
171
+ status: AuditRuleStatus;
172
+ message: string;
173
+ findings: AuditFinding[];
174
+ /** Exact findings before the bounded per-rule payload. */
175
+ findingCount: number;
176
+ examinedCount: number;
177
+ durationMs: number;
178
+ skipReason: string | null;
179
+ }
180
+
181
+ export interface AuditCounts {
182
+ rules: {
183
+ pass: number;
184
+ fail: number;
185
+ skip: number;
186
+ unavailable: number;
187
+ inconclusive: number;
188
+ total: number;
189
+ };
190
+ findings: {
191
+ /** Exact total before return-cap truncation. */
192
+ total: number;
193
+ /** Findings included in the report payload. */
194
+ returned: number;
195
+ truncated: boolean;
196
+ };
197
+ examined: {
198
+ documents: number;
199
+ };
200
+ }
201
+
202
+ export interface AuditTruncation {
203
+ findingsTruncated: boolean;
204
+ maxFindings: number;
205
+ }
206
+
207
+ export interface AuditTiming {
208
+ snapshotMs: number;
209
+ rulesMs: number;
210
+ totalMs: number;
211
+ }
212
+
213
+ export interface AuditVersions {
214
+ gno: string;
215
+ schema: typeof AUDIT_SCHEMA_VERSION;
216
+ ruleSet: typeof AUDIT_RULE_SET_VERSION;
217
+ }
218
+
219
+ export interface AuditReport {
220
+ schemaVersion: typeof AUDIT_SCHEMA_VERSION;
221
+ ruleSetVersion: typeof AUDIT_RULE_SET_VERSION;
222
+ status: AuditReportStatus;
223
+ scope: AuditScope;
224
+ capabilities: AuditCapabilitySnapshot;
225
+ fingerprints: AuditFingerprints;
226
+ versions: AuditVersions;
227
+ startedAt: string;
228
+ completedAt: string;
229
+ durationMs: number;
230
+ rules: AuditRuleResult[];
231
+ findings: AuditFinding[];
232
+ counts: AuditCounts;
233
+ truncation: AuditTruncation;
234
+ timing: AuditTiming;
235
+ }
236
+
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+ // Draft / evaluator contracts (category rules plug in later)
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+
241
+ export interface AuditFindingDraft {
242
+ subject: string;
243
+ location?: string | null;
244
+ severity: AuditFindingSeverity;
245
+ message: string;
246
+ evidence: AuditEvidence[];
247
+ guidance?: string[];
248
+ }
249
+
250
+ export interface AuditRuleContribution {
251
+ ruleId: string;
252
+ category: AuditCategory;
253
+ status: AuditRuleStatus;
254
+ message: string;
255
+ findings?: AuditFindingDraft[];
256
+ /** Exact findings before any evaluator-side payload cap. */
257
+ findingCount?: number;
258
+ examinedCount?: number;
259
+ durationMs?: number;
260
+ skipReason?: string | null;
261
+ }
262
+
263
+ export interface AuditRuleContext {
264
+ scope: AuditScope;
265
+ capabilities: AuditCapabilitySnapshot;
266
+ fingerprints: AuditFingerprints;
267
+ attempt: number;
268
+ }
269
+
270
+ export type AuditRuleEvaluator = (
271
+ ctx: AuditRuleContext
272
+ ) =>
273
+ | AuditRuleContribution
274
+ | AuditRuleContribution[]
275
+ | Promise<AuditRuleContribution | AuditRuleContribution[]>;
276
+
277
+ export interface AuditFingerprintCapture {
278
+ (): AuditFingerprints | Promise<AuditFingerprints>;
279
+ }
280
+
281
+ export interface AuditRunInput {
282
+ scope: AuditScope;
283
+ capabilities: AuditCapabilitySnapshot;
284
+ captureFingerprints: AuditFingerprintCapture;
285
+ rules: readonly AuditRuleEvaluator[];
286
+ maxFindings?: number;
287
+ maxAttempts?: number;
288
+ gnoVersion?: string;
289
+ clock?: () => Date;
290
+ monotonicNow?: () => number;
291
+ signal?: AbortSignal;
292
+ }
293
+
294
+ export type AuditRunResult =
295
+ | { ok: true; report: AuditReport; exit: AuditExitKind }
296
+ | { ok: false; exit: "invalid"; error: string };
@@ -0,0 +1,233 @@
1
+ /** Observable source/index freshness audit rules. */
2
+
3
+ import type { AuditFindingDraft, AuditRuleContribution } from "./audit";
4
+
5
+ import { compareAuditFindingDrafts } from "./audit";
6
+
7
+ export const FRESHNESS_AUDIT_MAX_FINDINGS_PER_RULE = 1000;
8
+
9
+ export interface AuditFreshnessDocument {
10
+ uri: string;
11
+ relPath: string;
12
+ contentType: string | null;
13
+ indexedSourceHash: string;
14
+ indexedSourceMtime: string;
15
+ indexedAt: string | null;
16
+ lastErrorCode: string | null;
17
+ source: {
18
+ state: "readable" | "missing" | "unreadable";
19
+ hash: string | null;
20
+ mtime: string | null;
21
+ /** Logical records use their physical container only for readability. */
22
+ byteComparable?: boolean;
23
+ changedDuringRead?: boolean;
24
+ };
25
+ }
26
+
27
+ export interface AuditAgePolicy {
28
+ maxAgeDays: number;
29
+ contentTypes?: readonly string[];
30
+ }
31
+
32
+ export interface AuditFreshnessOptions {
33
+ now: Date;
34
+ agePolicy?: AuditAgePolicy;
35
+ truncated?: boolean;
36
+ }
37
+
38
+ const finding = (input: {
39
+ document: AuditFreshnessDocument;
40
+ kind: string;
41
+ message: string;
42
+ detail: Record<string, unknown>;
43
+ severity?: "error" | "warning" | "info";
44
+ guidance: string;
45
+ }): AuditFindingDraft => ({
46
+ subject: input.document.uri,
47
+ location: null,
48
+ severity: input.severity ?? "warning",
49
+ message: input.message,
50
+ evidence: [
51
+ {
52
+ kind: input.kind,
53
+ summary: input.message,
54
+ uri: input.document.uri,
55
+ path: input.document.relPath,
56
+ detail: JSON.stringify(input.detail),
57
+ },
58
+ ],
59
+ guidance: [input.guidance],
60
+ });
61
+
62
+ const rule = (input: {
63
+ ruleId: string;
64
+ findings: AuditFindingDraft[];
65
+ examinedCount: number;
66
+ unavailable?: boolean;
67
+ inconclusive?: boolean;
68
+ skipped?: boolean;
69
+ message: string;
70
+ reason?: string;
71
+ }): AuditRuleContribution => ({
72
+ ruleId: input.ruleId,
73
+ category: "freshness",
74
+ status: input.inconclusive
75
+ ? "inconclusive"
76
+ : input.unavailable
77
+ ? "unavailable"
78
+ : input.skipped
79
+ ? "skip"
80
+ : input.findings.length > 0
81
+ ? "fail"
82
+ : "pass",
83
+ message: input.message,
84
+ findings: [...input.findings]
85
+ .sort(compareAuditFindingDrafts)
86
+ .slice(0, FRESHNESS_AUDIT_MAX_FINDINGS_PER_RULE),
87
+ findingCount: input.findings.length,
88
+ examinedCount: input.examinedCount,
89
+ skipReason: input.reason ?? null,
90
+ });
91
+
92
+ /** Age findings are policy signals only and do not claim factual incorrectness. */
93
+ export const evaluateFreshnessAudit = (
94
+ documents: readonly AuditFreshnessDocument[],
95
+ options: AuditFreshnessOptions
96
+ ): AuditRuleContribution[] => {
97
+ const unavailable: AuditFindingDraft[] = [];
98
+ const drift: AuditFindingDraft[] = [];
99
+ const staleRevision: AuditFindingDraft[] = [];
100
+ const ageSignals: AuditFindingDraft[] = [];
101
+ let changedDuringRead = false;
102
+ const ageTypes = new Set(options.agePolicy?.contentTypes ?? []);
103
+ for (const document of documents) {
104
+ if (document.source.changedDuringRead) changedDuringRead = true;
105
+ if (document.source.state !== "readable") {
106
+ unavailable.push(
107
+ finding({
108
+ document,
109
+ kind: `source-${document.source.state}`,
110
+ message: `Source evidence is ${document.source.state}`,
111
+ detail: { state: document.source.state },
112
+ guidance: "Restore read access and re-run the audit",
113
+ })
114
+ );
115
+ continue;
116
+ }
117
+ if (
118
+ document.source.byteComparable !== false &&
119
+ document.source.hash !== null &&
120
+ document.source.hash !== document.indexedSourceHash
121
+ ) {
122
+ drift.push(
123
+ finding({
124
+ document,
125
+ kind: "source-index-hash-drift",
126
+ message: "Source bytes differ from the indexed revision",
127
+ detail: {
128
+ indexedHash: document.indexedSourceHash,
129
+ sourceHash: document.source.hash,
130
+ },
131
+ guidance: "Run gno index and inspect conversion errors",
132
+ })
133
+ );
134
+ }
135
+ if (!document.indexedAt || document.lastErrorCode) {
136
+ staleRevision.push(
137
+ finding({
138
+ document,
139
+ kind: "stale-indexed-revision",
140
+ message: document.lastErrorCode
141
+ ? "The latest indexing attempt recorded an error"
142
+ : "The document has no indexed-at revision evidence",
143
+ detail: {
144
+ indexedAt: document.indexedAt,
145
+ lastErrorCode: document.lastErrorCode,
146
+ },
147
+ guidance: "Run gno index and resolve the reported ingest error",
148
+ })
149
+ );
150
+ }
151
+ if (
152
+ options.agePolicy &&
153
+ (ageTypes.size === 0 ||
154
+ (document.contentType !== null && ageTypes.has(document.contentType)))
155
+ ) {
156
+ const timestamp = Date.parse(document.indexedSourceMtime);
157
+ const ageDays = (options.now.getTime() - timestamp) / 86_400_000;
158
+ if (Number.isFinite(ageDays) && ageDays > options.agePolicy.maxAgeDays) {
159
+ ageSignals.push(
160
+ finding({
161
+ document,
162
+ kind: "configured-age-signal",
163
+ message: `Source age exceeds the configured ${options.agePolicy.maxAgeDays}-day review signal`,
164
+ detail: {
165
+ ageDays: Math.floor(ageDays),
166
+ maxAgeDays: options.agePolicy.maxAgeDays,
167
+ },
168
+ severity: "info",
169
+ guidance:
170
+ "Review if useful; age is not evidence that the content is false",
171
+ })
172
+ );
173
+ }
174
+ }
175
+ }
176
+ const truncated = options.truncated === true;
177
+ const inconclusive = truncated || changedDuringRead;
178
+ const byteComparableCount = documents.filter(
179
+ ({ source }) => source.byteComparable !== false
180
+ ).length;
181
+ const excludedLogicalRecords = documents.length - byteComparableCount;
182
+ const reason = changedDuringRead
183
+ ? "source_changed_during_read"
184
+ : truncated
185
+ ? "snapshot_truncated"
186
+ : undefined;
187
+ return [
188
+ rule({
189
+ ruleId: "freshness.source-readable",
190
+ findings: unavailable,
191
+ examinedCount: documents.length,
192
+ unavailable: unavailable.length > 0 && !inconclusive,
193
+ inconclusive,
194
+ message: inconclusive
195
+ ? "Source readability evidence changed or was truncated"
196
+ : `${unavailable.length} sources are missing or unreadable`,
197
+ reason,
198
+ }),
199
+ rule({
200
+ ruleId: "freshness.source-index-drift",
201
+ findings: drift,
202
+ examinedCount: byteComparableCount,
203
+ inconclusive,
204
+ message: inconclusive
205
+ ? "Source/index drift evidence changed or was truncated"
206
+ : `${drift.length} source revisions differ from the index${excludedLogicalRecords > 0 ? `; ${excludedLogicalRecords} logical records excluded from byte comparison` : ""}`,
207
+ reason,
208
+ }),
209
+ rule({
210
+ ruleId: "freshness.index-revision",
211
+ findings: staleRevision,
212
+ examinedCount: documents.length,
213
+ inconclusive,
214
+ message: inconclusive
215
+ ? "Indexed revision evidence changed or was truncated"
216
+ : `${staleRevision.length} indexed revisions need attention`,
217
+ reason,
218
+ }),
219
+ rule({
220
+ ruleId: "freshness.configured-age-signal",
221
+ findings: ageSignals,
222
+ examinedCount: documents.length,
223
+ inconclusive,
224
+ skipped: !options.agePolicy,
225
+ message: !options.agePolicy
226
+ ? "No age review policy was configured"
227
+ : `${ageSignals.length} configured age review signals`,
228
+ reason:
229
+ reason ??
230
+ (!options.agePolicy ? "age_policy_not_configured" : undefined),
231
+ }),
232
+ ];
233
+ };