@bookedsolid/rea 0.46.0 → 0.48.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.
@@ -165,6 +165,107 @@ function alignToBucket(epochMs, bucketSeconds) {
165
165
  const bucketMs = bucketSeconds * 1000;
166
166
  return Math.floor(epochMs / bucketMs) * bucketMs;
167
167
  }
168
+ /**
169
+ * Format a duration in seconds as the coarsest single-unit compact
170
+ * string that round-trips through `parseDurationSeconds`. Mirrors the
171
+ * shape `--since` accepts (`s`/`m`/`h`/`d`/`w`).
172
+ *
173
+ * 0.47.0 charter item 1: powers the helpful-error suggestion + the
174
+ * auto-clamp `clamped_since` field. The largest-unit pass keeps the
175
+ * suggestion readable — `"21d"` not `"1814400s"`.
176
+ */
177
+ export function formatDurationCompact(seconds) {
178
+ if (!Number.isFinite(seconds) || seconds <= 0)
179
+ return '0s';
180
+ const units = [
181
+ ['w', 60 * 60 * 24 * 7],
182
+ ['d', 60 * 60 * 24],
183
+ ['h', 60 * 60],
184
+ ['m', 60],
185
+ ['s', 1],
186
+ ];
187
+ for (const [unit, factor] of units) {
188
+ if (seconds % factor === 0) {
189
+ return `${String(seconds / factor)}${unit}`;
190
+ }
191
+ }
192
+ return `${String(seconds)}s`;
193
+ }
194
+ /**
195
+ * 0.47.0 charter item 1: build a helpful error message for the
196
+ * MAX_BUCKETS guard. Computes a concrete "use --bucket=X" and
197
+ * "use --since=Y" suggestion based on the actual inputs, so the
198
+ * operator sees the next step inline instead of having to do the
199
+ * division themselves.
200
+ *
201
+ * Strategy:
202
+ * - "Try a wider bucket" — smallest unit from {1h, 4h, 1d, 1w} that
203
+ * brings projected ≤ MAX_BUCKETS. Falls back to a concrete second
204
+ * count if no unit fits (extreme `--since`).
205
+ * - "Try a narrower since" — largest multiple of the requested bucket
206
+ * that fits under MAX_BUCKETS, rendered compactly.
207
+ *
208
+ * The error text always includes the substrings `bucket=`, `since=`,
209
+ * and `Try` so test assertions can pin the shape.
210
+ */
211
+ function bucketOverflowMessage(windowSeconds, bucketSeconds, rawBucket, rawSince, sinceImplicit) {
212
+ const projected = Math.ceil(windowSeconds / bucketSeconds);
213
+ // Candidate wider buckets, in ascending size — pick the first that fits.
214
+ const allCandidates = [
215
+ ['1h', 60 * 60],
216
+ ['4h', 4 * 60 * 60],
217
+ ['1d', 60 * 60 * 24],
218
+ ['1w', 60 * 60 * 24 * 7],
219
+ ];
220
+ const widerCandidates = allCandidates.filter((entry) => entry[1] > bucketSeconds);
221
+ let bucketSuggestion = null;
222
+ let bucketSuggestionCount = null;
223
+ for (const [label, secs] of widerCandidates) {
224
+ // Account for alignment slack: a window of N seconds at bucket
225
+ // size S emits up to `ceil(N/S) + 1` buckets after alignment
226
+ // (lower-edge + upper-edge alignment can each contribute one
227
+ // extra bucket vs the naive division). Codex round-4 P2 (0.47.0):
228
+ // suggesting a bucket where `ceil(N/S) === MAX_BUCKETS` would
229
+ // still re-throw at the post-alignment guard. Use the +1
230
+ // worst-case so the operator's retry actually succeeds.
231
+ const cnt = Math.ceil(windowSeconds / secs) + 1;
232
+ if (cnt <= MAX_BUCKETS) {
233
+ bucketSuggestion = label;
234
+ bucketSuggestionCount = cnt;
235
+ break;
236
+ }
237
+ }
238
+ // Largest --since that fits at the requested bucket size, rendered
239
+ // compactly. Subtract one bucket so the suggested value survives the
240
+ // post-alignment guard regardless of where `now` falls on the
241
+ // bucket lattice — codex round-1 P2 (0.47.0): a window of exactly
242
+ // `MAX_BUCKETS * bucketSeconds` aligns to MAX_BUCKETS+1 buckets in
243
+ // the common case (`now` not already on a boundary), so the
244
+ // operator pasting our suggestion would hit the same error they
245
+ // just got. `(MAX_BUCKETS - 1) * bucketSeconds` leaves alignment
246
+ // slack on either edge.
247
+ const fittingSinceSeconds = (MAX_BUCKETS - 1) * bucketSeconds;
248
+ const sinceSuggestion = formatDurationCompact(fittingSinceSeconds);
249
+ const sinceSuggestionCount = Math.floor(fittingSinceSeconds / bucketSeconds);
250
+ const parts = [];
251
+ const reason = sinceImplicit
252
+ ? `--since not specified; defaulting to full audit log (${rawSince}) at --bucket=${rawBucket} = ${String(projected)} buckets exceeds MAX_BUCKETS=${String(MAX_BUCKETS)}.`
253
+ : `--bucket=${rawBucket} × --since=${rawSince} = ${String(projected)} buckets exceeds MAX_BUCKETS=${String(MAX_BUCKETS)}.`;
254
+ parts.push(reason);
255
+ const suggestions = [];
256
+ if (bucketSuggestion !== null && bucketSuggestionCount !== null) {
257
+ suggestions.push(`--bucket=${bucketSuggestion} (${String(bucketSuggestionCount)} buckets)`);
258
+ }
259
+ suggestions.push(`--since=${sinceSuggestion} (${String(sinceSuggestionCount)} buckets)`);
260
+ parts.push(`Try ${suggestions.join(' or ')}.`);
261
+ return parts.join(' ');
262
+ }
263
+ // 0.47.0 round-3 (codex P1+P2): the pre-scan `measureLogBounds`
264
+ // helper was removed. The all-time auto-clamp now runs as a
265
+ // post-scan recovery against observed bucket keys (the only data
266
+ // that can't be fooled by caller-supplied timestamps or empty
267
+ // current-file edge cases). See the "post-scan auto-clamp" branch
268
+ // inside `computeAuditTimeline`.
168
269
  /**
169
270
  * Compute the bucketed timeline. Pure (read-only). Throws
170
271
  * `AuditTimelineOptionError` on bad `--since` / `--bucket`; throws on
@@ -181,6 +282,7 @@ export async function computeAuditTimeline(options = {}) {
181
282
  let windowSeconds = null;
182
283
  let windowStart = null;
183
284
  let windowEnd = null;
285
+ let clampedSince = null;
184
286
  if (options.since !== undefined && options.since.length > 0) {
185
287
  try {
186
288
  windowSeconds = parseDurationSeconds(options.since);
@@ -194,16 +296,28 @@ export async function computeAuditTimeline(options = {}) {
194
296
  windowEnd = now;
195
297
  windowStart = new Date(now.getTime() - windowSeconds * 1000);
196
298
  // Guard against runaway bucket counts under a wide --since with a
197
- // tiny --bucket. A simple ceiling is cheaper than rendering 100k
198
- // empty rows and gives the operator a clear remediation path.
299
+ // tiny --bucket. 0.47.0 charter item 1: deliver a concrete-suggestion
300
+ // error rather than the prior "use a larger --bucket or narrower
301
+ // --since" generic line — the operator should see the next step
302
+ // inline.
199
303
  const projected = Math.ceil(windowSeconds / bucketSeconds);
200
304
  if (projected > MAX_BUCKETS) {
201
- throw new AuditTimelineOptionError(`--bucket=${bucketRaw} with --since=${options.since} would emit ` +
202
- `${String(projected)} buckets (max ${String(MAX_BUCKETS)}). ` +
203
- `Use a larger --bucket or narrower --since.`);
305
+ throw new AuditTimelineOptionError(bucketOverflowMessage(windowSeconds, bucketSeconds, bucketRaw, options.since, false));
204
306
  }
205
307
  }
206
308
  const files = await resolveTimelineFileWalk(baseDir);
309
+ // 0.47.0 charter item 2: auto-clamp on long-history repos is
310
+ // implemented as a POST-SCAN recovery, not a pre-scan guess. Codex
311
+ // round-3 P1: a pre-scan clamp based on `latestMs - earliestMs`
312
+ // span is wrong for the no-`--since` path, which normally emits
313
+ // ONLY event-bearing buckets. A sparse long-lived repo (two
314
+ // records a year apart at `--bucket=1h`) has a 365d span but a
315
+ // 2-bucket result — pre-clamping would incorrectly drop one of
316
+ // those events. We must let the scan see what bucket count the
317
+ // actual records produce, then clamp only if that exceeds
318
+ // MAX_BUCKETS. The clamp anchor uses the busiest in-data range
319
+ // (max observed timestamp + alignment buffer), so we never have
320
+ // to guess from disk metadata. See the post-scan branch below.
207
321
  // Bucket key is the aligned epoch-ms boundary; value is the count.
208
322
  const buckets = new Map();
209
323
  let totalEvents = 0;
@@ -275,8 +389,19 @@ export async function computeAuditTimeline(options = {}) {
275
389
  // the renderer.
276
390
  const emit = Math.floor((endKey - startKey) / stepMs) + 1;
277
391
  if (emit > MAX_BUCKETS) {
278
- throw new AuditTimelineOptionError(`--bucket / --since would emit ${String(emit)} buckets after alignment ` +
279
- `(max ${String(MAX_BUCKETS)}). Use a larger --bucket or narrower --since.`);
392
+ // Post-alignment overflow is a near-miss vs the pre-scan
393
+ // projection check (alignment can add at most one bucket on
394
+ // either edge). Codex round-6 P3 (0.47.0): the helpful-error
395
+ // builder previously recomputed the projected count from
396
+ // `windowSeconds` and could end up saying "= 2000 buckets
397
+ // exceeds MAX_BUCKETS=2000" when the actual post-alignment
398
+ // count was 2001. Inflate `windowSeconds` by enough to make
399
+ // the projection match the actual aligned emit count — that
400
+ // way the operator sees a consistent number, and the
401
+ // remediation suggestions inherit the same +1 bias.
402
+ const effectiveSince = clampedSince ?? (options.since ?? formatDurationCompact(windowSeconds ?? 0));
403
+ const reportedSeconds = Math.max(windowSeconds ?? 0, (emit - 1) * bucketSeconds + 1);
404
+ throw new AuditTimelineOptionError(bucketOverflowMessage(reportedSeconds, bucketSeconds, bucketRaw, effectiveSince, options.since === undefined || options.since.length === 0));
280
405
  }
281
406
  for (let k = startKey; k <= endKey; k += stepMs) {
282
407
  result.push({
@@ -288,20 +413,107 @@ export async function computeAuditTimeline(options = {}) {
288
413
  }
289
414
  else if (buckets.size > 0) {
290
415
  const keys = Array.from(buckets.keys()).sort((a, b) => a - b);
416
+ const stepMs = bucketSeconds * 1000;
291
417
  if (keys.length > MAX_BUCKETS) {
292
- // Without --since the operator hasn't asked for a fixed shape,
293
- // but a runaway result still needs a guard. Refuse rather than
294
- // truncate silently.
295
- throw new AuditTimelineOptionError(`Without --since, the audit log spans ${String(keys.length)} ${bucketRaw} buckets ` +
296
- `(max ${String(MAX_BUCKETS)}). Pass --since to scope, or use a larger --bucket.`);
418
+ // 0.47.0 charter item 2 (post-scan auto-clamp): the actual
419
+ // observed bucket count exceeds MAX_BUCKETS. The no-`--since`
420
+ // path emits only event-bearing buckets (not a zero-filled
421
+ // lattice), so clamping by a contiguous time window would
422
+ // discard most of the newest activity on SPARSE logs (codex
423
+ // round-5 P1: e.g. one record/day across 2001 days at
424
+ // --bucket=1h, a time-window clamp keeps ~83 buckets — the
425
+ // newest-2000-keys clamp keeps all 2000 newest event-bearing
426
+ // buckets). The right shape: slice the keys array to the newest
427
+ // MAX_BUCKETS entries directly.
428
+ //
429
+ // Codex round-3 P1: clamp on OBSERVED data, not guessed span.
430
+ // Codex round-3 P2: observed-max timestamp sidesteps both the
431
+ // empty-current-file case and the out-of-order-timestamp case.
432
+ // Codex round-4 P2: full MAX_BUCKETS budget (the +1 alignment
433
+ // slack doesn't apply when cherry-picking from observed keys).
434
+ const fittingBuckets = MAX_BUCKETS;
435
+ const kept = keys.slice(keys.length - fittingBuckets);
436
+ const startKey = kept[0];
437
+ const anchorKey = kept[kept.length - 1];
438
+ // Determine whether the kept buckets form a CONTIGUOUS lattice
439
+ // (every bucket between startKey and anchorKey is present) or a
440
+ // SPARSE one (gaps inside). The no-`--since` path emits only
441
+ // event-bearing buckets, so a sparse clamp is the common case.
442
+ // Codex round-6 P2 (0.47.0): if we filled `window.start/end/
443
+ // seconds` with the bucket span of a sparse clamp, the JSON
444
+ // would lie to dashboard consumers — `total_events / window.
445
+ // seconds` would derive a wildly-wrong rate, and the operator
446
+ // could NOT reproduce the view by re-running with
447
+ // `--since=<clamped_since>` (it would either error or include
448
+ // far more buckets). Treat the two shapes distinctly:
449
+ // - contiguous: report the time-window shape (operator can
450
+ // paste `--since=<clamped_since>` to reproduce).
451
+ // - sparse: leave `window` null (no reproducible duration
452
+ // exists), but still report `clamped_since` so callers
453
+ // know the kept-bucket count was budgeted.
454
+ const expectedContiguousCount = Math.floor((anchorKey - startKey) / stepMs) + 1;
455
+ const isContiguous = expectedContiguousCount === kept.length;
456
+ // 0.47.0 charter item 2: `clamped_since` is ALWAYS a duration
457
+ // string (per the charter `clamped_since: "<DUR>"` contract).
458
+ // It carries the approximate time span the rendered window
459
+ // covers — informative, not necessarily paste-back-safe.
460
+ //
461
+ // Codex round-8 P2 (0.47.0): on stale logs (latest record
462
+ // hours/days ago) `--since=<DUR>` would NOT reproduce the
463
+ // returned data because `--since` always anchors on `now`,
464
+ // not on the audit's latest record. The reproducibility
465
+ // promise we entertained briefly across rounds 7-8 is
466
+ // inherently unsound — `--since` semantics fix one side of
467
+ // the window (now), so any clamp anchored at an older
468
+ // timestamp can't round-trip through it. The `note:` line in
469
+ // human output now describes the field as APPROXIMATE
470
+ // rather than pasteable.
471
+ //
472
+ // Codex round-8 P2 (0.47.0): the sparse-clamp branch
473
+ // previously emitted `"newest 2000 buckets"` for clarity, but
474
+ // that broke the documented `<DUR>` shape — dashboards trying
475
+ // to parse it as a duration would fail only on sparse logs.
476
+ // Both branches now emit a duration string; the human note
477
+ // adds the "sparse" qualifier so operators understand what
478
+ // they're looking at.
479
+ const spanSeconds = Math.max(bucketSeconds, Math.ceil((anchorKey - startKey) / 1000) + bucketSeconds);
480
+ clampedSince = formatDurationCompact(spanSeconds);
481
+ // For contiguous clamps, also fill window.* so consumers can
482
+ // compute rates against a real duration. For sparse clamps,
483
+ // window.* stays null — `total_events / window.seconds` would
484
+ // be meaningless when the kept buckets don't form a contiguous
485
+ // lattice.
486
+ if (isContiguous) {
487
+ windowSeconds = spanSeconds;
488
+ windowEnd = new Date(anchorKey + stepMs);
489
+ windowStart = new Date(startKey);
490
+ }
491
+ // Track the contiguous-vs-sparse shape so the renderer can
492
+ // surface the right note.
493
+ void expectedContiguousCount; // used above via isContiguous
494
+ // Emit each kept bucket. total_events under the post-scan clamp
495
+ // path counts only what the rendered buckets contain — older
496
+ // sliced-out buckets contribute nothing to the report.
497
+ let inWindow = 0;
498
+ for (const k of kept) {
499
+ const cnt = buckets.get(k) ?? 0;
500
+ result.push({
501
+ start: new Date(k).toISOString(),
502
+ end: new Date(k + stepMs).toISOString(),
503
+ count: cnt,
504
+ });
505
+ inWindow += cnt;
506
+ }
507
+ totalEvents = inWindow;
297
508
  }
298
- const stepMs = bucketSeconds * 1000;
299
- for (const k of keys) {
300
- result.push({
301
- start: new Date(k).toISOString(),
302
- end: new Date(k + stepMs).toISOString(),
303
- count: buckets.get(k) ?? 0,
304
- });
509
+ else {
510
+ for (const k of keys) {
511
+ result.push({
512
+ start: new Date(k).toISOString(),
513
+ end: new Date(k + stepMs).toISOString(),
514
+ count: buckets.get(k) ?? 0,
515
+ });
516
+ }
305
517
  }
306
518
  }
307
519
  // Peak index. -1 when no events at all (every bucket is 0 or the
@@ -329,6 +541,7 @@ export async function computeAuditTimeline(options = {}) {
329
541
  total_events: totalEvents,
330
542
  peak_index: peakIndex,
331
543
  files_scanned: filesScanned,
544
+ clamped_since: clampedSince,
332
545
  };
333
546
  }
334
547
  /**
@@ -381,10 +594,39 @@ function formatWindowLabel(seconds) {
381
594
  */
