@gmickel/gno 2.7.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +3 -2
  2. package/assets/skill/SKILL.md +8 -1
  3. package/assets/skill/cli-reference.md +8 -1
  4. package/assets/skill/examples.md +2 -1
  5. package/assets/skill/mcp-reference.md +3 -1
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.7.1.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/spec/cli.md +48 -6
  12. package/spec/db/schema.sql +0 -1
  13. package/spec/mcp.md +18 -3
  14. package/spec/output-schemas/audit-report.schema.json +18 -4
  15. package/spec/output-schemas/backlinks.schema.json +4 -0
  16. package/spec/output-schemas/collection-list.schema.json +13 -0
  17. package/spec/output-schemas/graph.schema.json +2 -0
  18. package/spec/output-schemas/links-list.schema.json +4 -0
  19. package/spec/output-schemas/status.schema.json +15 -0
  20. package/src/cli/commands/audit.ts +23 -4
  21. package/src/cli/commands/collection/list.ts +39 -5
  22. package/src/cli/commands/embed.ts +3 -3
  23. package/src/cli/commands/links.ts +34 -131
  24. package/src/cli/commands/shared.ts +7 -0
  25. package/src/cli/commands/status.ts +5 -0
  26. package/src/cli/program.ts +12 -2
  27. package/src/config/loader.ts +43 -0
  28. package/src/config/types.ts +8 -0
  29. package/src/core/audit-contract.ts +16 -4
  30. package/src/core/audit-freshness.ts +11 -1
  31. package/src/core/audit-links.ts +145 -25
  32. package/src/core/audit-provenance.ts +11 -4
  33. package/src/core/audit-workspace.ts +19 -4
  34. package/src/core/audit.ts +67 -15
  35. package/src/core/context-compiler.ts +3 -0
  36. package/src/core/context-evidence.ts +11 -0
  37. package/src/core/graph-edge-confidence.ts +23 -1
  38. package/src/core/host-paths.ts +1 -0
  39. package/src/core/knowledge-impact.ts +28 -0
  40. package/src/core/link-workspace.ts +324 -0
  41. package/src/core/retrieval-replay-candidate.ts +6 -0
  42. package/src/core/retrieval-trace-request.ts +3 -0
  43. package/src/index.ts +14 -1
  44. package/src/ingestion/graph-reconciliation.ts +77 -15
  45. package/src/ingestion/source-availability/darwin-path.ts +9 -3
  46. package/src/ingestion/sync.ts +22 -1
  47. package/src/ingestion/types.ts +14 -0
  48. package/src/llm/inference-scope.ts +4 -3
  49. package/src/mcp/http-egress.ts +42 -3
  50. package/src/mcp/tools/audit.ts +11 -2
  51. package/src/mcp/tools/changes.ts +1 -0
  52. package/src/mcp/tools/links.ts +3 -0
  53. package/src/mcp/tools/sessions.ts +33 -4
  54. package/src/mcp/tools/status.ts +3 -0
  55. package/src/pipeline/expansion.ts +19 -31
  56. package/src/pipeline/graph-retrieval.ts +22 -2
  57. package/src/pipeline/hybrid.ts +1 -1
  58. package/src/pipeline/types.ts +6 -3
  59. package/src/serve/findings-pass.ts +1 -1
  60. package/src/serve/public/pages/GraphView.tsx +2 -0
  61. package/src/serve/routes/changes.ts +6 -1
  62. package/src/serve/routes/links.ts +13 -0
  63. package/src/serve/routes/sessions.ts +41 -53
  64. package/src/sessions/config-refresh.ts +111 -0
  65. package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
  66. package/src/store/migrations/034-collection-link-workspace.ts +47 -0
  67. package/src/store/migrations/index.ts +4 -0
  68. package/src/store/sqlite/adapter.ts +390 -231
  69. package/src/store/sqlite/eligibility.ts +8 -2
  70. package/src/store/sqlite/graph-link-resolver.ts +252 -5
  71. package/src/store/sqlite/graph-neighbors.ts +147 -40
  72. package/src/store/sqlite/graph-reference-state.ts +13 -2
  73. package/src/store/sqlite/workspace-link-resolver.ts +654 -0
  74. package/src/store/types.ts +49 -3
  75. package/src/store/vector/stats.ts +1 -1
  76. package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +0 -1
