@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.
- package/README.md +26 -30
- package/assets/skill/SKILL.md +8 -1
- package/assets/skill/cli-reference.md +8 -1
- package/assets/skill/examples.md +2 -1
- package/assets/skill/mcp-reference.md +3 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.0.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +48 -6
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +18 -3
- package/spec/output-schemas/audit-report.schema.json +18 -4
- package/spec/output-schemas/backlinks.schema.json +4 -0
- package/spec/output-schemas/collection-list.schema.json +15 -2
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/status.schema.json +27 -16
- package/src/cli/commands/ask.ts +31 -12
- package/src/cli/commands/audit.ts +23 -4
- package/src/cli/commands/collection/list.ts +39 -5
- package/src/cli/commands/embed.ts +3 -3
- package/src/cli/commands/links.ts +34 -131
- package/src/cli/commands/ls.ts +6 -1
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +5 -0
- package/src/cli/program.ts +12 -2
- package/src/config/loader.ts +43 -0
- package/src/config/types.ts +8 -0
- package/src/core/audit-contract.ts +16 -4
- package/src/core/audit-freshness.ts +11 -1
- package/src/core/audit-links.ts +145 -25
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +19 -4
- package/src/core/audit.ts +67 -15
- package/src/core/context-compiler.ts +3 -0
- package/src/core/context-evidence.ts +11 -0
- package/src/core/graph-edge-confidence.ts +23 -1
- package/src/core/host-paths.ts +24 -5
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-workspace.ts +324 -0
- package/src/core/request-receipts.ts +63 -9
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/core/windows-private-path.ts +136 -1
- package/src/embed/backlog.ts +22 -10
- package/src/embed/variant-backlog.ts +34 -9
- package/src/index.ts +14 -1
- package/src/ingestion/graph-reconciliation.ts +77 -15
- package/src/ingestion/source-availability/darwin-path.ts +9 -3
- package/src/ingestion/sync.ts +22 -1
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +19 -0
- package/src/mcp/http-egress.ts +42 -3
- package/src/mcp/tools/audit.ts +11 -2
- package/src/mcp/tools/changes.ts +1 -0
- package/src/mcp/tools/links.ts +3 -0
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +22 -6
- package/src/pipeline/expansion.ts +19 -31
- package/src/pipeline/graph-retrieval.ts +22 -2
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/types.ts +6 -3
- package/src/serve/embed-scheduler.ts +2 -2
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/host-path-redaction.ts +51 -14
- package/src/serve/public/components/BootstrapStatus.tsx +5 -3
- package/src/serve/public/components/CaptureModal.tsx +1 -1
- package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
- package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
- package/src/serve/public/components/FirstRunWizard.tsx +4 -2
- package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
- package/src/serve/public/pages/Collections.tsx +16 -13
- package/src/serve/public/pages/Connectors.tsx +7 -4
- package/src/serve/public/pages/Dashboard.tsx +8 -6
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/resident-runtime.ts +7 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/links.ts +13 -0
- package/src/serve/routes/sessions.ts +81 -37
- package/src/serve/server.ts +5 -3
- package/src/serve/status-model.ts +10 -6
- package/src/serve/status.ts +2 -7
- package/src/sessions/config-refresh.ts +111 -0
- package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
- package/src/store/migrations/034-collection-link-workspace.ts +47 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +392 -233
- package/src/store/sqlite/change-journal-store.ts +1 -1
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +252 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
- package/src/store/sqlite/workspace-link-resolver.ts +654 -0
- package/src/store/types.ts +49 -3
- package/src/store/vector/stats.ts +1 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +0 -1
|
@@ -11,8 +11,11 @@ export const AUDIT_RULE_SET_VERSION = "1.0" as const;
|
|
|
11
11
|
|
|
12
12
|
/** Default returned finding cap; totals remain exact when truncated. */
|
|
13
13
|
export const AUDIT_DEFAULT_MAX_FINDINGS = 100;
|
|
14
|
-
/** Hard upper bound for `--max-findings` / MCP maxFindings. */
|
|
15
|
-
export const AUDIT_MAX_FINDINGS_LIMIT =
|
|
14
|
+
/** Hard numeric upper bound for `--max-findings` / MCP maxFindings. */
|
|
15
|
+
export const AUDIT_MAX_FINDINGS_LIMIT = 100_000;
|
|
16
|
+
/** Returns every finding of the bounded audit snapshot. */
|
|
17
|
+
export const AUDIT_MAX_FINDINGS_ALL = "all" as const;
|
|
18
|
+
export type AuditMaxFindings = number | typeof AUDIT_MAX_FINDINGS_ALL;
|
|
16
19
|
export const AUDIT_MAX_EVIDENCE_PER_FINDING = 8;
|
|
17
20
|
export const AUDIT_MAX_GUIDANCE_PER_FINDING = 4;
|
|
18
21
|
export const AUDIT_MAX_EVIDENCE_DETAIL_CHARS = 512;
|
|
@@ -201,7 +204,14 @@ export interface AuditCounts {
|
|
|
201
204
|
|
|
202
205
|
export interface AuditTruncation {
|
|
203
206
|
findingsTruncated: boolean;
|
|
204
|
-
maxFindings:
|
|
207
|
+
maxFindings: AuditMaxFindings;
|
|
208
|
+
/**
|
|
209
|
+
* The bounded audit snapshot (documents or links) was cut; finding totals
|
|
210
|
+
* cover the snapshot only and are not complete-index totals.
|
|
211
|
+
*/
|
|
212
|
+
snapshotTruncated: boolean;
|
|
213
|
+
/** Evidence items, candidate lists or evidence detail text were clipped. */
|
|
214
|
+
evidenceTruncated: boolean;
|
|
205
215
|
}
|
|
206
216
|
|
|
207
217
|
export interface AuditTiming {
|
|
@@ -255,6 +265,8 @@ export interface AuditRuleContribution {
|
|
|
255
265
|
findings?: AuditFindingDraft[];
|
|
256
266
|
/** Exact findings before any evaluator-side payload cap. */
|
|
257
267
|
findingCount?: number;
|
|
268
|
+
/** Evaluator clipped evidence (for example a long tied-candidate list). */
|
|
269
|
+
evidenceTruncated?: boolean;
|
|
258
270
|
examinedCount?: number;
|
|
259
271
|
durationMs?: number;
|
|
260
272
|
skipReason?: string | null;
|
|
@@ -283,7 +295,7 @@ export interface AuditRunInput {
|
|
|
283
295
|
capabilities: AuditCapabilitySnapshot;
|
|
284
296
|
captureFingerprints: AuditFingerprintCapture;
|
|
285
297
|
rules: readonly AuditRuleEvaluator[];
|
|
286
|
-
maxFindings?:
|
|
298
|
+
maxFindings?: AuditMaxFindings;
|
|
287
299
|
maxAttempts?: number;
|
|
288
300
|
gnoVersion?: string;
|
|
289
301
|
clock?: () => Date;
|
|
@@ -33,6 +33,8 @@ export interface AuditFreshnessOptions {
|
|
|
33
33
|
now: Date;
|
|
34
34
|
agePolicy?: AuditAgePolicy;
|
|
35
35
|
truncated?: boolean;
|
|
36
|
+
/** Effective per-rule cap; defaults to FRESHNESS_AUDIT_MAX_FINDINGS_PER_RULE. */
|
|
37
|
+
maxFindingsPerRule?: number;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const finding = (input: {
|
|
@@ -68,6 +70,7 @@ const rule = (input: {
|
|
|
68
70
|
skipped?: boolean;
|
|
69
71
|
message: string;
|
|
70
72
|
reason?: string;
|
|
73
|
+
maxFindingsPerRule?: number;
|
|
71
74
|
}): AuditRuleContribution => ({
|
|
72
75
|
ruleId: input.ruleId,
|
|
73
76
|
category: "freshness",
|
|
@@ -83,7 +86,10 @@ const rule = (input: {
|
|
|
83
86
|
message: input.message,
|
|
84
87
|
findings: [...input.findings]
|
|
85
88
|
.sort(compareAuditFindingDrafts)
|
|
86
|
-
.slice(
|
|
89
|
+
.slice(
|
|
90
|
+
0,
|
|
91
|
+
input.maxFindingsPerRule ?? FRESHNESS_AUDIT_MAX_FINDINGS_PER_RULE
|
|
92
|
+
),
|
|
87
93
|
findingCount: input.findings.length,
|
|
88
94
|
examinedCount: input.examinedCount,
|
|
89
95
|
skipReason: input.reason ?? null,
|
|
@@ -186,6 +192,7 @@ export const evaluateFreshnessAudit = (
|
|
|
186
192
|
: undefined;
|
|
187
193
|
return [
|
|
188
194
|
rule({
|
|
195
|
+
maxFindingsPerRule: options.maxFindingsPerRule,
|
|
189
196
|
ruleId: "freshness.source-readable",
|
|
190
197
|
findings: unavailable,
|
|
191
198
|
examinedCount: documents.length,
|
|
@@ -197,6 +204,7 @@ export const evaluateFreshnessAudit = (
|
|
|
197
204
|
reason,
|
|
198
205
|
}),
|
|
199
206
|
rule({
|
|
207
|
+
maxFindingsPerRule: options.maxFindingsPerRule,
|
|
200
208
|
ruleId: "freshness.source-index-drift",
|
|
201
209
|
findings: drift,
|
|
202
210
|
examinedCount: byteComparableCount,
|
|
@@ -207,6 +215,7 @@ export const evaluateFreshnessAudit = (
|
|
|
207
215
|
reason,
|
|
208
216
|
}),
|
|
209
217
|
rule({
|
|
218
|
+
maxFindingsPerRule: options.maxFindingsPerRule,
|
|
210
219
|
ruleId: "freshness.index-revision",
|
|
211
220
|
findings: staleRevision,
|
|
212
221
|
examinedCount: documents.length,
|
|
@@ -217,6 +226,7 @@ export const evaluateFreshnessAudit = (
|
|
|
217
226
|
reason,
|
|
218
227
|
}),
|
|
219
228
|
rule({
|
|
229
|
+
maxFindingsPerRule: options.maxFindingsPerRule,
|
|
220
230
|
ruleId: "freshness.configured-age-signal",
|
|
221
231
|
findings: ageSignals,
|
|
222
232
|
examinedCount: documents.length,
|
package/src/core/audit-links.ts
CHANGED
|
@@ -6,9 +6,16 @@ import type {
|
|
|
6
6
|
} from "../store/sqlite/graph-link-resolver";
|
|
7
7
|
import type { AuditFindingDraft, AuditRuleContribution } from "./audit";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { isTraversableResolution } from "../store/sqlite/graph-link-resolver";
|
|
10
|
+
import {
|
|
11
|
+
AUDIT_MAX_EVIDENCE_DETAIL_CHARS,
|
|
12
|
+
compareAuditCodeUnits,
|
|
13
|
+
compareAuditFindingDrafts,
|
|
14
|
+
} from "./audit";
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
/** 1.1: workspace-wide resolution and reference/resolution/scope evidence. */
|
|
17
|
+
export const LINK_AUDIT_RULE_VERSION = "1.1" as const;
|
|
18
|
+
/** Default per-rule cap; runs pass the effective `maxFindings` instead. */
|
|
12
19
|
export const LINK_AUDIT_MAX_FINDINGS_PER_RULE = 1000;
|
|
13
20
|
|
|
14
21
|
export interface AuditOrphanPolicy {
|
|
@@ -16,23 +23,123 @@ export interface AuditOrphanPolicy {
|
|
|
16
23
|
ignorePathPrefixes: readonly string[];
|
|
17
24
|
/** Mirrored duplicate rows are excluded from orphan claims by default. */
|
|
18
25
|
ignoreMirrorDuplicates?: boolean;
|
|
26
|
+
/** Effective per-rule cap; defaults to LINK_AUDIT_MAX_FINDINGS_PER_RULE. */
|
|
27
|
+
maxFindingsPerRule?: number;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
const boundedFindings = (
|
|
22
|
-
findings: readonly AuditFindingDraft[]
|
|
31
|
+
findings: readonly AuditFindingDraft[],
|
|
32
|
+
maxFindingsPerRule: number
|
|
23
33
|
): AuditFindingDraft[] =>
|
|
24
|
-
[...findings]
|
|
25
|
-
.sort(compareAuditFindingDrafts)
|
|
26
|
-
.slice(0, LINK_AUDIT_MAX_FINDINGS_PER_RULE);
|
|
34
|
+
[...findings].sort(compareAuditFindingDrafts).slice(0, maxFindingsPerRule);
|
|
27
35
|
|
|
28
36
|
const lineLocation = (line: number, column: number): string =>
|
|
29
37
|
`L${line}:C${column}`;
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
type LinkReferenceKind =
|
|
40
|
+
| "wiki-name"
|
|
41
|
+
| "wiki-path"
|
|
42
|
+
| "explicit-collection"
|
|
43
|
+
| "markdown";
|
|
44
|
+
|
|
45
|
+
const referenceKind = (
|
|
32
46
|
link: AuditLinkSnapshot["links"][number]
|
|
47
|
+
): LinkReferenceKind =>
|
|
48
|
+
link.linkType === "markdown"
|
|
49
|
+
? "markdown"
|
|
50
|
+
: link.explicitCollection === true
|
|
51
|
+
? "explicit-collection"
|
|
52
|
+
: link.targetRef.includes("/")
|
|
53
|
+
? "wiki-path"
|
|
54
|
+
: "wiki-name";
|
|
55
|
+
|
|
56
|
+
interface CandidateEvidence {
|
|
57
|
+
/** Canonical-path-ordered tied candidates visible in the audit scope. */
|
|
58
|
+
uris: string[];
|
|
59
|
+
/** Candidates outside the requested collection scope (identity withheld). */
|
|
60
|
+
withheld: number;
|
|
61
|
+
truncated: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const linkFinding = (
|
|
65
|
+
link: AuditLinkSnapshot["links"][number],
|
|
66
|
+
context: {
|
|
67
|
+
documents: ReadonlyMap<number, AuditLinkSnapshotDocument>;
|
|
68
|
+
scope: ReadonlySet<string> | null;
|
|
69
|
+
},
|
|
70
|
+
onEvidenceTruncated: () => void
|
|
33
71
|
): AuditFindingDraft => {
|
|
34
72
|
const ambiguous = (link.resolved?.matchCount ?? 0) > 1;
|
|
35
73
|
const target = `${link.targetCollection}:${link.targetRef}`;
|
|
74
|
+
const kind = referenceKind(link);
|
|
75
|
+
const tied = (link.resolved?.candidates ?? []).map((candidate) =>
|
|
76
|
+
context.documents.get(candidate.id)
|
|
77
|
+
);
|
|
78
|
+
const visible = tied.filter(
|
|
79
|
+
(document): document is AuditLinkSnapshotDocument =>
|
|
80
|
+
document !== undefined &&
|
|
81
|
+
(context.scope === null || context.scope.has(document.collection))
|
|
82
|
+
);
|
|
83
|
+
// Scope describes every tied candidate; withheld ones are only counted.
|
|
84
|
+
const resolvedScope = !ambiguous
|
|
85
|
+
? null
|
|
86
|
+
: link.explicitCollection === true
|
|
87
|
+
? "explicit-collection"
|
|
88
|
+
: tied.some(
|
|
89
|
+
(document) =>
|
|
90
|
+
document !== undefined &&
|
|
91
|
+
document.collection !== link.sourceCollection
|
|
92
|
+
)
|
|
93
|
+
? "cross-collection"
|
|
94
|
+
: "same-collection";
|
|
95
|
+
const base = {
|
|
96
|
+
anchor: link.targetAnchor,
|
|
97
|
+
endColumn: link.endCol,
|
|
98
|
+
endLine: link.endLine,
|
|
99
|
+
linkType: link.linkType,
|
|
100
|
+
matchCount: link.resolved?.matchCount ?? 0,
|
|
101
|
+
matchRank: link.resolved?.matchRank ?? null,
|
|
102
|
+
normalizedTarget: link.targetRefNorm,
|
|
103
|
+
referenceKind: kind,
|
|
104
|
+
resolutionStatus: ambiguous ? "ambiguous" : "unresolved",
|
|
105
|
+
resolvedScope,
|
|
106
|
+
};
|
|
107
|
+
// Candidate URIs are added while the detail stays within the evidence
|
|
108
|
+
// bound, so a long tie is reported as truncated rather than silently cut.
|
|
109
|
+
const candidates: CandidateEvidence = {
|
|
110
|
+
uris: [],
|
|
111
|
+
withheld: tied.length - visible.length,
|
|
112
|
+
truncated: false,
|
|
113
|
+
};
|
|
114
|
+
const render = (): string =>
|
|
115
|
+
JSON.stringify(
|
|
116
|
+
tied.length > 0
|
|
117
|
+
? {
|
|
118
|
+
...base,
|
|
119
|
+
candidateCount: tied.length,
|
|
120
|
+
candidates: candidates.uris,
|
|
121
|
+
...(candidates.withheld > 0
|
|
122
|
+
? { candidatesWithheld: candidates.withheld }
|
|
123
|
+
: {}),
|
|
124
|
+
...(candidates.truncated ? { candidatesTruncated: true } : {}),
|
|
125
|
+
}
|
|
126
|
+
: base
|
|
127
|
+
);
|
|
128
|
+
for (const document of visible) {
|
|
129
|
+
candidates.uris.push(document.uri);
|
|
130
|
+
if (Array.from(render()).length > AUDIT_MAX_EVIDENCE_DETAIL_CHARS) {
|
|
131
|
+
candidates.uris.pop();
|
|
132
|
+
candidates.truncated = true;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
candidates.truncated &&
|
|
138
|
+
Array.from(render()).length > AUDIT_MAX_EVIDENCE_DETAIL_CHARS
|
|
139
|
+
) {
|
|
140
|
+
candidates.uris.pop();
|
|
141
|
+
}
|
|
142
|
+
if (candidates.truncated) onEvidenceTruncated();
|
|
36
143
|
return {
|
|
37
144
|
subject: link.sourceUri,
|
|
38
145
|
location: lineLocation(link.startLine, link.startCol),
|
|
@@ -46,19 +153,15 @@ const linkFinding = (
|
|
|
46
153
|
summary: target,
|
|
47
154
|
uri: link.sourceUri,
|
|
48
155
|
path: link.sourceRelPath,
|
|
49
|
-
detail:
|
|
50
|
-
anchor: link.targetAnchor,
|
|
51
|
-
endColumn: link.endCol,
|
|
52
|
-
endLine: link.endLine,
|
|
53
|
-
linkType: link.linkType,
|
|
54
|
-
matchCount: link.resolved?.matchCount ?? 0,
|
|
55
|
-
matchRank: link.resolved?.matchRank ?? null,
|
|
56
|
-
normalizedTarget: link.targetRefNorm,
|
|
57
|
-
}),
|
|
156
|
+
detail: render(),
|
|
58
157
|
},
|
|
59
158
|
],
|
|
60
159
|
guidance: ambiguous
|
|
61
|
-
? [
|
|
160
|
+
? [
|
|
161
|
+
tied.length > 0
|
|
162
|
+
? "Use a longer path such as [[Folder/Note]] to pick one target"
|
|
163
|
+
: "Use an explicit collection-relative target path",
|
|
164
|
+
]
|
|
62
165
|
: ["Create the target or correct the local link"],
|
|
63
166
|
};
|
|
64
167
|
};
|
|
@@ -112,20 +215,36 @@ export const evaluateLinkAudit = (
|
|
|
112
215
|
const auditedDocumentIds = new Set(
|
|
113
216
|
snapshot.auditedDocumentIds ?? snapshot.documents.map(({ id }) => id)
|
|
114
217
|
);
|
|
218
|
+
let evidenceTruncated = false;
|
|
219
|
+
const findingContext = {
|
|
220
|
+
documents: new Map(snapshot.documents.map((doc) => [doc.id, doc])),
|
|
221
|
+
scope: snapshot.scopeCollections
|
|
222
|
+
? new Set(snapshot.scopeCollections)
|
|
223
|
+
: null,
|
|
224
|
+
};
|
|
225
|
+
const finding = (link: AuditLinkSnapshot["links"][number]) =>
|
|
226
|
+
linkFinding(link, findingContext, () => {
|
|
227
|
+
evidenceTruncated = true;
|
|
228
|
+
});
|
|
115
229
|
for (const link of snapshot.links) {
|
|
116
230
|
const sourceAudited = auditedDocumentIds.has(link.sourceId);
|
|
117
231
|
if (!link.resolved) {
|
|
118
|
-
if (sourceAudited) unresolved.push(
|
|
232
|
+
if (sourceAudited) unresolved.push(finding(link));
|
|
119
233
|
continue;
|
|
120
234
|
}
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
connected.add(link.
|
|
235
|
+
// A tied workspace link is not a resolved link: it connects nothing.
|
|
236
|
+
if (isTraversableResolution(link.resolved)) {
|
|
237
|
+
if (sourceAudited) connected.add(link.sourceId);
|
|
238
|
+
if (auditedDocumentIds.has(link.resolved.targetId)) {
|
|
239
|
+
connected.add(link.resolved.targetId);
|
|
240
|
+
}
|
|
124
241
|
}
|
|
125
242
|
if (sourceAudited && link.resolved.matchCount > 1) {
|
|
126
|
-
ambiguous.push(
|
|
243
|
+
ambiguous.push(finding(link));
|
|
127
244
|
}
|
|
128
245
|
}
|
|
246
|
+
const maxFindingsPerRule =
|
|
247
|
+
policy.maxFindingsPerRule ?? LINK_AUDIT_MAX_FINDINGS_PER_RULE;
|
|
129
248
|
const roots = new Set(normalizedPrefixes(policy.rootUris));
|
|
130
249
|
const ignorePrefixes = normalizedPrefixes(policy.ignorePathPrefixes);
|
|
131
250
|
const mirroredIds = duplicateMirrorIds(
|
|
@@ -179,7 +298,7 @@ export const evaluateLinkAudit = (
|
|
|
179
298
|
message: partial
|
|
180
299
|
? "Local target scan was truncated"
|
|
181
300
|
: `${unresolved.length} unresolved or broken local links`,
|
|
182
|
-
findings: boundedFindings(unresolved),
|
|
301
|
+
findings: boundedFindings(unresolved, maxFindingsPerRule),
|
|
183
302
|
findingCount: unresolved.length,
|
|
184
303
|
skipReason: partial ? "snapshot_truncated" : null,
|
|
185
304
|
},
|
|
@@ -187,11 +306,12 @@ export const evaluateLinkAudit = (
|
|
|
187
306
|
...common,
|
|
188
307
|
ruleId: "links.ambiguous-targets",
|
|
189
308
|
category: "links",
|
|
309
|
+
evidenceTruncated,
|
|
190
310
|
status: statusFor(ambiguous),
|
|
191
311
|
message: partial
|
|
192
312
|
? "Ambiguous target scan was truncated"
|
|
193
313
|
: `${ambiguous.length} ambiguous local links`,
|
|
194
|
-
findings: boundedFindings(ambiguous),
|
|
314
|
+
findings: boundedFindings(ambiguous, maxFindingsPerRule),
|
|
195
315
|
findingCount: ambiguous.length,
|
|
196
316
|
skipReason: partial ? "snapshot_truncated" : null,
|
|
197
317
|
},
|
|
@@ -203,7 +323,7 @@ export const evaluateLinkAudit = (
|
|
|
203
323
|
message: partial
|
|
204
324
|
? "Orphan scan was truncated"
|
|
205
325
|
: `${orphanFindings.length} policy-defined orphan documents`,
|
|
206
|
-
findings: boundedFindings(orphanFindings),
|
|
326
|
+
findings: boundedFindings(orphanFindings, maxFindingsPerRule),
|
|
207
327
|
findingCount: orphanFindings.length,
|
|
208
328
|
skipReason: partial ? "snapshot_truncated" : null,
|
|
209
329
|
},
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
validateDeclaredRecordProvenance,
|
|
12
12
|
} from "./record-metadata";
|
|
13
13
|
|
|
14
|
+
/** Default per-rule cap; runs pass the effective `maxFindings` instead. */
|
|
14
15
|
export const PROVENANCE_AUDIT_MAX_FINDINGS_PER_RULE = 1000;
|
|
15
16
|
|
|
16
17
|
export interface AuditProvenanceDocument {
|
|
@@ -87,7 +88,7 @@ const memoryFinding = (
|
|
|
87
88
|
*/
|
|
88
89
|
export const evaluateMemoryRecordAudit = (
|
|
89
90
|
documents: readonly AuditProvenanceDocument[],
|
|
90
|
-
options: { truncated?: boolean } = {}
|
|
91
|
+
options: { truncated?: boolean; maxFindingsPerRule?: number } = {}
|
|
91
92
|
): AuditRuleContribution => {
|
|
92
93
|
const findings: AuditFindingDraft[] = [];
|
|
93
94
|
let managedDocuments = 0;
|
|
@@ -129,7 +130,10 @@ export const evaluateMemoryRecordAudit = (
|
|
|
129
130
|
: managedDocuments === 0
|
|
130
131
|
? "No memory-managed collections in scope"
|
|
131
132
|
: `${managedDocuments} managed memory record(s) satisfy the contract`,
|
|
132
|
-
findings: findings.slice(
|
|
133
|
+
findings: findings.slice(
|
|
134
|
+
0,
|
|
135
|
+
options.maxFindingsPerRule ?? PROVENANCE_AUDIT_MAX_FINDINGS_PER_RULE
|
|
136
|
+
),
|
|
133
137
|
findingCount: findings.length,
|
|
134
138
|
examinedCount: managedDocuments,
|
|
135
139
|
skipReason: truncated
|
|
@@ -145,7 +149,7 @@ export const evaluateMemoryRecordAudit = (
|
|
|
145
149
|
/** Missing provenance is completeness evidence, never a truth judgment. */
|
|
146
150
|
export const evaluateProvenanceAudit = (
|
|
147
151
|
documents: readonly AuditProvenanceDocument[],
|
|
148
|
-
options: { truncated?: boolean } = {}
|
|
152
|
+
options: { truncated?: boolean; maxFindingsPerRule?: number } = {}
|
|
149
153
|
): AuditRuleContribution[] => {
|
|
150
154
|
const captureFindings: AuditFindingDraft[] = [];
|
|
151
155
|
const recordFindings: AuditFindingDraft[] = [];
|
|
@@ -217,7 +221,10 @@ export const evaluateProvenanceAudit = (
|
|
|
217
221
|
: `${findings.length} declared provenance completeness issues`,
|
|
218
222
|
findings: [...findings]
|
|
219
223
|
.sort(compareAuditFindingDrafts)
|
|
220
|
-
.slice(
|
|
224
|
+
.slice(
|
|
225
|
+
0,
|
|
226
|
+
options.maxFindingsPerRule ?? PROVENANCE_AUDIT_MAX_FINDINGS_PER_RULE
|
|
227
|
+
),
|
|
221
228
|
findingCount: findings.length,
|
|
222
229
|
examinedCount: documents.length,
|
|
223
230
|
skipReason: truncated
|
|
@@ -9,6 +9,7 @@ import type { DocumentRow } from "../store/types";
|
|
|
9
9
|
import type {
|
|
10
10
|
AuditCategory,
|
|
11
11
|
AuditFingerprints,
|
|
12
|
+
AuditMaxFindings,
|
|
12
13
|
AuditRunResult,
|
|
13
14
|
AuditScope,
|
|
14
15
|
} from "./audit";
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
AUDIT_RULE_SET_VERSION,
|
|
27
28
|
canonicalAuditJson,
|
|
28
29
|
hashAuditCanonical,
|
|
30
|
+
resolveAuditMaxFindings,
|
|
29
31
|
runAudit,
|
|
30
32
|
} from "./audit";
|
|
31
33
|
import { evaluateFreshnessAudit } from "./audit-freshness";
|
|
@@ -55,7 +57,7 @@ export interface WorkspaceAuditOptions {
|
|
|
55
57
|
collectionFilters?: readonly string[];
|
|
56
58
|
pathFilters?: readonly string[];
|
|
57
59
|
tagFilters?: readonly string[];
|
|
58
|
-
maxFindings?:
|
|
60
|
+
maxFindings?: AuditMaxFindings;
|
|
59
61
|
agePolicy?: AuditFreshnessOptions["agePolicy"];
|
|
60
62
|
orphanRoots?: readonly string[];
|
|
61
63
|
orphanIgnorePrefixes?: readonly string[];
|
|
@@ -347,7 +349,8 @@ const filterLinkSnapshot = (
|
|
|
347
349
|
snapshot: AuditLinkSnapshot,
|
|
348
350
|
selectedIds: ReadonlySet<number>,
|
|
349
351
|
selectedDocuments: readonly DocumentRow[],
|
|
350
|
-
selectionTruncated: boolean
|
|
352
|
+
selectionTruncated: boolean,
|
|
353
|
+
scopeCollections: readonly string[]
|
|
351
354
|
): AuditLinkSnapshot => {
|
|
352
355
|
const links = snapshot.links.filter(
|
|
353
356
|
(link) =>
|
|
@@ -362,6 +365,9 @@ const filterLinkSnapshot = (
|
|
|
362
365
|
// Preserve graph-wide documents as duplicate-mirror evidence while the
|
|
363
366
|
// explicit id set prevents findings outside the requested audit scope.
|
|
364
367
|
auditedDocumentIds: [...selectedIds],
|
|
368
|
+
...(scopeCollections.length > 0
|
|
369
|
+
? { scopeCollections: [...scopeCollections] }
|
|
370
|
+
: {}),
|
|
365
371
|
links,
|
|
366
372
|
totals: { documents: selectedDocuments.length, links: outgoingTotal },
|
|
367
373
|
truncated: {
|
|
@@ -429,7 +435,8 @@ const loadWorkspaceSnapshot = async (
|
|
|
429
435
|
rawLinks,
|
|
430
436
|
selectedIds,
|
|
431
437
|
selected.documents,
|
|
432
|
-
selected.truncated
|
|
438
|
+
selected.truncated,
|
|
439
|
+
filters.collections
|
|
433
440
|
),
|
|
434
441
|
truncated: selected.truncated,
|
|
435
442
|
};
|
|
@@ -596,6 +603,12 @@ export const runWorkspaceAudit = async (
|
|
|
596
603
|
snapshots.set(attempt, pending);
|
|
597
604
|
return pending;
|
|
598
605
|
};
|
|
606
|
+
// Per-family caps follow the effective report cap; runAudit rejects an
|
|
607
|
+
// invalid value before any rule runs.
|
|
608
|
+
const resolvedMaxFindings = resolveAuditMaxFindings(options.maxFindings);
|
|
609
|
+
const maxFindingsPerRule = resolvedMaxFindings.ok
|
|
610
|
+
? resolvedMaxFindings.limit
|
|
611
|
+
: 0;
|
|
599
612
|
const result = await runAudit({
|
|
600
613
|
scope,
|
|
601
614
|
capabilities: {
|
|
@@ -645,6 +658,7 @@ export const runWorkspaceAudit = async (
|
|
|
645
658
|
...evaluateLinkAudit(snapshot.links, {
|
|
646
659
|
rootUris: options.orphanRoots ?? [],
|
|
647
660
|
ignorePathPrefixes: options.orphanIgnorePrefixes ?? [],
|
|
661
|
+
maxFindingsPerRule,
|
|
648
662
|
})
|
|
649
663
|
);
|
|
650
664
|
await options.onProgress?.({
|
|
@@ -657,7 +671,7 @@ export const runWorkspaceAudit = async (
|
|
|
657
671
|
contributions.push(
|
|
658
672
|
...evaluateProvenanceAudit(
|
|
659
673
|
snapshot.documents.map(({ provenance }) => provenance),
|
|
660
|
-
{ truncated: snapshot.truncated }
|
|
674
|
+
{ truncated: snapshot.truncated, maxFindingsPerRule }
|
|
661
675
|
)
|
|
662
676
|
);
|
|
663
677
|
await options.onProgress?.({
|
|
@@ -674,6 +688,7 @@ export const runWorkspaceAudit = async (
|
|
|
674
688
|
now: options.now ?? new Date(),
|
|
675
689
|
agePolicy: options.agePolicy,
|
|
676
690
|
truncated: snapshot.truncated,
|
|
691
|
+
maxFindingsPerRule,
|
|
677
692
|
}
|
|
678
693
|
)
|
|
679
694
|
);
|
package/src/core/audit.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
AuditFinding,
|
|
7
7
|
AuditFingerprints,
|
|
8
8
|
AuditForbiddenStoreMethod,
|
|
9
|
+
AuditMaxFindings,
|
|
9
10
|
AuditReport,
|
|
10
11
|
AuditReportStatus,
|
|
11
12
|
AuditRuleContribution,
|
|
@@ -24,6 +25,9 @@ import {
|
|
|
24
25
|
AUDIT_DEFAULT_MAX_FINDINGS,
|
|
25
26
|
AUDIT_FORBIDDEN_STORE_METHODS,
|
|
26
27
|
AUDIT_MAX_CODE_CHARS,
|
|
28
|
+
AUDIT_MAX_EVIDENCE_DETAIL_CHARS,
|
|
29
|
+
AUDIT_MAX_EVIDENCE_PER_FINDING,
|
|
30
|
+
AUDIT_MAX_FINDINGS_ALL,
|
|
27
31
|
AUDIT_MAX_FINDINGS_LIMIT,
|
|
28
32
|
AUDIT_MAX_IDENTIFIER_CHARS,
|
|
29
33
|
AUDIT_MAX_MESSAGE_CHARS,
|
|
@@ -127,23 +131,61 @@ export const normalizeAuditScope = (
|
|
|
127
131
|
};
|
|
128
132
|
};
|
|
129
133
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
export const AUDIT_MAX_FINDINGS_RANGE_MESSAGE = `maxFindings must be an integer between 1 and ${AUDIT_MAX_FINDINGS_LIMIT}, or "${AUDIT_MAX_FINDINGS_ALL}"`;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate `maxFindings`. `limit` is the effective numeric cap (Infinity for
|
|
138
|
+
* `all`); `reported` is the value echoed in the report's truncation block.
|
|
139
|
+
*/
|
|
140
|
+
export const resolveAuditMaxFindings = (
|
|
141
|
+
value: AuditMaxFindings | undefined
|
|
142
|
+
):
|
|
143
|
+
| { ok: true; limit: number; reported: AuditMaxFindings }
|
|
144
|
+
| { ok: false; error: string } => {
|
|
145
|
+
if (value === AUDIT_MAX_FINDINGS_ALL) {
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
limit: Number.POSITIVE_INFINITY,
|
|
149
|
+
reported: AUDIT_MAX_FINDINGS_ALL,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
133
152
|
const maxFindings = value ?? AUDIT_DEFAULT_MAX_FINDINGS;
|
|
134
153
|
if (
|
|
135
154
|
!Number.isSafeInteger(maxFindings) ||
|
|
136
155
|
maxFindings < 1 ||
|
|
137
156
|
maxFindings > AUDIT_MAX_FINDINGS_LIMIT
|
|
138
157
|
) {
|
|
139
|
-
return {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
return { ok: false, error: AUDIT_MAX_FINDINGS_RANGE_MESSAGE };
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, limit: maxFindings, reported: maxFindings };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** Parse a CLI `--max-findings` value: a positive integer or `all`. */
|
|
164
|
+
export const parseAuditMaxFindingsInput = (
|
|
165
|
+
raw: string
|
|
166
|
+
): AuditMaxFindings | undefined => {
|
|
167
|
+
const value = raw.trim();
|
|
168
|
+
if (value.toLowerCase() === AUDIT_MAX_FINDINGS_ALL) {
|
|
169
|
+
return AUDIT_MAX_FINDINGS_ALL;
|
|
143
170
|
}
|
|
144
|
-
|
|
171
|
+
if (!/^\d+$/.test(value)) return undefined;
|
|
172
|
+
const parsed = Number(value);
|
|
173
|
+
return resolveAuditMaxFindings(parsed).ok ? parsed : undefined;
|
|
145
174
|
};
|
|
146
175
|
|
|
176
|
+
const draftEvidenceTruncated = (contribution: AuditRuleContribution): boolean =>
|
|
177
|
+
contribution.evidenceTruncated === true ||
|
|
178
|
+
(contribution.findings ?? []).some(
|
|
179
|
+
(draft) =>
|
|
180
|
+
draft.evidence.length > AUDIT_MAX_EVIDENCE_PER_FINDING ||
|
|
181
|
+
draft.evidence.some(
|
|
182
|
+
(item) =>
|
|
183
|
+
item.detail !== undefined &&
|
|
184
|
+
Array.from(normalizeAuditText(item.detail)).length >
|
|
185
|
+
AUDIT_MAX_EVIDENCE_DETAIL_CHARS
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
|
|
147
189
|
const fingerprintsEqual = (
|
|
148
190
|
left: AuditFingerprints,
|
|
149
191
|
right: AuditFingerprints
|
|
@@ -231,7 +273,8 @@ const buildReportFromRules = (input: {
|
|
|
231
273
|
versions: AuditVersions;
|
|
232
274
|
rules: AuditRuleResult[];
|
|
233
275
|
status: AuditReportStatus;
|
|
234
|
-
maxFindings: number;
|
|
276
|
+
maxFindings: { limit: number; reported: AuditMaxFindings };
|
|
277
|
+
evidenceTruncated: boolean;
|
|
235
278
|
startedAt: string;
|
|
236
279
|
completedAt: string;
|
|
237
280
|
snapshotMs: number;
|
|
@@ -261,9 +304,9 @@ const buildReportFromRules = (input: {
|
|
|
261
304
|
(sum, rule) => sum + rule.findingCount,
|
|
262
305
|
0
|
|
263
306
|
);
|
|
264
|
-
const truncated = exactFindingCount > input.maxFindings;
|
|
307
|
+
const truncated = exactFindingCount > input.maxFindings.limit;
|
|
265
308
|
const returnedFindings = truncated
|
|
266
|
-
? uniqueFindings.slice(0, input.maxFindings)
|
|
309
|
+
? uniqueFindings.slice(0, input.maxFindings.limit)
|
|
267
310
|
: uniqueFindings;
|
|
268
311
|
const returnedFindingIds = new Set(
|
|
269
312
|
returnedFindings.map((finding) => finding.id)
|
|
@@ -308,7 +351,11 @@ const buildReportFromRules = (input: {
|
|
|
308
351
|
},
|
|
309
352
|
truncation: {
|
|
310
353
|
findingsTruncated: truncated,
|
|
311
|
-
maxFindings: input.maxFindings,
|
|
354
|
+
maxFindings: input.maxFindings.reported,
|
|
355
|
+
snapshotTruncated: rules.some(
|
|
356
|
+
(rule) => rule.skipReason === "snapshot_truncated"
|
|
357
|
+
),
|
|
358
|
+
evidenceTruncated: input.evidenceTruncated,
|
|
312
359
|
},
|
|
313
360
|
timing: {
|
|
314
361
|
snapshotMs: Math.max(0, Math.round(input.snapshotMs)),
|
|
@@ -329,7 +376,7 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
|
|
|
329
376
|
if (!scopeResult.ok) {
|
|
330
377
|
return { ok: false, exit: "invalid", error: scopeResult.error };
|
|
331
378
|
}
|
|
332
|
-
const maxFindingsResult =
|
|
379
|
+
const maxFindingsResult = resolveAuditMaxFindings(input.maxFindings);
|
|
333
380
|
if (!maxFindingsResult.ok) {
|
|
334
381
|
return { ok: false, exit: "invalid", error: maxFindingsResult.error };
|
|
335
382
|
}
|
|
@@ -355,6 +402,7 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
|
|
|
355
402
|
let snapshotMs = 0;
|
|
356
403
|
let rulesMs = 0;
|
|
357
404
|
let lastRules: AuditRuleResult[] = [];
|
|
405
|
+
let lastEvidenceTruncated = false;
|
|
358
406
|
let lastFingerprints: AuditFingerprints | null = null;
|
|
359
407
|
let snapshotChanged = false;
|
|
360
408
|
let failed = false;
|
|
@@ -405,6 +453,7 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
|
|
|
405
453
|
try {
|
|
406
454
|
const contributions = await collectContributions(input.rules, ruleCtx);
|
|
407
455
|
lastRules = contributions.map(materializeRule);
|
|
456
|
+
lastEvidenceTruncated = contributions.some(draftEvidenceTruncated);
|
|
408
457
|
} catch (cause) {
|
|
409
458
|
if (input.signal?.aborted) {
|
|
410
459
|
lastRules = [cancellationRule()];
|
|
@@ -453,6 +502,7 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
|
|
|
453
502
|
}
|
|
454
503
|
// Discard rule results from a drifted attempt; retry with a fresh snapshot.
|
|
455
504
|
lastRules = [];
|
|
505
|
+
lastEvidenceTruncated = false;
|
|
456
506
|
}
|
|
457
507
|
|
|
458
508
|
const completedAt = clock();
|
|
@@ -484,7 +534,8 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
|
|
|
484
534
|
},
|
|
485
535
|
],
|
|
486
536
|
status: "failed",
|
|
487
|
-
maxFindings: maxFindingsResult
|
|
537
|
+
maxFindings: maxFindingsResult,
|
|
538
|
+
evidenceTruncated: false,
|
|
488
539
|
startedAt: runStartedAt.toISOString(),
|
|
489
540
|
completedAt: completedAt.toISOString(),
|
|
490
541
|
snapshotMs,
|
|
@@ -531,7 +582,8 @@ export async function runAudit(input: AuditRunInput): Promise<AuditRunResult> {
|
|
|
531
582
|
rules: rulesForReport,
|
|
532
583
|
snapshotChanged: false,
|
|
533
584
|
}),
|
|
534
|
-
maxFindings: maxFindingsResult
|
|
585
|
+
maxFindings: maxFindingsResult,
|
|
586
|
+
evidenceTruncated: lastEvidenceTruncated,
|
|
535
587
|
startedAt: runStartedAt.toISOString(),
|
|
536
588
|
completedAt: completedAt.toISOString(),
|
|
537
589
|
snapshotMs,
|
|
@@ -355,6 +355,9 @@ export const planContextEvidence = async <T, P>(
|
|
|
355
355
|
await deps.retrieve({
|
|
356
356
|
query,
|
|
357
357
|
collection,
|
|
358
|
+
// The primary search is partitioned per collection; graph expansion
|
|
359
|
+
// may bridge between the requested collections, never beyond them.
|
|
360
|
+
...(collections.length > 0 ? { graphCollections: collections } : {}),
|
|
358
361
|
noExpand: true,
|
|
359
362
|
queryModes,
|
|
360
363
|
tagsAll: input.tagsAll,
|