382
595
  export function renderAuditTimeline(result) {
383
596
  const lines = [];
384
- const windowLabel = formatWindowLabel(result.window.seconds);
597
+ // Codex round-9 P2 (0.47.0): when auto-clamp fires, the regular
598
+ // `last <DUR>` header (derived from `window.seconds`) is wrong —
599
+ // contiguous stale logs would print "last 1d" even though the
600
+ // newest event was days ago, and sparse clamps would fall back
601
+ // to "all time". Use a clamp-aware header that describes the
602
+ // returned shape instead.
385
603
  const cadenceLabel = bucketLabel(result.bucket.seconds, result.bucket.raw);
604
+ let windowLabel;
605
+ if (result.clamped_since !== null) {
606
+ windowLabel = `clamped to ~${result.clamped_since} of newest activity`;
607
+ }
608
+ else {
609
+ windowLabel = formatWindowLabel(result.window.seconds);
610
+ }
386
611
  lines.push(`rea audit timeline (${windowLabel}, ${cadenceLabel})`);
387
612
  lines.push('─'.repeat(40));
613
+ // 0.47.0 charter item 2: surface the auto-clamp inline. Operators
614
+ // scanning the rendered output should immediately see that the
615
+ // window they got isn't the full audit log. Codex round-8 P2
616
+ // (0.47.0): `clamped_since` is informational, not reproducible —
617
+ // `--since=DUR` anchors at `now`, so a clamp anchored at an older
618
+ // record can't round-trip. Codex round-9 P3 (0.47.0): only a
619
+ // WIDER `--bucket` actually changes the result — pinning the same
620
+ // bucket would just re-trigger the clamp. The remediation
621
+ // suggestion names "wider" explicitly to avoid sending operators
622
+ // down a no-op retry path.
623
+ if (result.clamped_since !== null) {
624
+ lines.push(`note: --since not specified; auto-clamped to newest ${String(MAX_BUCKETS)} buckets ` +
625
+ `(~${result.clamped_since} span at --bucket=${result.bucket.raw}). ` +
626
+ `Pass --since=DUR to anchor at now, or rerun with a WIDER --bucket ` +
627
+ `(current ${result.bucket.raw}) to fit the full log.`);
628
+ lines.push('');
629
+ }
388
630
  // Codex round-1 P2 (0.46.0): the zero-events case has two distinct
389
631
  // shapes and the renderer must NOT collapse them.
390
632
  //
@@ -0,0 +1,154 @@
1
+ /**
2
+ * `rea audit top-blocks [--since=DUR] [--limit=N] [--json]` — 0.47.0
3
+ * charter item 3.
4
+ *
5
+ * Surface the most recent refusal events from the audit log. Designed
6
+ * for the question "why was that refused?" — operators see the latest
7
+ * blocks at a glance with enough context (timestamp, tool, reason) to
8
+ * grep the offending Bash/Edit/Write call site or fix the policy that
9
+ * tripped the gate.
10
+ *
11
+ * A "refusal" in the rea audit schema is any record whose
12
+ * `InvocationStatus` is NOT `Allowed` — that's `Denied` (policy
13
+ * refused) OR `Error` (middleware exception or downstream failure).
14
+ * Both are interesting to operators debugging "why didn't this run".
15
+ *
16
+ * # Walk scope
17
+ *
18
+ * Mirrors `audit summary` / `audit by-tool` / `audit timeline`: the
19
+ * current `.rea/audit.jsonl` PLUS every rotated `audit-…jsonl` segment
20
+ * is walked regardless of `--since` (the per-record timestamp filter
21
+ * inside the main loop decides what counts). Rotated filename stamps
22
+ * mark the rotation INSTANT, not the earliest record contained
23
+ * (0.41.0 round-3 P2 / 0.42.0 charter item 3) — pruning by filename
24
+ * would silently drop in-window records from conservatively-rotated
25
+ * logs. Walking every segment is the only sound shape.
26
+ *
27
+ * # Output (default)
28
+ *
29
+ * rea audit top-blocks (last 24h, limit 20)
30
+ * ─────────────────────────────────────────
31
+ * a1b2c3d4 2026-05-17T12:34:56.789Z Bash rm -rf bypass attempted (...)
32
+ * deadbeef 2026-05-17T11:20:01.123Z Write blocked-path .env write
33
+ * …
34
+ * total: 4 refusal events in window
35
+ * files scanned: 2
36
+ *
37
+ * # JSON output
38
+ *
39
+ * {
40
+ * "schema_version": 1,
41
+ * "since": "24h",
42
+ * "limit": 20,
43
+ * "window": { "seconds": 86400, "start": "...", "end": "..." },
44
+ * "total_matched": 4,
45
+ * "events": [
46
+ * { "hash": "a1b2c3d4...", "timestamp": "...", "tool": "Bash",
47
+ * "status": "denied", "reason": "rm -rf bypass attempted (...)" },
48
+ * …
49
+ * ],
50
+ * "files_scanned": ["/abs/path/.rea/audit.jsonl"]
51
+ * }
52
+ *
53
+ * `total_matched` is the pre-limit count so dashboards can show "20 of
54
+ * 47 refusals in window". `events` is sorted newest-first and capped at
55
+ * `limit`.
56
+ */
57
+ import type { Command } from 'commander';
58
+ export declare const AUDIT_TOP_BLOCKS_SCHEMA_VERSION = 1;
59
+ /** Default `--limit` value. 20 fits a debugging session's eyeballable window. */
60
+ export declare const DEFAULT_LIMIT = 20;
61
+ /**
62
+ * Hard ceiling on `--limit`. Refusal events are typically a small slice
63
+ * of total traffic, but a 1000 cap keeps the renderer / JSON output
64
+ * bounded under a runaway misconfiguration that's denying everything.
65
+ */
66
+ export declare const MAX_LIMIT = 1000;
67
+ /**
68
+ * Thrown when `--limit` is outside [1, MAX_LIMIT] or `--since` fails to
69
+ * parse. The commander wrapper catches and exits 1. Distinct from
70
+ * `AuditByToolOptionError` / `AuditTimelineOptionError` so the
71
+ * caller-facing message names the right flag.
72
+ */
73
+ export declare class AuditTopBlocksOptionError extends Error {
74
+ constructor(message: string);
75
+ }
76
+ export interface AuditTopBlocksEvent {
77
+ /** Full sha256 hash from the audit record — stable cross-tool ID. */
78
+ hash: string;
79
+ /** Raw ISO-8601 timestamp from the record. */
80
+ timestamp: string;
81
+ /** Tool name as recorded; `(unknown)` for missing/empty. */
82
+ tool: string;
83
+ /** Raw `status` value (`denied` / `error`). */
84
+ status: string;
85
+ /**
86
+ * Best-effort human-readable reason. Sourced from the record's
87
+ * `error` field when present, else a synthesized "<status>: <tool>"
88
+ * fallback so the row carries SOMETHING informative even when the
89
+ * middleware didn't attach an error message.
90
+ */
91
+ reason: string;
92
+ /** Session ID from the record; useful for cross-referencing. */
93
+ session_id: string;
94
+ }
95
+ export interface AuditTopBlocksResult {
96
+ schema_version: typeof AUDIT_TOP_BLOCKS_SCHEMA_VERSION;
97
+ /** Raw `--since` value as passed by the caller (`null` when omitted). */
98
+ since: string | null;
99
+ /** Resolved `--limit` actually used. */
100
+ limit: number;
101
+ window: {
102
+ seconds: number | null;
103
+ start: string | null;
104
+ end: string | null;
105
+ };
106
+ /** Pre-limit count of refusal records in window. */
107
+ total_matched: number;
108
+ /** Sorted newest-first; capped at `limit`. */
109
+ events: AuditTopBlocksEvent[];
110
+ /** Absolute paths of audit files actually read. */
111
+ files_scanned: string[];
112
+ }
113
+ export interface ComputeAuditTopBlocksOptions {
114
+ /** Override CWD. Tests set this; production uses `process.cwd()`. */
115
+ baseDir?: string;
116
+ /** Raw `--since` value (e.g. `24h`, `7d`). Parsed via parseDuration. */
117
+ since?: string;
118
+ /** Raw `--limit` value. Default `DEFAULT_LIMIT`. */
119
+ limit?: number;
120
+ /** Test seam — pin "now" for deterministic window calculations. */
121
+ now?: Date;
122
+ }
123
+ /**
124
+ * Compute the top-blocks list. Pure (read-only). Throws
125
+ * `AuditTopBlocksOptionError` on bad `--since` / `--limit`.
126
+ */
127
+ export declare function computeAuditTopBlocks(options?: ComputeAuditTopBlocksOptions): Promise<AuditTopBlocksResult>;
128
+ /**
129
+ * Render the result as a human-readable terminal block. JSON callers
130
+ * bypass this; the rendering is intentionally minimal — a fixed-column
131
+ * table that scans cleanly in a typical terminal.
132
+ */
133
+ export declare function renderAuditTopBlocks(result: AuditTopBlocksResult): string;
134
+ export interface RunAuditTopBlocksOptions {
135
+ since?: string;
136
+ limit?: number;
137
+ json?: boolean;
138
+ /** Test seam — pin "now". */
139
+ now?: Date;
140
+ }
141
+ /** Commander entrypoint. */
142
+ export declare function runAuditTopBlocks(options: RunAuditTopBlocksOptions): Promise<void>;
143
+ /**
144
+ * Strict integer parser for the commander `--limit <n>` option.
145
+ *
146
+ * Mirrors the `parseTopOption` discipline in `audit-by-tool.ts`:
147
+ * reject anything that isn't a bare integer so `Number.parseInt`
148
+ * can't silently truncate (`1.5` → `1`, `10abc` → `10`).
149
+ */
150
+ export declare function parseLimitOption(raw: string): number;
151
+ /**
152
+ * Register `rea audit top-blocks` under the `audit` command group.
153
+ */
154
+ export declare function registerAuditTopBlocksCommand(auditCommand: Command): void;