@@ -5,8 +5,6 @@
5
5
  * @module src/cli/commands/links
6
6
  */
7
7
 
8
- import { basename } from "node:path";
9
-
10
8
  import type {
11
9
  DocEdgeRow,
12
10
  DocLinkRow,
@@ -14,7 +12,6 @@ import type {
14
12
  StorePort,
15
13
  } from "../../store/types";
16
14
 
17
- import { normalizeWikiName } from "../../core/links";
18
15
  import { resolveDocRef } from "../../core/ref-parser";
19
16
  import { initStore } from "./shared";
20
17
 
@@ -51,6 +48,8 @@ export interface LinkWithResolution {
51
48
  resolved: boolean;
52
49
  resolvedDocid?: string;
53
50
  resolvedUri?: string;
51
+ /** Collection of the resolved target (may differ from the source's). */
52
+ resolvedCollection?: string;
54
53
  }
55
54
 
56
55
  export interface SemanticLinkItem {
@@ -155,6 +154,8 @@ export interface BacklinkItem {
155
154
  sourceDocid: string;
156
155
  sourceUri: string;
157
156
  sourceTitle?: string;
157
+ /** Collection of the linking document. */
158
+ sourceCollection?: string;
158
159
  linkText?: string;
159
160
  startLine: number;
160
161
  startCol: number;
@@ -233,96 +234,13 @@ export type SimilarResult =
233
234
  | { success: true; data: SimilarResponse }
234
235
  | { success: false; error: string; isValidation?: boolean };
235
236
 
236
- // ─────────────────────────────────────────────────────────────────────────────
237
- // Helper: Build resolution indexes (cached per collection)
238
- // ─────────────────────────────────────────────────────────────────────────────
239
-
240
- interface ResolutionIndexes {
241
- // Map: normalized wiki name -> DocumentRow
242
- wikiIndex: Map<string, DocumentRow>;
243
- // Map: relPath -> DocumentRow
244
- pathIndex: Map<string, DocumentRow>;
245
- }
246
-
247
- /** Normalize markdown link path for matching (strip ./, collapse ..) */
248
- function normalizeMarkdownPath(path: string): string {
249
- // Strip leading ./
250
- let normalized = path.replace(/^\.\//, "");
251
- // Collapse simple parent refs (a/b/../c -> a/c)
252
- while (normalized.includes("/../")) {
253
- normalized = normalized.replace(/[^/]+\/\.\.\//, "");
254
- }
255
- return normalized;
256
- }
257
-
258
- async function buildResolutionIndexes(
259
- store: StorePort,
260
- collection: string,
261
- cache: Map<string, ResolutionIndexes>
262
- ): Promise<ResolutionIndexes> {
263
- const cached = cache.get(collection);
264
- if (cached) {
265
- return cached;
266
- }
267
-
268
- const indexes: ResolutionIndexes = {
269
- wikiIndex: new Map(),
270
- pathIndex: new Map(),
271
- };
272
-
273
- const docsResult = await store.listDocuments(collection);
274
- if (!docsResult.ok) {
275
- // Collection may not exist or store error - return empty indexes
276
- // Links to this collection will show as unresolved
277
- cache.set(collection, indexes);
278
- return indexes;
279
- }
280
-
281
- for (const d of docsResult.value) {
282
- if (!d.active) continue;
283
-
284
- // Index by relPath for markdown links (exact match)
285
- indexes.pathIndex.set(d.relPath, d);
286
-
287
- // Also index by normalized path (without ./) for common variants
288
- const normalizedPath = normalizeMarkdownPath(d.relPath);
289
- if (
290
- normalizedPath !== d.relPath &&
291
- !indexes.pathIndex.has(normalizedPath)
292
- ) {
293
- indexes.pathIndex.set(normalizedPath, d);
294
- }
295
-
296
- // Index by normalized title for wiki links
297
- if (d.title) {
298
- const wikiKey = normalizeWikiName(d.title);
299
- indexes.wikiIndex.set(wikiKey, d);
300
- }
301
-
302
- // Also index by filename stem as fallback for wiki links
303
- const stem = basename(d.relPath).replace(/\.[^.]+$/, "");
304
- if (stem) {
305
- const stemKey = normalizeWikiName(stem);
306
- // Don't overwrite title match
307
- if (!indexes.wikiIndex.has(stemKey)) {
308
- indexes.wikiIndex.set(stemKey, d);
309
- }
310
- }
311
- }
312
-
313
- cache.set(collection, indexes);
314
- return indexes;
315
- }
316
-
317
237
  // ─────────────────────────────────────────────────────────────────────────────
318
238
  // Helper: Map DocLinkRow to output format (avoids null leakage)
319
239
  // ─────────────────────────────────────────────────────────────────────────────
320
240
 
321
241
  function mapLinkToOutput(
322
242
  link: DocLinkRow,
323
- resolved: boolean,
324
- resolvedDocid?: string,
325
- resolvedUri?: string
243
+ resolved: { docid: string; uri: string; collection?: string } | null
326
244
  ): LinkWithResolution {
327
245
  return {
328
246
  targetRef: link.targetRef,
@@ -336,9 +254,12 @@ function mapLinkToOutput(
336
254
  startCol: link.startCol,
337
255
  endLine: link.endLine,
338
256
  endCol: link.endCol,
339
- resolved,
340
- ...(resolvedDocid && { resolvedDocid }),
341
- ...(resolvedUri && { resolvedUri }),
257
+ resolved: resolved !== null,
258
+ ...(resolved && {
259
+ resolvedDocid: resolved.docid,
260
+ resolvedUri: resolved.uri,
261
+ ...(resolved.collection && { resolvedCollection: resolved.collection }),
262
+ }),
342
263
  };
343
264
  }
344
265
 
@@ -443,48 +364,29 @@ export async function linksList(
443
364
  return a.startCol - b.startCol;
444
365
  });
445
366
 
446
- // Build resolution indexes (cached per collection)
447
- const indexCache = new Map<string, ResolutionIndexes>();
448
- const linksWithResolution: LinkWithResolution[] = [];
449
-
450
- for (const link of links) {
451
- let resolvedDoc: DocumentRow | undefined;
452
-
453
- // Determine target collection (explicit or same as source)
454
- const targetCollection = link.targetCollection ?? doc.collection;
455
-
456
- // Get or build index for target collection
457
- const indexes = await buildResolutionIndexes(
458
- store,
459
- targetCollection,
460
- indexCache
461
- );
462
-
463
- // Safe fallback for targetRefNorm
464
- const targetNorm = link.targetRefNorm || link.targetRef;
465
-
466
- if (link.linkType === "wiki") {
467
- // Wiki links: match by normalized title or filename
468
- const wikiKey = normalizeWikiName(targetNorm);
469
- resolvedDoc = indexes.wikiIndex.get(wikiKey);
470
- } else {
471
- // Markdown links: match by relPath (try exact, then normalized)
472
- resolvedDoc = indexes.pathIndex.get(targetNorm);
473
- if (!resolvedDoc) {
474
- const normalizedTarget = normalizeMarkdownPath(targetNorm);
475
- resolvedDoc = indexes.pathIndex.get(normalizedTarget);
476
- }
477
- }
478
-
479
- linksWithResolution.push(
480
- mapLinkToOutput(
481
- link,
482
- !!resolvedDoc,
483
- resolvedDoc?.docid,
484
- resolvedDoc?.uri
485
- )
486
- );
367
+ // Resolve with the shared link resolver (workspace-aware for plain wiki
368
+ // links whose source sits in a link workspace).
369
+ const resolvedResult = await store.resolveLinks(
370
+ links.map((link) => ({
371
+ targetRefNorm: link.targetRefNorm || link.targetRef,
372
+ targetCollection: link.targetCollection ?? doc.collection,
373
+ linkType: link.linkType,
374
+ source: {
375
+ collection: doc.collection,
376
+ relPath: doc.relPath,
377
+ explicit: Boolean(link.targetCollection),
378
+ },
379
+ }))
380
+ );
381
+ if (!resolvedResult.ok) {
382
+ return { success: false, error: resolvedResult.error.message };
487
383
  }
384
+ const linksWithResolution: LinkWithResolution[] = links.map(
385
+ (link, index) => {
386
+ const resolved = resolvedResult.value[index] ?? null;
387
+ return mapLinkToOutput(link, resolved);
388
+ }
389
+ );
488
390
 
489
391
  const resolvedCount = linksWithResolution.filter((l) => l.resolved).length;
490
392
 
@@ -606,6 +508,7 @@ export async function backlinks(
606
508
  sourceDocid: bl.sourceDocid,
607
509
  sourceUri: bl.sourceDocUri,
608
510
  ...(bl.sourceDocTitle && { sourceTitle: bl.sourceDocTitle }),
511
+ ...(bl.sourceCollection && { sourceCollection: bl.sourceCollection }),
609
512
  ...(bl.linkText && { linkText: bl.linkText }),
610
513
  startLine: bl.startLine,
611
514
  startCol: bl.startCol,
@@ -178,6 +178,13 @@ export function formatSyncResultLines(
178
178
  `Rechunked ${syncResult.rechunkedMirrors} cached mirrors. Run gno embed if embedding was skipped.`
179
179
  );
180
180
  }
181
+ if (syncResult.graphRebuild) {
182
+ lines.push(
183
+ syncResult.graphRebuild === "resolver-upgrade"
184
+ ? "Link graph rebuilt: link resolution was upgraded."
185
+ : "Link graph rebuilt: collection settings or link workspace membership changed."
186
+ );
187
+ }
181
188
 
182
189
  for (const c of syncResult.collections) {
183
190
  lines.push(`${c.collection}:`);
@@ -21,6 +21,7 @@ import {
21
21
  import { isConnectorActivationComplete } from "../../core/activation-connector-health";
22
22
  import { buildActivationStatus } from "../../core/activation-status";
23
23
  import { formatChunkingStatus } from "../../core/chunking-status";
24
+ import { formatLinkWorkspace } from "../../core/link-workspace";
24
25
  import {
25
26
  buildMemoryStatus,
26
27
  formatMemoryStatusLines,
@@ -140,6 +141,8 @@ function formatTerminal(
140
141
  ` ${c.name}: ${c.activeDocuments} docs, ${c.totalChunks} chunks` +
141
142
  (c.embeddedChunks > 0 ? `, ${c.embeddedChunks} embedded` : "")
142
143
  );
144
+ const workspace = formatLinkWorkspace(c);
145
+ if (workspace) lines.push(` Link workspace: ${workspace}`);
143
146
  }
144
147
  }
145
148
 
@@ -395,6 +398,8 @@ export function formatStatus(
395
398
  collections: s.collections.map((c) => ({
396
399
  name: c.name,
397
400
  path: c.path,
401
+ ...(c.workspaceRoot ? { workspaceRoot: c.workspaceRoot } : {}),
402
+ workspaceSource: c.workspaceSource ?? "none",
398
403
  documentCount: c.activeDocuments,
399
404
  chunkCount: c.totalChunks,
400
405
  embeddedCount: c.embeddedChunks,
@@ -1694,7 +1694,10 @@ function wireOnboardingCommands(program: Command): void {
1694
1694
  collectRepeatableValue,
1695
1695
  []
1696
1696
  )
1697
- .option("--max-findings <count>", "maximum returned findings", Number)
1697
+ .option(
1698
+ "--max-findings <count>",
1699
+ "maximum returned findings (1-100000, or all)"
1700
+ )
1698
1701
  .option("--max-age-days <days>", "explicit age review signal", Number)
1699
1702
  .option(
1700
1703
  "--orphan-root <uri>",
@@ -1736,7 +1739,7 @@ function wireOnboardingCommands(program: Command): void {
1736
1739
  collections: cmdOpts.collection as string[],
1737
1740
  paths: cmdOpts.path as string[],
1738
1741
  tags: cmdOpts.tag as string[],
1739
- maxFindings: cmdOpts.maxFindings as number | undefined,
1742
+ maxFindings: cmdOpts.maxFindings as string | undefined,
1740
1743
  maxAgeDays: cmdOpts.maxAgeDays as number | undefined,
1741
1744
  orphanRoots: cmdOpts.orphanRoot as string[],
1742
1745
  orphanIgnorePrefixes: cmdOpts.orphanIgnorePrefix as string[],
@@ -4749,6 +4752,12 @@ function wireKnowledgeDeltaCommands(program: Command): void {
4749
4752
  program
4750
4753
  .command("impact <doc>")
4751
4754
  .description("Find bounded inbound knowledge dependencies")
4755
+ .option(
4756
+ "-c, --collection <name>",
4757
+ "only traverse these collections (repeatable; default all)",
4758
+ collectRepeatableValue,
4759
+ []
4760
+ )
4752
4761
  .option("--max-depth <n>", "maximum dependency depth", "3")
4753
4762
  .option("--max-nodes <n>", "maximum returned nodes", "100")
4754
4763
  .option("--max-edges <n>", "maximum traversed evidence edges", "250")
@@ -4764,6 +4773,7 @@ function wireKnowledgeDeltaCommands(program: Command): void {
4764
4773
  const result = await impact(
4765
4774
  doc,
4766
4775
  {
4776
+ collections: cmdOpts.collection as string[],
4767
4777
  maxDepth: parsePositiveInt("max-depth", cmdOpts.maxDepth),
4768
4778
  maxNodes: parsePositiveInt("max-nodes", cmdOpts.maxNodes),
4769
4779
  maxEdges: parsePositiveInt("max-edges", cmdOpts.maxEdges),
@@ -7,6 +7,10 @@
7
7
 
8
8
  import type { ZodError } from "zod";
9
9
 
10
+ import {
11
+ validateWorkspaceRootSetting,
12
+ workspaceRootSettingMessage,
13
+ } from "../core/link-workspace";
10
14
  import {
11
15
  normalizeConfigContentTypes,
12
16
  type ConfigWarning,
@@ -132,6 +136,18 @@ export async function loadConfigFromPath(
132
136
  };
133
137
  }
134
138
 
139
+ const workspaceIssues = validateCollectionWorkspaceRoots(result.data);
140
+ if (workspaceIssues.length > 0) {
141
+ return {
142
+ ok: false,
143
+ error: {
144
+ code: "VALIDATION_ERROR",
145
+ message: `Config validation failed: ${workspaceIssues.map((issue) => issue.message).join("; ")}`,
146
+ issues: workspaceIssues,
147
+ },
148
+ };
149
+ }
150
+
135
151
  const normalized = normalizeConfigContentTypes(result.data);
136
152
  return {
137
153
  ok: true,
@@ -140,6 +156,33 @@ export async function loadConfigFromPath(
140
156
  };
141
157
  }
142
158
 
159
+ /**
160
+ * Validate explicit `workspaceRoot` settings against the filesystem: each must
161
+ * be absolute, exist, and contain its collection root. Messages name the
162
+ * collection so the error is actionable.
163
+ */
164
+ export function validateCollectionWorkspaceRoots(
165
+ config: Config
166
+ ): ZodError["issues"] {
167
+ const issues: ZodError["issues"] = [];
168
+ for (const [index, collection] of config.collections.entries()) {
169
+ if (typeof collection.workspaceRoot !== "string") continue;
170
+ const error = validateWorkspaceRootSetting(
171
+ collection.path,
172
+ collection.workspaceRoot
173
+ );
174
+ if (error) {
175
+ issues.push({
176
+ code: "custom",
177
+ message: workspaceRootSettingMessage(collection.name, error),
178
+ path: ["collections", index, "workspaceRoot"],
179
+ input: collection.workspaceRoot,
180
+ });
181
+ }
182
+ }
183
+ return issues;
184
+ }
185
+
143
186
  /**
144
187
  * Load config, returning null if not found (convenience wrapper).
145
188
  * Throws on parse/validation errors.
@@ -166,6 +166,14 @@ export const CollectionSchema = z.object({
166
166
  */
167
167
  sourceAvailability: SourceAvailabilitySchema.optional(),
168
168
 
169
+ /**
170
+ * Link workspace for plain wiki links. Omitted: auto-detect the nearest
171
+ * `.obsidian/` ancestor-or-self. An absolute path joins the collection to
172
+ * that workspace root (it must contain the collection root); `false` keeps
173
+ * the collection's links collection-scoped.
174
+ */
175
+ workspaceRoot: z.union([z.string().min(1), z.literal(false)]).optional(),
176
+
169
177
  /**
170
178
  * Declares the collection as a GNO-managed memory substrate: `remember`
171
179
  * writes fact files here and refuses every collection without the flag.
@@ -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 = 1000;
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: number;
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?: number;
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(0, FRESHNESS_AUDIT_MAX_FINDINGS_PER_RULE),
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,