@gmickel/gno 1.33.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.
- package/README.md +7 -2
- package/assets/skill/SKILL.md +19 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.33.0.zip → gno-browser-clipper-v1.34.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +44 -0
- package/spec/mcp.md +21 -0
- package/spec/output-schemas/audit-report.schema.json +284 -0
- package/src/cli/commands/audit.ts +231 -0
- package/src/cli/errors.ts +9 -2
- package/src/cli/program.ts +112 -0
- package/src/core/audit-contract.ts +296 -0
- package/src/core/audit-freshness.ts +233 -0
- package/src/core/audit-links.ts +222 -0
- package/src/core/audit-provenance.ts +154 -0
- package/src/core/audit-report.ts +318 -0
- package/src/core/audit-workspace.ts +678 -0
- package/src/core/audit.ts +569 -0
- package/src/core/capture.ts +196 -3
- package/src/core/document-capabilities.ts +9 -8
- package/src/core/record-metadata.ts +33 -0
- package/src/mcp/http-egress.ts +8 -0
- package/src/mcp/tools/audit.ts +97 -0
- package/src/mcp/tools/index.ts +13 -0
- package/src/store/sqlite/adapter.ts +82 -0
- package/src/store/sqlite/graph-link-bulk-resolver.ts +191 -0
- package/src/store/sqlite/graph-link-resolver.ts +241 -2
- package/browser-extension/artifacts/gno-browser-clipper-v1.33.0.zip.sha256 +0 -1
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ gno daemon --detach # headless indexing + resident MCP gateway
|
|
|
117
117
|
|
|
118
118
|
<!-- public-truth:current-version -->
|
|
119
119
|
|
|
120
|
-
> Current release: **v1.
|
|
120
|
+
> Current release: **v1.34.0** — see [CHANGELOG.md](./CHANGELOG.md)
|
|
121
121
|
|
|
122
122
|
<!-- /public-truth -->
|
|
123
123
|
|
|
@@ -174,6 +174,10 @@ gno daemon --detach # headless indexing + resident MCP gateway
|
|
|
174
174
|
`local_only`, `lan`, or `remote` boundary that follows mixed and derived
|
|
175
175
|
evidence through resident serving, inference, publishing, exports, Capsules,
|
|
176
176
|
and traces. Authentication never overrides policy.
|
|
177
|
+
- **Read-only integrity audits**: `gno audit` and MCP `gno_audit` inspect local
|
|
178
|
+
links, declared provenance, and source/index freshness offline. Stable,
|
|
179
|
+
bounded findings distinguish complete, partial, unavailable, and changing
|
|
180
|
+
evidence without repairing or persisting anything.
|
|
177
181
|
- **Schema-lite content types**: optional `contentTypes` rules map configured
|
|
178
182
|
frontmatter `type` values or path prefixes to canonical `contentType` metadata
|
|
179
183
|
in JSON search/query results and can apply one bounded, explainable
|
|
@@ -945,7 +949,8 @@ graph TD
|
|
|
945
949
|
| **Local LLM** | AI answers via llama.cpp, no API keys |
|
|
946
950
|
| **Remote Inference** | Optional HTTP endpoints for embedding, reranking, expansion, and generation |
|
|
947
951
|
| **Privacy First** | Fail-closed per-collection egress policy; no telemetry; explicit network use |
|
|
948
|
-
| **MCP Server** | 10 automatic client targets;
|
|
952
|
+
| **MCP Server** | 10 automatic client targets; 32 read-only tools, 50 with writes enabled |
|
|
953
|
+
| **Integrity Audits** | Offline link, declared-provenance, and freshness reports with stable IDs |
|
|
949
954
|
| **Knowledge Delta** | Bounded metadata history, structural diffs, and dependency impact paths |
|
|
950
955
|
| **Context Capsules** | Deterministic evidence bundles plus saved-file freshness reverification |
|
|
951
956
|
| **Verified Ask** | Claim-by-claim evidence checks with explicit abstention |
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -313,6 +313,23 @@ at the remote service. For gno.sh, supported private links can be revoked or
|
|
|
313
313
|
expired in Studio; public-space deletion is not yet self-service, so request
|
|
314
314
|
takedown. Encrypted gno.sh shares are client-encrypted and never server-decrypted.
|
|
315
315
|
|
|
316
|
+
## Read-Only Knowledge Integrity Audits
|
|
317
|
+
|
|
318
|
+
Use `gno audit [links|provenance|freshness|all] --json` or read-only MCP
|
|
319
|
+
`gno_audit` only when the user asks what needs attention in a workspace. These
|
|
320
|
+
offline audits inspect parsed local links, explicitly declared capture/logical-
|
|
321
|
+
record provenance, and observable source/index freshness. They never repair,
|
|
322
|
+
rewrite, persist findings, judge factual truth, or replace retrieval.
|
|
323
|
+
|
|
324
|
+
Treat exit `4` as a complete report with findings. Exit `5` or report status
|
|
325
|
+
`partial`/`changed_during_audit` means evidence is unavailable, inconclusive,
|
|
326
|
+
cancelled, truncated, or repeatedly changed—never healthy. Preserve stable
|
|
327
|
+
finding IDs and exact totals when summarizing bounded results. Age is only a
|
|
328
|
+
review signal when `maxAgeDays`/`--max-age-days` is explicitly supplied. Apply
|
|
329
|
+
collection/path/tag scope and orphan roots/ignore prefixes only from the user's
|
|
330
|
+
request. `gno egress-audit` is separate: it manages content-free transport-
|
|
331
|
+
policy receipts.
|
|
332
|
+
|
|
316
333
|
## MCP Retrieval Strategy
|
|
317
334
|
|
|
318
335
|
For a long-lived client that supports Streamable HTTP, start one resident owner
|
|
@@ -377,6 +394,8 @@ Use narrower tools when the request tells you to:
|
|
|
377
394
|
- `gno_search`: exact phrase, filename, identifier, stack trace, error text
|
|
378
395
|
- `gno_vsearch`: conceptual similarity when exact wording differs
|
|
379
396
|
- `gno_status`: stale results, missing embeddings, vector unavailable
|
|
397
|
+
- `gno_audit`: explicit offline workspace-integrity review; report findings and
|
|
398
|
+
partial evidence, never mutate or imply repairs
|
|
380
399
|
- `gno_graph`: graph report/stats, hubs, isolates, unresolved links, edge confidence/audit, communities, unfamiliar corpus overview
|
|
381
400
|
- `gno_graph_query`: bounded typed-edge traversal from a known document
|
|
382
401
|
- `gno_graph_neighbors`: relationship/corpus-navigation questions around a known document
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0acc125d848721bdced540fe49412ef8c83df5d4ff3ba086076c46ed9d441998 gno-browser-clipper-v1.34.0.zip
|
package/package.json
CHANGED
package/spec/cli.md
CHANGED
|
@@ -692,6 +692,50 @@ projection.
|
|
|
692
692
|
content-free decision, lineage, partial disclosure, audit metadata, and
|
|
693
693
|
remediation contract.
|
|
694
694
|
|
|
695
|
+
### gno audit
|
|
696
|
+
|
|
697
|
+
Read-only, offline knowledge-integrity audits. This command is distinct from
|
|
698
|
+
`gno egress-audit`: it examines workspace links, declared provenance
|
|
699
|
+
requirements, and source/index freshness without persisting findings or
|
|
700
|
+
modifying notes, configuration, index rows, graph edges, or daemon state.
|
|
701
|
+
|
|
702
|
+
```bash
|
|
703
|
+
gno audit [links|provenance|freshness|all] [--collection <name>...] \
|
|
704
|
+
[--path <prefix>...] [--tag <tag>...] [--max-findings <n>] \
|
|
705
|
+
[--max-age-days <days>] [--orphan-root <uri>...] \
|
|
706
|
+
[--orphan-ignore-prefix <prefix>...] [--no-progress] \
|
|
707
|
+
[--json] [--output <path>]
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
The default category is `all`; `--max-findings` defaults to 100 and is bounded
|
|
711
|
+
to 1–1000. Truncation limits returned findings but preserves exact totals.
|
|
712
|
+
`--output` writes only the requested report artifact with local file
|
|
713
|
+
permissions. Human output renders the same report represented by
|
|
714
|
+
`audit-report.schema.json`.
|
|
715
|
+
When attached to a terminal, human mode reports snapshot/rule progress on
|
|
716
|
+
stderr; `--no-progress`, `--quiet`, and JSON mode suppress it. `Ctrl-C` returns
|
|
717
|
+
the bounded report gathered so far with partial status and exit 5. Freshness
|
|
718
|
+
age is reported only when `--max-age-days` supplies an explicit policy.
|
|
719
|
+
|
|
720
|
+
Rule statuses are `pass`, `fail`, `skip`, `unavailable`, and `inconclusive`.
|
|
721
|
+
Report statuses are `complete`, `partial`, `changed_during_audit`, and `failed`.
|
|
722
|
+
A skipped, unavailable, inconclusive, or changing check is never rendered as
|
|
723
|
+
healthy.
|
|
724
|
+
|
|
725
|
+
**Exit Codes:**
|
|
726
|
+
|
|
727
|
+
- `0` — complete and clean
|
|
728
|
+
- `1` — invalid input
|
|
729
|
+
- `2` — runtime failure
|
|
730
|
+
- `4` — complete report with findings
|
|
731
|
+
- `5` — partial, inconclusive, unavailable, or changed-during-audit evidence
|
|
732
|
+
|
|
733
|
+
The JSON contract is versioned as `gno://schemas/audit-report@1.0`. Finding IDs
|
|
734
|
+
are stable SHA-256 identities derived from rule, normalized subject/location,
|
|
735
|
+
and load-bearing evidence. Wall-clock timing and traversal order do not affect
|
|
736
|
+
identity. A source/index fingerprint change causes one bounded retry and then
|
|
737
|
+
`changed_during_audit`; it can never produce a clean exit.
|
|
738
|
+
|
|
695
739
|
### gno egress-audit
|
|
696
740
|
|
|
697
741
|
Manage local content-free policy decision receipts:
|
package/spec/mcp.md
CHANGED
|
@@ -152,6 +152,27 @@ Collection names are case-insensitive on input and normalized to lowercase in re
|
|
|
152
152
|
|
|
153
153
|
## Tools
|
|
154
154
|
|
|
155
|
+
### Knowledge integrity audit contract
|
|
156
|
+
|
|
157
|
+
`gno_audit` is a read-only, offline tool that returns the same
|
|
158
|
+
`gno://schemas/audit-report@1.0` report as `gno audit`. Its closed input accepts
|
|
159
|
+
`category` (`links`, `provenance`, `freshness`, or `all`), collection/path/tag
|
|
160
|
+
filters, and `maxFindings` (1–1000). `maxAgeDays`, `orphanRoots`, and
|
|
161
|
+
`orphanIgnorePrefixes` are optional explicit policy inputs. Request
|
|
162
|
+
cancellation returns a partial report rather than a false clean result. The
|
|
163
|
+
tool is annotated with
|
|
164
|
+
`readOnlyHint: true`; the implementation must additionally prove that it never
|
|
165
|
+
modifies source files, config, index rows, graph edges, daemon state, or a
|
|
166
|
+
persisted audit baseline.
|
|
167
|
+
|
|
168
|
+
Rules report `pass`, `fail`, `skip`, `unavailable`, or `inconclusive`; reports
|
|
169
|
+
are `complete`, `partial`, `changed_during_audit`, or `failed`. Unavailable or
|
|
170
|
+
changing evidence never appears healthy. Stable finding IDs are derived from
|
|
171
|
+
rule, normalized subject/location, and evidence fingerprint. Responses are
|
|
172
|
+
canonically ordered and bounded while retaining exact pre-truncation totals.
|
|
173
|
+
This knowledge audit is distinct from the content-free `gno_egress_audit_*`
|
|
174
|
+
receipt-management tools.
|
|
175
|
+
|
|
155
176
|
### Agent Retrieval Playbook
|
|
156
177
|
|
|
157
178
|
- Prefer `gno_context` when the agent needs a complete, bounded evidence handoff
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "gno://schemas/audit-report@1.0",
|
|
4
|
+
"title": "GNO Knowledge Integrity Audit Report",
|
|
5
|
+
"description": "Deterministic read-only audit report for links, provenance, and freshness. Not egress-audit receipts.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"schemaVersion",
|
|
10
|
+
"ruleSetVersion",
|
|
11
|
+
"status",
|
|
12
|
+
"scope",
|
|
13
|
+
"capabilities",
|
|
14
|
+
"fingerprints",
|
|
15
|
+
"versions",
|
|
16
|
+
"startedAt",
|
|
17
|
+
"completedAt",
|
|
18
|
+
"durationMs",
|
|
19
|
+
"rules",
|
|
20
|
+
"findings",
|
|
21
|
+
"counts",
|
|
22
|
+
"truncation",
|
|
23
|
+
"timing"
|
|
24
|
+
],
|
|
25
|
+
"properties": {
|
|
26
|
+
"schemaVersion": { "const": "1.0" },
|
|
27
|
+
"ruleSetVersion": { "const": "1.0" },
|
|
28
|
+
"status": {
|
|
29
|
+
"enum": ["complete", "partial", "changed_during_audit", "failed"]
|
|
30
|
+
},
|
|
31
|
+
"scope": { "$ref": "#/definitions/scope" },
|
|
32
|
+
"capabilities": { "$ref": "#/definitions/capabilities" },
|
|
33
|
+
"fingerprints": { "$ref": "#/definitions/fingerprints" },
|
|
34
|
+
"versions": { "$ref": "#/definitions/versions" },
|
|
35
|
+
"startedAt": { "type": "string", "format": "date-time" },
|
|
36
|
+
"completedAt": { "type": "string", "format": "date-time" },
|
|
37
|
+
"durationMs": { "type": "integer", "minimum": 0 },
|
|
38
|
+
"rules": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"maxItems": 256,
|
|
41
|
+
"items": { "$ref": "#/definitions/ruleResult" }
|
|
42
|
+
},
|
|
43
|
+
"findings": {
|
|
44
|
+
"type": "array",
|
|
45
|
+
"maxItems": 1000,
|
|
46
|
+
"items": { "$ref": "#/definitions/finding" }
|
|
47
|
+
},
|
|
48
|
+
"counts": { "$ref": "#/definitions/counts" },
|
|
49
|
+
"truncation": { "$ref": "#/definitions/truncation" },
|
|
50
|
+
"timing": { "$ref": "#/definitions/timing" }
|
|
51
|
+
},
|
|
52
|
+
"definitions": {
|
|
53
|
+
"sha256": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
56
|
+
},
|
|
57
|
+
"category": {
|
|
58
|
+
"enum": ["links", "provenance", "freshness"]
|
|
59
|
+
},
|
|
60
|
+
"ruleStatus": {
|
|
61
|
+
"enum": ["pass", "fail", "skip", "unavailable", "inconclusive"]
|
|
62
|
+
},
|
|
63
|
+
"severity": {
|
|
64
|
+
"enum": ["error", "warning", "info"]
|
|
65
|
+
},
|
|
66
|
+
"scope": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"additionalProperties": false,
|
|
69
|
+
"required": ["categories", "collections", "paths", "tags", "indexName"],
|
|
70
|
+
"properties": {
|
|
71
|
+
"categories": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"minItems": 1,
|
|
74
|
+
"maxItems": 3,
|
|
75
|
+
"items": { "$ref": "#/definitions/category" },
|
|
76
|
+
"uniqueItems": true
|
|
77
|
+
},
|
|
78
|
+
"collections": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"maxItems": 256,
|
|
81
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 256 }
|
|
82
|
+
},
|
|
83
|
+
"paths": {
|
|
84
|
+
"type": "array",
|
|
85
|
+
"maxItems": 256,
|
|
86
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 2048 }
|
|
87
|
+
},
|
|
88
|
+
"tags": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"maxItems": 256,
|
|
91
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 256 }
|
|
92
|
+
},
|
|
93
|
+
"indexName": { "type": "string", "minLength": 1, "maxLength": 64 }
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"capabilities": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"additionalProperties": false,
|
|
99
|
+
"required": [
|
|
100
|
+
"indexReadable",
|
|
101
|
+
"sourcesReadable",
|
|
102
|
+
"linksGraphAvailable",
|
|
103
|
+
"provenanceSchemaAvailable",
|
|
104
|
+
"offline",
|
|
105
|
+
"llmDisabled"
|
|
106
|
+
],
|
|
107
|
+
"properties": {
|
|
108
|
+
"indexReadable": { "type": "boolean" },
|
|
109
|
+
"sourcesReadable": { "type": "boolean" },
|
|
110
|
+
"linksGraphAvailable": { "type": "boolean" },
|
|
111
|
+
"provenanceSchemaAvailable": { "type": "boolean" },
|
|
112
|
+
"offline": { "const": true },
|
|
113
|
+
"llmDisabled": { "const": true }
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"fingerprints": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"additionalProperties": false,
|
|
119
|
+
"required": ["config", "source", "index", "rules"],
|
|
120
|
+
"properties": {
|
|
121
|
+
"config": { "type": "string", "minLength": 0, "maxLength": 128 },
|
|
122
|
+
"source": { "type": "string", "minLength": 0, "maxLength": 128 },
|
|
123
|
+
"index": { "type": "string", "minLength": 0, "maxLength": 128 },
|
|
124
|
+
"rules": { "type": "string", "minLength": 0, "maxLength": 128 }
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"versions": {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"additionalProperties": false,
|
|
130
|
+
"required": ["gno", "schema", "ruleSet"],
|
|
131
|
+
"properties": {
|
|
132
|
+
"gno": { "type": "string", "minLength": 1, "maxLength": 64 },
|
|
133
|
+
"schema": { "const": "1.0" },
|
|
134
|
+
"ruleSet": { "const": "1.0" }
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"evidence": {
|
|
138
|
+
"type": "object",
|
|
139
|
+
"additionalProperties": false,
|
|
140
|
+
"required": ["kind", "summary"],
|
|
141
|
+
"properties": {
|
|
142
|
+
"kind": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
143
|
+
"summary": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
144
|
+
"uri": { "type": "string", "minLength": 1, "maxLength": 2048 },
|
|
145
|
+
"path": { "type": "string", "minLength": 1, "maxLength": 2048 },
|
|
146
|
+
"detail": { "type": "string", "minLength": 1, "maxLength": 512 }
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"finding": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"additionalProperties": false,
|
|
152
|
+
"required": [
|
|
153
|
+
"id",
|
|
154
|
+
"ruleId",
|
|
155
|
+
"category",
|
|
156
|
+
"severity",
|
|
157
|
+
"subject",
|
|
158
|
+
"location",
|
|
159
|
+
"message",
|
|
160
|
+
"evidence",
|
|
161
|
+
"guidance",
|
|
162
|
+
"evidenceFingerprint"
|
|
163
|
+
],
|
|
164
|
+
"properties": {
|
|
165
|
+
"id": { "$ref": "#/definitions/sha256" },
|
|
166
|
+
"ruleId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
167
|
+
"category": { "$ref": "#/definitions/category" },
|
|
168
|
+
"severity": { "$ref": "#/definitions/severity" },
|
|
169
|
+
"subject": { "type": "string", "minLength": 1, "maxLength": 2048 },
|
|
170
|
+
"location": { "type": ["string", "null"], "maxLength": 2048 },
|
|
171
|
+
"message": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
172
|
+
"evidence": {
|
|
173
|
+
"type": "array",
|
|
174
|
+
"maxItems": 8,
|
|
175
|
+
"items": { "$ref": "#/definitions/evidence" }
|
|
176
|
+
},
|
|
177
|
+
"guidance": {
|
|
178
|
+
"type": "array",
|
|
179
|
+
"maxItems": 4,
|
|
180
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 256 }
|
|
181
|
+
},
|
|
182
|
+
"evidenceFingerprint": { "$ref": "#/definitions/sha256" }
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"ruleResult": {
|
|
186
|
+
"type": "object",
|
|
187
|
+
"additionalProperties": false,
|
|
188
|
+
"required": [
|
|
189
|
+
"ruleId",
|
|
190
|
+
"category",
|
|
191
|
+
"status",
|
|
192
|
+
"message",
|
|
193
|
+
"findings",
|
|
194
|
+
"findingCount",
|
|
195
|
+
"examinedCount",
|
|
196
|
+
"durationMs",
|
|
197
|
+
"skipReason"
|
|
198
|
+
],
|
|
199
|
+
"properties": {
|
|
200
|
+
"ruleId": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
201
|
+
"category": { "$ref": "#/definitions/category" },
|
|
202
|
+
"status": { "$ref": "#/definitions/ruleStatus" },
|
|
203
|
+
"message": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
204
|
+
"findings": {
|
|
205
|
+
"type": "array",
|
|
206
|
+
"maxItems": 1000,
|
|
207
|
+
"items": { "$ref": "#/definitions/finding" }
|
|
208
|
+
},
|
|
209
|
+
"findingCount": { "type": "integer", "minimum": 0 },
|
|
210
|
+
"examinedCount": { "type": "integer", "minimum": 0 },
|
|
211
|
+
"durationMs": { "type": "integer", "minimum": 0 },
|
|
212
|
+
"skipReason": { "type": ["string", "null"], "maxLength": 512 }
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
"counts": {
|
|
216
|
+
"type": "object",
|
|
217
|
+
"additionalProperties": false,
|
|
218
|
+
"required": ["rules", "findings", "examined"],
|
|
219
|
+
"properties": {
|
|
220
|
+
"rules": {
|
|
221
|
+
"type": "object",
|
|
222
|
+
"additionalProperties": false,
|
|
223
|
+
"required": [
|
|
224
|
+
"pass",
|
|
225
|
+
"fail",
|
|
226
|
+
"skip",
|
|
227
|
+
"unavailable",
|
|
228
|
+
"inconclusive",
|
|
229
|
+
"total"
|
|
230
|
+
],
|
|
231
|
+
"properties": {
|
|
232
|
+
"pass": { "type": "integer", "minimum": 0 },
|
|
233
|
+
"fail": { "type": "integer", "minimum": 0 },
|
|
234
|
+
"skip": { "type": "integer", "minimum": 0 },
|
|
235
|
+
"unavailable": { "type": "integer", "minimum": 0 },
|
|
236
|
+
"inconclusive": { "type": "integer", "minimum": 0 },
|
|
237
|
+
"total": { "type": "integer", "minimum": 0 }
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"findings": {
|
|
241
|
+
"type": "object",
|
|
242
|
+
"additionalProperties": false,
|
|
243
|
+
"required": ["total", "returned", "truncated"],
|
|
244
|
+
"properties": {
|
|
245
|
+
"total": { "type": "integer", "minimum": 0 },
|
|
246
|
+
"returned": { "type": "integer", "minimum": 0, "maximum": 1000 },
|
|
247
|
+
"truncated": { "type": "boolean" }
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"examined": {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"additionalProperties": false,
|
|
253
|
+
"required": ["documents"],
|
|
254
|
+
"properties": {
|
|
255
|
+
"documents": { "type": "integer", "minimum": 0 }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
"truncation": {
|
|
261
|
+
"type": "object",
|
|
262
|
+
"additionalProperties": false,
|
|
263
|
+
"required": ["findingsTruncated", "maxFindings"],
|
|
264
|
+
"properties": {
|
|
265
|
+
"findingsTruncated": { "type": "boolean" },
|
|
266
|
+
"maxFindings": {
|
|
267
|
+
"type": "integer",
|
|
268
|
+
"minimum": 1,
|
|
269
|
+
"maximum": 1000
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
"timing": {
|
|
274
|
+
"type": "object",
|
|
275
|
+
"additionalProperties": false,
|
|
276
|
+
"required": ["snapshotMs", "rulesMs", "totalMs"],
|
|
277
|
+
"properties": {
|
|
278
|
+
"snapshotMs": { "type": "integer", "minimum": 0 },
|
|
279
|
+
"rulesMs": { "type": "integer", "minimum": 0 },
|
|
280
|
+
"totalMs": { "type": "integer", "minimum": 0 }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/** Read-only knowledge-integrity audit CLI adapter. */
|
|
2
|
+
|
|
3
|
+
// node:fs/promises provides atomic filesystem structure operations with no Bun equivalent.
|
|
4
|
+
import { chmod, mkdtemp, rename, rmdir, unlink } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AuditCategory,
|
|
9
|
+
AuditReport,
|
|
10
|
+
AuditRunResult,
|
|
11
|
+
} from "../../core/audit";
|
|
12
|
+
import type { WorkspaceAuditProgress } from "../../core/audit-workspace";
|
|
13
|
+
|
|
14
|
+
import { getIndexDbPath } from "../../app/constants";
|
|
15
|
+
import { loadConfig } from "../../config";
|
|
16
|
+
import {
|
|
17
|
+
auditExitCode,
|
|
18
|
+
AUDIT_CATEGORIES,
|
|
19
|
+
serializeAuditReportCanonical,
|
|
20
|
+
} from "../../core/audit";
|
|
21
|
+
import { runWorkspaceAudit } from "../../core/audit-workspace";
|
|
22
|
+
import { normalizeTag, validateTag } from "../../core/tags";
|
|
23
|
+
import { normalizeCollectionName } from "../../core/validation";
|
|
24
|
+
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
25
|
+
|
|
26
|
+
export interface AuditCommandOptions {
|
|
27
|
+
category?: string;
|
|
28
|
+
configPath?: string;
|
|
29
|
+
indexName?: string;
|
|
30
|
+
collections?: string[];
|
|
31
|
+
paths?: string[];
|
|
32
|
+
tags?: string[];
|
|
33
|
+
maxFindings?: number;
|
|
34
|
+
maxAgeDays?: number;
|
|
35
|
+
orphanRoots?: string[];
|
|
36
|
+
orphanIgnorePrefixes?: string[];
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
onProgress?: (progress: WorkspaceAuditProgress) => void | Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type AuditCommandResult =
|
|
42
|
+
| { success: true; report: AuditReport; exitCode: number }
|
|
43
|
+
| { success: false; error: string; invalid: boolean };
|
|
44
|
+
|
|
45
|
+
const resolveCategories = (
|
|
46
|
+
value: string | undefined
|
|
47
|
+
): AuditCategory[] | null => {
|
|
48
|
+
const normalized = (value ?? "all").trim().toLowerCase();
|
|
49
|
+
if (normalized === "all") return [...AUDIT_CATEGORIES];
|
|
50
|
+
return (AUDIT_CATEGORIES as readonly string[]).includes(normalized)
|
|
51
|
+
? [normalized as AuditCategory]
|
|
52
|
+
: null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const invalidPositiveInteger = (value: number | undefined): boolean =>
|
|
56
|
+
value !== undefined && (!Number.isSafeInteger(value) || value < 1);
|
|
57
|
+
|
|
58
|
+
const AUDIT_SCOPE_FILTER_MAX_ITEMS = 256;
|
|
59
|
+
|
|
60
|
+
const oversizedScopeFilter = (
|
|
61
|
+
options: AuditCommandOptions
|
|
62
|
+
): "collections" | "paths" | "tags" | null => {
|
|
63
|
+
for (const name of ["collections", "paths", "tags"] as const) {
|
|
64
|
+
if ((options[name]?.length ?? 0) > AUDIT_SCOPE_FILTER_MAX_ITEMS) {
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const audit = async (
|
|
72
|
+
options: AuditCommandOptions
|
|
73
|
+
): Promise<AuditCommandResult> => {
|
|
74
|
+
const categories = resolveCategories(options.category);
|
|
75
|
+
if (!categories) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
invalid: true,
|
|
79
|
+
error: "category must be links, provenance, freshness, or all",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (invalidPositiveInteger(options.maxFindings)) {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
invalid: true,
|
|
86
|
+
error: "maxFindings must be a positive integer",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (invalidPositiveInteger(options.maxAgeDays)) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
invalid: true,
|
|
93
|
+
error: "maxAgeDays must be a positive integer",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const oversizedFilter = oversizedScopeFilter(options);
|
|
97
|
+
if (oversizedFilter) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
invalid: true,
|
|
101
|
+
error: `${oversizedFilter} must contain at most ${AUDIT_SCOPE_FILTER_MAX_ITEMS} values`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const configResult = await loadConfig(options.configPath);
|
|
105
|
+
if (!configResult.ok) {
|
|
106
|
+
return { success: false, invalid: true, error: configResult.error.message };
|
|
107
|
+
}
|
|
108
|
+
const config = configResult.value;
|
|
109
|
+
const requestedCollections = (options.collections ?? []).map(
|
|
110
|
+
normalizeCollectionName
|
|
111
|
+
);
|
|
112
|
+
const requestedTags = (options.tags ?? []).map(normalizeTag);
|
|
113
|
+
const invalidTag = requestedTags.find((tag) => !validateTag(tag));
|
|
114
|
+
if (invalidTag) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
invalid: true,
|
|
118
|
+
error: `Invalid tag: "${invalidTag}"`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const knownCollections = new Set(
|
|
122
|
+
config.collections.map((collection) => collection.name)
|
|
123
|
+
);
|
|
124
|
+
const missingCollection = requestedCollections.find(
|
|
125
|
+
(collection) => !knownCollections.has(collection)
|
|
126
|
+
);
|
|
127
|
+
if (missingCollection) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
invalid: true,
|
|
131
|
+
error: `Collection not found: ${missingCollection}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const dbPath = getIndexDbPath(options.indexName);
|
|
135
|
+
if (!(await Bun.file(dbPath).exists())) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
invalid: false,
|
|
139
|
+
error: `Index database not found: ${dbPath}. Run gno index first.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const store = new SqliteAdapter();
|
|
143
|
+
const opened = store.openReadOnly(dbPath);
|
|
144
|
+
if (!opened.ok) {
|
|
145
|
+
return { success: false, invalid: false, error: opened.error.message };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const result: AuditRunResult = await runWorkspaceAudit({
|
|
149
|
+
store,
|
|
150
|
+
config,
|
|
151
|
+
collections: config.collections,
|
|
152
|
+
indexName: options.indexName ?? "default",
|
|
153
|
+
categories,
|
|
154
|
+
collectionFilters: requestedCollections,
|
|
155
|
+
pathFilters: options.paths,
|
|
156
|
+
tagFilters: requestedTags,
|
|
157
|
+
maxFindings: options.maxFindings,
|
|
158
|
+
agePolicy:
|
|
159
|
+
options.maxAgeDays === undefined
|
|
160
|
+
? undefined
|
|
161
|
+
: { maxAgeDays: options.maxAgeDays },
|
|
162
|
+
orphanRoots: options.orphanRoots,
|
|
163
|
+
orphanIgnorePrefixes: options.orphanIgnorePrefixes,
|
|
164
|
+
signal: options.signal,
|
|
165
|
+
onProgress: options.onProgress,
|
|
166
|
+
});
|
|
167
|
+
if (!result.ok) {
|
|
168
|
+
return { success: false, invalid: true, error: result.error };
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
success: true,
|
|
172
|
+
report: result.report,
|
|
173
|
+
exitCode: auditExitCode(result.exit),
|
|
174
|
+
};
|
|
175
|
+
} finally {
|
|
176
|
+
await store.close();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const formatAuditReport = (
|
|
181
|
+
report: AuditReport,
|
|
182
|
+
options: { json?: boolean } = {}
|
|
183
|
+
): string => {
|
|
184
|
+
if (options.json) return serializeAuditReportCanonical(report);
|
|
185
|
+
const lines = [
|
|
186
|
+
`Audit: ${report.status}`,
|
|
187
|
+
`Categories: ${report.scope.categories.join(", ")}`,
|
|
188
|
+
`Rules: ${report.counts.rules.total} (${report.counts.rules.fail} failed, ${report.counts.rules.unavailable} unavailable, ${report.counts.rules.inconclusive} inconclusive)`,
|
|
189
|
+
`Findings: ${report.counts.findings.total}${report.counts.findings.truncated ? ` (${report.counts.findings.returned} shown)` : ""}`,
|
|
190
|
+
`Examined: ${report.counts.examined.documents} document/rule observations`,
|
|
191
|
+
`Duration: ${report.durationMs}ms`,
|
|
192
|
+
];
|
|
193
|
+
for (const finding of report.findings) {
|
|
194
|
+
const location = finding.location ? ` ${finding.location}` : "";
|
|
195
|
+
lines.push(
|
|
196
|
+
`- [${finding.severity}] ${finding.ruleId}: ${finding.subject}${location} — ${finding.message}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
for (const rule of report.rules) {
|
|
200
|
+
if (
|
|
201
|
+
rule.status === "skip" ||
|
|
202
|
+
rule.status === "unavailable" ||
|
|
203
|
+
rule.status === "inconclusive"
|
|
204
|
+
) {
|
|
205
|
+
lines.push(`- [${rule.status}] ${rule.ruleId}: ${rule.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return lines.join("\n");
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const writeAuditReport = async (
|
|
212
|
+
path: string,
|
|
213
|
+
report: AuditReport,
|
|
214
|
+
options: { json?: boolean } = {}
|
|
215
|
+
): Promise<void> => {
|
|
216
|
+
const temporaryDirectory = await mkdtemp(
|
|
217
|
+
join(dirname(path), `.${basename(path)}-`)
|
|
218
|
+
);
|
|
219
|
+
const temporaryPath = join(temporaryDirectory, "report");
|
|
220
|
+
try {
|
|
221
|
+
await Bun.write(temporaryPath, `${formatAuditReport(report, options)}\n`, {
|
|
222
|
+
createPath: false,
|
|
223
|
+
mode: 0o600,
|
|
224
|
+
});
|
|
225
|
+
await chmod(temporaryPath, 0o600);
|
|
226
|
+
await rename(temporaryPath, path);
|
|
227
|
+
} finally {
|
|
228
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
229
|
+
await rmdir(temporaryDirectory).catch(() => undefined);
|
|
230
|
+
}
|
|
231
|
+
};
|