@akasecurity/ai-tc-claude-code 0.8.1 → 0.9.0-rc1

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.
@@ -491,6 +491,9 @@ var require_ignore = __commonJS({
491
491
  }
492
492
  });
493
493
 
494
+ // src/backfill.ts
495
+ import { fileURLToPath } from "url";
496
+
494
497
  // ../../packages/persistence/src/database.ts
495
498
  import { randomUUID as randomUUID8 } from "crypto";
496
499
  import { existsSync, renameSync, rmSync } from "fs";
@@ -538,6 +541,10 @@ var SQLITE_MIGRATIONS = [
538
541
  {
539
542
  tag: "0009_findings_path_expression_index",
540
543
  sql: "-- Custom migration: partial expression index for the resolver's per-path reads.\n--\n-- openAtRestKeysForPath / resolvedAtRestKeysForPath (and the scanner's tier-3\n-- open-key probe) filter at-rest events by\n-- e.kind = 'code_change' AND json_extract(e.metadata, '$.filePath') = :path\n-- once per changed/deleted file on every scan \u2014 previously a full events scan\n-- per file. json_extract is deterministic, so SQLite allows it in an index;\n-- the WHERE kind = 'code_change' keeps the index to exactly the rows those\n-- queries can match (in-flight events are never path-addressed).\nCREATE INDEX `idx_events_code_change_path` ON `events` (json_extract(`metadata`, '$.filePath')) WHERE `kind` = 'code_change';\n"
544
+ },
545
+ {
546
+ tag: "0010_events_session_expression_index",
547
+ sql: "-- Custom migration: partial expression index for session-scoped finding reads.\n--\n-- sessionFindingsCount, the session-scoped listGroupedFindings paths, and the\n-- insert-time session dedup all filter live-capture events by\n-- json_extract(e.metadata, '$.sessionId') = :sessionId\n-- \u2014 previously a full findings-join scan with a JSON parse per row. The\n-- IS NOT NULL predicate keeps the index to session-stamped events only (an\n-- equality probe implies non-null, so SQLite still uses it).\nCREATE INDEX `idx_events_session_id` ON `events` (json_extract(`metadata`, '$.sessionId')) WHERE json_extract(`metadata`, '$.sessionId') IS NOT NULL;\n"
541
548
  }
542
549
  ];
543
550
 
@@ -640,6 +647,12 @@ var defaultCostModel = {
640
647
  }
641
648
  };
642
649
 
650
+ // ../../packages/schema/src/token/format.ts
651
+ var COMPACT = new Intl.NumberFormat("en-US", {
652
+ notation: "compact",
653
+ maximumFractionDigits: 1
654
+ });
655
+
643
656
  // ../../packages/schema/src/token/token-report.ts
644
657
  var num = (value) => value ?? 0;
645
658
  function costUsageOf(a) {
@@ -15311,6 +15324,12 @@ var FindingInstance = external_exports.object({
15311
15324
  provider: FindingProvider,
15312
15325
  repo: external_exports.string(),
15313
15326
  file: external_exports.string(),
15327
+ // Host tool that produced the scanned text (event metadata's toolName).
15328
+ // Present whenever the capturing hook recorded one — including
15329
+ // file-attributed captures (views prefer `file`); its display value is
15330
+ // the location fallback ("via Bash") when no filePath exists. Absent for
15331
+ // legacy rows and non-tool captures (prompts, worktree scans).
15332
+ toolName: external_exports.string().optional(),
15314
15333
  // Effective action: override.action ?? actionTaken, translated to FindingAction.
15315
15334
  action: FindingAction,
15316
15335
  detectedAt: external_exports.iso.datetime(),
@@ -15367,6 +15386,9 @@ var ListGroupedFindingsQuery = external_exports.object({
15367
15386
  provider: external_exports.array(FindingProvider).optional(),
15368
15387
  action: external_exports.array(FindingAction).optional(),
15369
15388
  q: external_exports.string().optional(),
15389
+ // Scope to findings whose event carries this session id (the Activity page's
15390
+ // session → findings drilldown). Findings without a session never match.
15391
+ sessionId: external_exports.string().optional(),
15370
15392
  groupBy: external_exports.literal("type").optional(),
15371
15393
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15372
15394
  cursor: external_exports.string().optional()
@@ -15378,7 +15400,13 @@ var ListGroupedFindingsResponse = external_exports.object({
15378
15400
  }),
15379
15401
  facets: FindingFacets,
15380
15402
  items: external_exports.array(FindingGroup),
15381
- nextCursor: external_exports.string().nullable()
15403
+ nextCursor: external_exports.string().nullable(),
15404
+ // Present only on session-scoped queries (`sessionId` set): per ruleId, how
15405
+ // many times that rule fired in the session's persisted transcript. Findings
15406
+ // here are deduplicated to unique values while the transcript tally counts
15407
+ // every firing, so the two numbers legitimately differ — this map lets a
15408
+ // session-scoped view show both.
15409
+ sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
15382
15410
  }).meta({ id: "ListGroupedFindingsResponse" });
15383
15411
  var ApplyFindingActionRequest = external_exports.object({
15384
15412
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
@@ -15430,6 +15458,11 @@ var AuditEventType = external_exports.enum([
15430
15458
  "prompt",
15431
15459
  "response",
15432
15460
  "code_change",
15461
+ // The events.kind of a scanned tool call, widened in to keep this a
15462
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15463
+ // 'tool_call' is the reconciler's structural row for every call, while
15464
+ // 'tool_use' exists only where a hook enforced against the arguments.
15465
+ "tool_use",
15433
15466
  // One row per config-inventory scan, hung off the session root. It is the
15434
15467
  // fact the posture inspection findings reference (findings require an
15435
15468
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15784,6 +15817,10 @@ var ListActivitySessionsQuery = external_exports.object({
15784
15817
  from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15785
15818
  /** Upper bound on startedAt; omitted defaults to now. */
15786
15819
  to: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15820
+ /** Exclude zero-activity sessions — roots whose only recorded children are
15821
+ * bookkeeping rows (hooks, config scans), typically background `claude`
15822
+ * launches. Omitted = list everything. `z.stringbool()` per the note above. */
15823
+ excludeEmpty: external_exports.stringbool().optional(),
15787
15824
  /** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
15788
15825
  limit: external_exports.coerce.number().int().min(1).max(100).default(50),
15789
15826
  /** Opaque pagination cursor (most-recent first). */
@@ -15792,7 +15829,11 @@ var ListActivitySessionsQuery = external_exports.object({
15792
15829
  var ListActivitySessionsResponse = external_exports.object({
15793
15830
  items: external_exports.array(ActivitySessionSummary),
15794
15831
  /** `null` once the last page is reached. */
15795
- nextCursor: external_exports.string().nullable()
15832
+ nextCursor: external_exports.string().nullable(),
15833
+ /** Zero-activity sessions matching the query's filters/range (whether or
15834
+ * not `excludeEmpty` dropped them from `items`) — the count a UI toggle
15835
+ * shows when collapsing them. */
15836
+ emptyCount: external_exports.number().int().nonnegative()
15796
15837
  }).meta({ id: "ListActivitySessionsResponse" });
15797
15838
  var ListSessionEventsQuery = external_exports.object({
15798
15839
  /** Default 100, range 1–500. */
@@ -15821,12 +15862,18 @@ var ActivityOverviewResponse = external_exports.object({
15821
15862
  }).meta({ id: "ActivityOverviewResponse" });
15822
15863
 
15823
15864
  // ../../packages/schema/src/zod/event.ts
15824
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15865
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15825
15866
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15826
15867
  var EventMetadata = external_exports.object({
15827
15868
  sessionId: external_exports.string().optional(),
15828
15869
  repo: external_exports.string().optional(),
15829
15870
  filePath: external_exports.string().optional(),
15871
+ // The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch'),
15872
+ // set by the tool-scanning hooks. The tool NAME only — never the tool's
15873
+ // arguments or output, which can carry the very value a finding masked
15874
+ // (metadata is stored unredacted). Gives findings on non-file captures a
15875
+ // display location ("via Bash") when no filePath exists.
15876
+ toolName: external_exports.string().optional(),
15830
15877
  // Set (true) by the worktree scanner when the file is excluded by the
15831
15878
  // repo's .gitignore. Gitignored files ARE still scanned — local scratch and
15832
15879
  // generated code can leak real secrets — but the provenance is recorded so
@@ -16134,7 +16181,7 @@ var DetectionException = external_exports.object({
16134
16181
  justification: external_exports.string().min(1),
16135
16182
  conditions: ExceptionConditions.nullable(),
16136
16183
  createdBy: external_exports.string(),
16137
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16184
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16138
16185
  createdAt: external_exports.iso.datetime(),
16139
16186
  updatedAt: external_exports.iso.datetime(),
16140
16187
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16158,7 +16205,10 @@ var ExceptionBundleEntry = DetectionException.pick({
16158
16205
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16159
16206
  var KeywordMatcher = external_exports.object({
16160
16207
  type: external_exports.literal("keyword"),
16161
- keywords: external_exports.array(external_exports.string()).min(1),
16208
+ // An empty keyword matches at every position, yielding one zero-length span
16209
+ // per character. Rejected here because a keyword that matches everything is
16210
+ // never intentional.
16211
+ keywords: external_exports.array(external_exports.string().min(1)).min(1),
16162
16212
  caseSensitive: external_exports.boolean().default(false)
16163
16213
  });
16164
16214
  function isValidRegex(pattern, flags) {
@@ -16303,20 +16353,26 @@ var PolicyBundle = external_exports.object({
16303
16353
  customKeywords: external_exports.array(external_exports.string()),
16304
16354
  fetchedAt: external_exports.iso.datetime()
16305
16355
  }).meta({ id: "PolicyBundle" });
16306
- var DEFAULT_ACTIONS = {
16307
- secret: "block",
16308
- pii: "redact",
16309
- financial: "redact",
16310
- phi: "redact",
16311
- code_context: "warn",
16312
- code_flaw: "warn",
16313
- custom: "warn",
16314
- // Config-posture findings only observe today (they land in
16315
- // inspection_findings, outside the live-capture enforcement path).
16316
- config: "warn"
16317
- };
16318
16356
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16319
16357
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16358
+ var CATEGORY_PEAK_SEVERITY = {
16359
+ secret: "critical",
16360
+ financial: "critical",
16361
+ // core-financial/credit-card
16362
+ code_flaw: "critical",
16363
+ pii: "high",
16364
+ phi: "high",
16365
+ custom: "high",
16366
+ // user-defined; conservative
16367
+ code_context: "low",
16368
+ config: "low"
16369
+ // observe-only; floors to monitor regardless
16370
+ };
16371
+ function severityFloorPolicy(category) {
16372
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16373
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16374
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16375
+ }
16320
16376
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16321
16377
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16322
16378
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16343,6 +16399,12 @@ var BUILTIN_POLICY_SPECS = {
16343
16399
  description: "Refuse the request entirely whenever any rule in this detection matches."
16344
16400
  }
16345
16401
  };
16402
+ function builtinPolicyToAction(id) {
16403
+ return BUILTIN_POLICY_SPECS[id].action;
16404
+ }
16405
+ var DEFAULT_ACTIONS = Object.fromEntries(
16406
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16407
+ );
16346
16408
  var BUILTIN_POLICIES = Object.fromEntries(
16347
16409
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16348
16410
  );
@@ -16395,6 +16457,10 @@ var ListEventsResponse = external_exports.object({
16395
16457
  items: external_exports.array(Event),
16396
16458
  nextCursor: external_exports.string().nullable()
16397
16459
  }).meta({ id: "ListEventsResponse" });
16460
+ var IngestResponse = external_exports.object({
16461
+ accepted: external_exports.number().int().nonnegative(),
16462
+ duplicates: external_exports.number().int().nonnegative()
16463
+ }).meta({ id: "IngestResponse" });
16398
16464
  var ListFindingsQuery = external_exports.object({
16399
16465
  cursor: external_exports.string().optional(),
16400
16466
  limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
@@ -16749,10 +16815,8 @@ function toApiProvider(sourceTool) {
16749
16815
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16750
16816
  }
16751
16817
  var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
16752
- function deriveGroupStatus(instances) {
16753
- const statuses = new Set(
16754
- instances.map((i) => i.status).filter((s) => s !== void 0)
16755
- );
16818
+ function foldGroupStatus(instanceStatuses) {
16819
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16756
16820
  if (statuses.size === 0) return void 0;
16757
16821
  for (const candidate of STATUS_PRECEDENCE) {
16758
16822
  if (statuses.has(candidate)) return candidate;
@@ -16770,6 +16834,7 @@ function deriveFindingStatus(row) {
16770
16834
  function buildFindingGroups(rows, opts = {}) {
16771
16835
  const overrides = opts.overrides;
16772
16836
  const packNames = opts.packNames;
16837
+ const aggregates = opts.aggregates;
16773
16838
  const byRuleId = /* @__PURE__ */ new Map();
16774
16839
  for (const row of rows) {
16775
16840
  const existing = byRuleId.get(row.ruleId);
@@ -16785,23 +16850,27 @@ function buildFindingGroups(rows, opts = {}) {
16785
16850
  provider: toApiProvider(r.sourceTool),
16786
16851
  repo: r.repo,
16787
16852
  file: r.file,
16853
+ ...r.toolName === void 0 ? {} : { toolName: r.toolName },
16788
16854
  action: toApiAction(effectiveDbAction),
16789
16855
  detectedAt: r.occurredAt,
16790
16856
  confidence: r.confidence,
16791
16857
  status: r.status
16792
16858
  };
16793
16859
  });
16794
- const latestDetectedAt = ruleRows.reduce(
16860
+ const agg = aggregates?.get(ruleId);
16861
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16795
16862
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16796
16863
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16797
16864
  );
16798
16865
  const seenProviders = /* @__PURE__ */ new Set();
16799
- const providers = instances.map((i) => i.provider).filter((p) => {
16866
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16800
16867
  if (seenProviders.has(p)) return false;
16801
16868
  seenProviders.add(p);
16802
16869
  return true;
16803
16870
  });
16804
- const actionSet = new Set(instances.map((i) => i.action));
16871
+ const actionSet = new Set(
16872
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16873
+ );
16805
16874
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16806
16875
  const severity = ruleRows[0]?.severity ?? "low";
16807
16876
  const detection = {
@@ -16815,8 +16884,10 @@ function buildFindingGroups(rows, opts = {}) {
16815
16884
  contextPrefix: ""
16816
16885
  // empty (pending privacy review)
16817
16886
  };
16818
- const status = deriveGroupStatus(instances);
16819
- groups.push({
16887
+ const status = foldGroupStatus(
16888
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16889
+ );
16890
+ const group = {
16820
16891
  id: ruleId,
16821
16892
  category: apiCategory,
16822
16893
  subtype: ruleId,
@@ -16825,21 +16896,26 @@ function buildFindingGroups(rows, opts = {}) {
16825
16896
  match,
16826
16897
  detection,
16827
16898
  policy,
16828
- instanceCount: instances.length,
16899
+ instanceCount: agg?.instanceCount ?? instances.length,
16829
16900
  providers,
16830
16901
  aggregateAction,
16831
16902
  latestDetectedAt,
16832
16903
  instances,
16833
16904
  status
16834
- });
16905
+ };
16906
+ if (agg) {
16907
+ actionsCache.set(group, [...actionSet]);
16908
+ if (agg.searchText !== void 0) {
16909
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16910
+ }
16911
+ }
16912
+ groups.push(group);
16835
16913
  }
16836
16914
  return groups;
16837
16915
  }
16838
16916
  var haystackCache = /* @__PURE__ */ new WeakMap();
16839
- function groupHaystack(g) {
16840
- const cached2 = haystackCache.get(g);
16841
- if (cached2 !== void 0) return cached2;
16842
- const haystack = [
16917
+ function buildHaystack(g, extra) {
16918
+ return [
16843
16919
  g.subtype,
16844
16920
  g.category,
16845
16921
  g.match.maskedValue,
@@ -16847,11 +16923,26 @@ function groupHaystack(g) {
16847
16923
  g.id,
16848
16924
  ...g.instances.map((i) => i.repo),
16849
16925
  ...g.instances.map((i) => i.file),
16850
- ...g.instances.map((i) => i.id)
16926
+ ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
16927
+ ...g.instances.map((i) => i.id),
16928
+ ...extra === void 0 ? [] : [extra]
16851
16929
  ].join(" ").toLowerCase();
16930
+ }
16931
+ function groupHaystack(g) {
16932
+ const cached2 = haystackCache.get(g);
16933
+ if (cached2 !== void 0) return cached2;
16934
+ const haystack = buildHaystack(g);
16852
16935
  haystackCache.set(g, haystack);
16853
16936
  return haystack;
16854
16937
  }
16938
+ var actionsCache = /* @__PURE__ */ new WeakMap();
16939
+ function groupActions(g) {
16940
+ const cached2 = actionsCache.get(g);
16941
+ if (cached2 !== void 0) return cached2;
16942
+ const actions = [...new Set(g.instances.map((i) => i.action))];
16943
+ actionsCache.set(g, actions);
16944
+ return actions;
16945
+ }
16855
16946
  function applyFindingFilters(groups, opts) {
16856
16947
  let filtered = groups;
16857
16948
  if (opts.severity && opts.severity.length > 0) {
@@ -16864,7 +16955,7 @@ function applyFindingFilters(groups, opts) {
16864
16955
  }
16865
16956
  if (opts.actions && opts.actions.length > 0) {
16866
16957
  const actionSet = new Set(opts.actions);
16867
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
16958
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16868
16959
  }
16869
16960
  if (opts.subtype && opts.subtype.length > 0) {
16870
16961
  const subtypeSet = new Set(opts.subtype);
@@ -16916,8 +17007,7 @@ function computeFindingFacets(allGroups, opts) {
16916
17007
  });
16917
17008
  const actionMap = /* @__PURE__ */ new Map();
16918
17009
  for (const g of forAction) {
16919
- const actionSet = new Set(g.instances.map((i) => i.action));
16920
- for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17010
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
16921
17011
  }
16922
17012
  const forSubtype = applyFindingFilters(allGroups, {
16923
17013
  providers: opts.providers,
@@ -17100,6 +17190,71 @@ var ProjectFilesScan = external_exports.object({
17100
17190
  scannedAt: external_exports.string()
17101
17191
  });
17102
17192
 
17193
+ // ../../packages/schema/src/zod/ranges.ts
17194
+ var TIME_RANGES = ["7d", "30d", "3m", "6m"];
17195
+ var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
17196
+ var DEFAULT_TIME_RANGE = "7d";
17197
+ var RANGE_DAYS = {
17198
+ "7d": 7,
17199
+ "30d": 30,
17200
+ "3m": 90,
17201
+ "6m": 180
17202
+ };
17203
+ var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
17204
+
17205
+ // ../../packages/schema/src/zod/remediation.ts
17206
+ var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
17207
+ var MaskedFindingLocation = external_exports.object({
17208
+ filePath: external_exports.string(),
17209
+ span: Span.optional()
17210
+ }).strict();
17211
+ var MaskedSecretFinding = external_exports.object({
17212
+ provider: external_exports.string(),
17213
+ maskedToken: external_exports.string(),
17214
+ where: MaskedFindingLocation,
17215
+ state: SecretFindingState,
17216
+ observedAt: external_exports.iso.datetime().optional()
17217
+ }).strict();
17218
+ var RotationChecklistEntry = external_exports.object({
17219
+ provider: external_exports.string(),
17220
+ maskedToken: external_exports.string(),
17221
+ consolePath: external_exports.string(),
17222
+ occurrenceSpread: external_exports.number().int().positive()
17223
+ }).strict();
17224
+ var RemediationOption = external_exports.enum([
17225
+ "redact-rotation-checklist",
17226
+ "redact-only",
17227
+ "set-secret-redact",
17228
+ "leave"
17229
+ ]);
17230
+ var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
17231
+ var RemediationEntryContext = external_exports.object({
17232
+ entrySource: RemediationEntrySource
17233
+ }).strict();
17234
+ var RemediationOptionChoice = external_exports.object({
17235
+ id: RemediationOption,
17236
+ label: external_exports.string()
17237
+ });
17238
+ var BatchedRemediationDecision = external_exports.object({
17239
+ kind: external_exports.literal("decision"),
17240
+ entrySource: RemediationEntrySource,
17241
+ secretCount: external_exports.number().int().positive(),
17242
+ prompt: external_exports.string(),
17243
+ options: external_exports.tuple([
17244
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
17245
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
17246
+ RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
17247
+ RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
17248
+ ])
17249
+ });
17250
+ var NoRemediationDecision = external_exports.object({
17251
+ kind: external_exports.literal("no-decision")
17252
+ });
17253
+ var BatchedRemediation = external_exports.discriminatedUnion("kind", [
17254
+ BatchedRemediationDecision,
17255
+ NoRemediationDecision
17256
+ ]);
17257
+
17103
17258
  // ../../packages/schema/src/zod/rule-test.ts
17104
17259
  var TestRulesRequest = external_exports.object({
17105
17260
  rules: external_exports.array(Rule).min(1).max(100),
@@ -17158,10 +17313,8 @@ var SeveritySummaryResponse = external_exports.object({
17158
17313
  // All four severity levels are always present (count may be 0).
17159
17314
  bySeverity: external_exports.array(SeveritySummaryItem)
17160
17315
  }).meta({ id: "SeveritySummaryResponse" });
17161
- var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
17162
- var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
17163
17316
  var SecurityRangeQuery = external_exports.object({
17164
- range: external_exports.enum(SECURITY_RANGES).default("30d")
17317
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
17165
17318
  });
17166
17319
  var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
17167
17320
  var EnforcementAction = external_exports.object({
@@ -17171,7 +17324,7 @@ var EnforcementAction = external_exports.object({
17171
17324
  delta: external_exports.number().int()
17172
17325
  }).meta({ id: "EnforcementAction" });
17173
17326
  var EnforcementActionsResponse = external_exports.object({
17174
- range: SecurityRange,
17327
+ range: TimeRange,
17175
17328
  // Sum of actions[].count in the window.
17176
17329
  total: external_exports.number().int().nonnegative(),
17177
17330
  // One entry per kind, always all three present (count may be 0).
@@ -17186,7 +17339,7 @@ var FindingsTimeseriesPoint = external_exports.object({
17186
17339
  medium: external_exports.number().int().nonnegative()
17187
17340
  }).meta({ id: "FindingsTimeseriesPoint" });
17188
17341
  var FindingsTimeseriesResponse = external_exports.object({
17189
- range: SecurityRange,
17342
+ range: TimeRange,
17190
17343
  granularity: TimeseriesGranularity,
17191
17344
  points: external_exports.array(FindingsTimeseriesPoint)
17192
17345
  }).meta({ id: "FindingsTimeseriesResponse" });
@@ -17201,7 +17354,7 @@ var MttrTrendPoint = external_exports.object({
17201
17354
  })
17202
17355
  }).meta({ id: "MttrTrendPoint" });
17203
17356
  var MttrTrendResponse = external_exports.object({
17204
- range: SecurityRange,
17357
+ range: TimeRange,
17205
17358
  granularity: TimeseriesGranularity,
17206
17359
  points: external_exports.array(MttrTrendPoint)
17207
17360
  }).meta({ id: "MttrTrendResponse" });
@@ -17228,11 +17381,11 @@ var TopSource = external_exports.object({
17228
17381
  findingsCount: external_exports.number().int().nonnegative()
17229
17382
  }).meta({ id: "TopSource" });
17230
17383
  var TopSourcesResponse = external_exports.object({
17231
- range: SecurityRange,
17384
+ range: TimeRange,
17232
17385
  items: external_exports.array(TopSource)
17233
17386
  }).meta({ id: "TopSourcesResponse" });
17234
17387
  var TopSourcesQuery = external_exports.object({
17235
- range: external_exports.enum(SECURITY_RANGES).default("30d"),
17388
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
17236
17389
  limit: external_exports.coerce.number().int().min(1).max(50).default(5),
17237
17390
  // Omit for both kinds.
17238
17391
  kind: external_exports.enum(SOURCE_KINDS).optional()
@@ -17245,7 +17398,7 @@ var ScanCoverageProvider = external_exports.object({
17245
17398
  supported: external_exports.boolean()
17246
17399
  }).meta({ id: "ScanCoverageProvider" });
17247
17400
  var ScanCoverageResponse = external_exports.object({
17248
- range: SecurityRange,
17401
+ range: TimeRange,
17249
17402
  providers: external_exports.array(ScanCoverageProvider)
17250
17403
  }).meta({ id: "ScanCoverageResponse" });
17251
17404
  var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
@@ -17294,6 +17447,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
17294
17447
  var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
17295
17448
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
17296
17449
 
17450
+ // ../../packages/schema/src/zod/triage.ts
17451
+ var TriageHit = external_exports.object({
17452
+ ruleId: external_exports.string(),
17453
+ category: DetectionCategory,
17454
+ severity: Severity,
17455
+ maskedMatch: external_exports.string(),
17456
+ rawMatch: external_exports.string(),
17457
+ context: external_exports.string(),
17458
+ filePath: external_exports.string().optional(),
17459
+ confidence: external_exports.number().min(0).max(1),
17460
+ id: external_exports.string().optional(),
17461
+ valueFingerprint: external_exports.string().optional(),
17462
+ keyVersion: external_exports.number().int().nonnegative().optional()
17463
+ });
17464
+ var TriagePolicy = BuiltinPolicyId;
17465
+ var TriageCategoryRec = external_exports.object({
17466
+ category: DetectionCategory,
17467
+ action: TriagePolicy,
17468
+ reasoning: external_exports.string(),
17469
+ genuineCount: external_exports.number().int().nonnegative(),
17470
+ fpCount: external_exports.number().int().nonnegative(),
17471
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17472
+ // this array's length — enforced by the consumer, not this schema.
17473
+ fpIds: external_exports.array(external_exports.string())
17474
+ });
17475
+ var TriageRecommendation = external_exports.object({
17476
+ perCategory: external_exports.array(TriageCategoryRec),
17477
+ notes: external_exports.string()
17478
+ });
17479
+
17480
+ // ../../packages/schema/src/zod/setup-frame.ts
17481
+ var CalibrationCounts = external_exports.object({
17482
+ total: external_exports.number().int().nonnegative(),
17483
+ important: external_exports.number().int().nonnegative(),
17484
+ routine: external_exports.number().int().nonnegative()
17485
+ }).refine((c) => c.total === c.important + c.routine, {
17486
+ message: "total must equal important + routine",
17487
+ path: ["total"]
17488
+ });
17489
+ var FalsePositivePatternValue = external_exports.object({
17490
+ ruleId: external_exports.string(),
17491
+ category: DetectionCategory,
17492
+ valueFingerprint: external_exports.string(),
17493
+ keyVersion: external_exports.number().int().nonnegative()
17494
+ });
17495
+ var FalsePositivePatternGroup = external_exports.object({
17496
+ pattern: external_exports.string(),
17497
+ count: external_exports.number().int().nonnegative(),
17498
+ values: external_exports.array(FalsePositivePatternValue).min(1)
17499
+ });
17500
+ var CalibrationFindingKind = external_exports.object({
17501
+ category: DetectionCategory,
17502
+ count: external_exports.number().int().nonnegative(),
17503
+ egress: external_exports.boolean()
17504
+ });
17505
+ var CalibrationFrame = external_exports.object({
17506
+ counts: CalibrationCounts,
17507
+ routineCategories: external_exports.array(DetectionCategory),
17508
+ surfacedCategories: external_exports.array(DetectionCategory),
17509
+ findingKinds: external_exports.array(CalibrationFindingKind),
17510
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
17511
+ maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
17512
+ falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
17513
+ });
17514
+ var CalibrationPreviewCategory = TriageCategoryRec.pick({
17515
+ category: true,
17516
+ genuineCount: true,
17517
+ fpCount: true
17518
+ }).extend({
17519
+ egress: external_exports.boolean()
17520
+ });
17521
+ var CalibrationPreview = external_exports.object({
17522
+ categories: external_exports.array(CalibrationPreviewCategory),
17523
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
17524
+ });
17525
+ var CalibrationResult = external_exports.object({
17526
+ frame: CalibrationFrame,
17527
+ copy: external_exports.string()
17528
+ });
17529
+ var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
17530
+ var SetupHandoffOption = external_exports.object({
17531
+ id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
17532
+ label: external_exports.string()
17533
+ });
17534
+ var DashboardHandoffOptions = external_exports.tuple([
17535
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17536
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17537
+ ]);
17538
+ var ComposedRemediationOptions = external_exports.tuple([
17539
+ SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
17540
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17541
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17542
+ ]);
17543
+ var SetupHandoffOffer = external_exports.object({
17544
+ worthALook: external_exports.number().int().nonnegative(),
17545
+ liveKeys: external_exports.number().int().nonnegative().optional(),
17546
+ options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
17547
+ }).refine(
17548
+ (o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
17549
+ {
17550
+ message: "the chain-entry option is present exactly when liveKeys > 0",
17551
+ path: ["options"]
17552
+ }
17553
+ ).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
17554
+ message: "liveKeys is a subset of worthALook and cannot exceed it",
17555
+ path: ["liveKeys"]
17556
+ });
17557
+
17297
17558
  // ../../packages/schema/src/zod/shares.ts
17298
17559
  var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17299
17560
  var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
@@ -17479,6 +17740,102 @@ function reviewSeverityRank(reasons) {
17479
17740
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17480
17741
  }
17481
17742
 
17743
+ // ../../packages/persistence/src/internal/sql-text.ts
17744
+ function escapeLikePattern(s) {
17745
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17746
+ }
17747
+ function placeholders(n) {
17748
+ return Array.from({ length: n }, () => "?").join(", ");
17749
+ }
17750
+ function containsPattern(q) {
17751
+ return `%${escapeLikePattern(q)}%`;
17752
+ }
17753
+ function likeAny(exprs) {
17754
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17755
+ }
17756
+
17757
+ // ../../packages/persistence/src/internal/transactions.ts
17758
+ var savepointSeq = 0;
17759
+ function withTransaction(db, fn, mode = "DEFERRED") {
17760
+ if (db.isTransaction) {
17761
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17762
+ db.exec(`SAVEPOINT ${savepoint}`);
17763
+ try {
17764
+ fn();
17765
+ db.exec(`RELEASE ${savepoint}`);
17766
+ } catch (error51) {
17767
+ try {
17768
+ db.exec(`ROLLBACK TO ${savepoint}`);
17769
+ db.exec(`RELEASE ${savepoint}`);
17770
+ } catch {
17771
+ }
17772
+ throw error51;
17773
+ }
17774
+ return;
17775
+ }
17776
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17777
+ try {
17778
+ fn();
17779
+ db.exec("COMMIT");
17780
+ } catch (error51) {
17781
+ try {
17782
+ db.exec("ROLLBACK");
17783
+ } catch {
17784
+ }
17785
+ throw error51;
17786
+ }
17787
+ }
17788
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17789
+ const nested = db.isTransaction;
17790
+ try {
17791
+ withTransaction(db, fn, mode);
17792
+ return true;
17793
+ } catch (error51) {
17794
+ if (!db.isTransaction && nested) throw error51;
17795
+ return false;
17796
+ }
17797
+ }
17798
+
17799
+ // ../../packages/persistence/src/internal/warn.ts
17800
+ function akaWarn(message) {
17801
+ process.stderr.write(`[aka] ${message}
17802
+ `);
17803
+ }
17804
+
17805
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17806
+ function evidenceObjects(sql) {
17807
+ const objects = [];
17808
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17809
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17810
+ objects.push({ kind: "table", name: m[1] });
17811
+ }
17812
+ }
17813
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17814
+ if (m[1] !== void 0 && m[2] !== void 0) {
17815
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17816
+ }
17817
+ }
17818
+ return objects;
17819
+ }
17820
+ function schemaObjectExists(db, kind, name) {
17821
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17822
+ return row !== void 0;
17823
+ }
17824
+ function indexExists(db, name) {
17825
+ return schemaObjectExists(db, "index", name);
17826
+ }
17827
+ function columnNames(db, table, opts) {
17828
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17829
+ const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
17830
+ return columns.map((c) => c.name);
17831
+ }
17832
+ function evidenceExists(db, object2) {
17833
+ if (object2.kind === "column") {
17834
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17835
+ }
17836
+ return schemaObjectExists(db, "table", object2.name);
17837
+ }
17838
+
17482
17839
  // ../../packages/persistence/src/ids.ts
17483
17840
  import { createHash } from "crypto";
17484
17841
  function sha256Hex(input) {
@@ -17515,28 +17872,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17515
17872
  }
17516
17873
 
17517
17874
  // ../../packages/persistence/src/migrations.ts
17518
- function evidenceObjects(sql) {
17519
- const objects = [];
17520
- for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17521
- if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17522
- objects.push({ kind: "table", name: m[1] });
17523
- }
17524
- }
17525
- for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17526
- if (m[1] !== void 0 && m[2] !== void 0) {
17527
- objects.push({ kind: "column", table: m[1], name: m[2] });
17528
- }
17529
- }
17530
- return objects;
17531
- }
17532
- function evidenceExists(db, object2) {
17533
- if (object2.kind === "column") {
17534
- const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
17535
- return columns.some((c) => c.name === object2.name);
17536
- }
17537
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
17538
- return row !== void 0;
17539
- }
17540
17875
  function describeObject(object2) {
17541
17876
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17542
17877
  }
@@ -17547,10 +17882,6 @@ function createdIndexName(statement) {
17547
17882
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17548
17883
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17549
17884
  }
17550
- function indexExists(db, name) {
17551
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
17552
- return row !== void 0;
17553
- }
17554
17885
  function applyMigrations(db) {
17555
17886
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17556
17887
  db.exec(
@@ -17569,44 +17900,39 @@ function applyMigrations(db) {
17569
17900
  const present = evidence.filter((o) => evidenceExists(db, o));
17570
17901
  if (present.length > 0 && present.length < evidence.length) {
17571
17902
  const missing = evidence.filter((o) => !present.includes(o));
17572
- const message = `[aka] sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
17573
- process.stderr.write(`${message}
17574
- `);
17575
- throw new Error(message);
17903
+ const message = `sqlite migration ${migration.tag} has no ledger row, but the store already has ${present.map(describeObject).join(", ")} while missing ${missing.map(describeObject).join(", ")} \u2014 the schema diverged from the migration history; refusing to replay or skip.`;
17904
+ akaWarn(message);
17905
+ throw new Error(`[aka] ${message}`);
17576
17906
  }
17577
17907
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17578
17908
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17579
17909
  const statements = splitStatements(migration.sql);
17580
17910
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17581
17911
  try {
17582
- db.exec("BEGIN IMMEDIATE");
17583
- try {
17584
- for (const statement of statements) {
17585
- const indexName = createdIndexName(statement);
17586
- if (indexName === void 0) {
17587
- if (alreadyApplied) continue;
17588
- } else if (indexExists(db, indexName)) {
17589
- continue;
17912
+ withTransaction(
17913
+ db,
17914
+ () => {
17915
+ for (const statement of statements) {
17916
+ const indexName = createdIndexName(statement);
17917
+ if (indexName === void 0) {
17918
+ if (alreadyApplied) continue;
17919
+ } else if (indexExists(db, indexName)) {
17920
+ continue;
17921
+ }
17922
+ db.exec(statement);
17590
17923
  }
17591
- db.exec(statement);
17592
- }
17593
- if (wantsFkOff && !alreadyApplied) {
17594
- const violations = db.prepare("PRAGMA foreign_key_check").all();
17595
- if (violations.length > 0) {
17596
- throw new Error(
17597
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17598
- );
17924
+ if (wantsFkOff && !alreadyApplied) {
17925
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17926
+ if (violations.length > 0) {
17927
+ throw new Error(
17928
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17929
+ );
17930
+ }
17599
17931
  }
17600
- }
17601
- record2.run(migration.tag, Date.now());
17602
- db.exec("COMMIT");
17603
- } catch (error51) {
17604
- try {
17605
- db.exec("ROLLBACK");
17606
- } catch {
17607
- }
17608
- throw error51;
17609
- }
17932
+ record2.run(migration.tag, Date.now());
17933
+ },
17934
+ "IMMEDIATE"
17935
+ );
17610
17936
  } finally {
17611
17937
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17612
17938
  }
@@ -17649,8 +17975,7 @@ var TOKEN_USAGE_COLUMNS = [
17649
17975
  }
17650
17976
  ];
17651
17977
  function ensureTokenUsageColumns(db) {
17652
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17653
- const existing = new Set(columns.map((c) => c.name));
17978
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17654
17979
  for (const column of TOKEN_USAGE_COLUMNS) {
17655
17980
  if (!existing.has(column.name)) {
17656
17981
  db.exec(column.ddl);
@@ -17688,47 +18013,39 @@ function reconcileSourceProjectIds(db) {
17688
18013
  repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
17689
18014
  }));
17690
18015
  const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
17691
- db.exec("BEGIN IMMEDIATE");
17692
- try {
17693
- for (const { row, canonicalId } of legacy) {
17694
- foldProject.run(
17695
- canonicalId,
17696
- row.url,
17697
- row.name,
17698
- row.attributes,
17699
- row.firstSeen,
17700
- row.lastSeen
17701
- );
17702
- repointAudit.run(canonicalId, row.id);
17703
- for (const { dropCollisions, repoint } of pathTables) {
17704
- dropCollisions.run(row.id, canonicalId);
17705
- repoint.run(canonicalId, row.id);
18016
+ withTransaction(
18017
+ db,
18018
+ () => {
18019
+ for (const { row, canonicalId } of legacy) {
18020
+ foldProject.run(
18021
+ canonicalId,
18022
+ row.url,
18023
+ row.name,
18024
+ row.attributes,
18025
+ row.firstSeen,
18026
+ row.lastSeen
18027
+ );
18028
+ repointAudit.run(canonicalId, row.id);
18029
+ for (const { dropCollisions, repoint } of pathTables) {
18030
+ dropCollisions.run(row.id, canonicalId);
18031
+ repoint.run(canonicalId, row.id);
18032
+ }
18033
+ repointCallSite.run(canonicalId, row.id);
18034
+ deleteLegacy.run(row.id);
17706
18035
  }
17707
- repointCallSite.run(canonicalId, row.id);
17708
- deleteLegacy.run(row.id);
17709
- }
17710
- db.exec("COMMIT");
17711
- } catch (error51) {
17712
- try {
17713
- db.exec("ROLLBACK");
17714
- } catch {
17715
- }
17716
- throw error51;
17717
- }
18036
+ },
18037
+ "IMMEDIATE"
18038
+ );
17718
18039
  } catch (error51) {
17719
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17720
- `);
18040
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17721
18041
  }
17722
18042
  }
17723
18043
  function isForeignSqliteLineage(db) {
17724
- const tenantsTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tenants' LIMIT 1").get();
17725
- if (tenantsTable !== void 0) return true;
17726
- const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
17727
- return eventsColumns.some((c) => c.name === "tenant_id");
18044
+ if (schemaObjectExists(db, "table", "tenants")) return true;
18045
+ return columnNames(db, "events").includes("tenant_id");
17728
18046
  }
17729
18047
  function ensureSyncedAtColumn(db, table) {
17730
- const columns = db.prepare(`PRAGMA table_info(${table})`).all();
17731
- if (!columns.some((c) => c.name === "synced_at")) {
18048
+ if (!columnNames(db, table).includes("synced_at")) {
17732
18049
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
17733
18050
  }
17734
18051
  }
@@ -17779,8 +18096,11 @@ function ensureDataDirSync(dir) {
17779
18096
  } catch {
17780
18097
  }
17781
18098
  }
18099
+ function walSidecars(file2) {
18100
+ return [`${file2}-wal`, `${file2}-shm`];
18101
+ }
17782
18102
  function tightenPerms(file2) {
17783
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
18103
+ for (const path of [file2, ...walSidecars(file2)]) {
17784
18104
  try {
17785
18105
  chmodSync(path, DATA_FILE_MODE);
17786
18106
  } catch {
@@ -17788,12 +18108,68 @@ function tightenPerms(file2) {
17788
18108
  }
17789
18109
  }
17790
18110
 
17791
- // ../../packages/persistence/src/repositories/sql-utils.ts
17792
- function escapeLikePattern(s) {
17793
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
18111
+ // ../../packages/persistence/src/internal/json.ts
18112
+ function safeJson(s, fallback) {
18113
+ if (s == null) return fallback;
18114
+ try {
18115
+ return JSON.parse(s);
18116
+ } catch {
18117
+ return fallback;
18118
+ }
17794
18119
  }
17795
- function placeholders(n) {
17796
- return Array.from({ length: n }, () => "?").join(", ");
18120
+ function parseJsonObject(s) {
18121
+ if (s == null) return void 0;
18122
+ try {
18123
+ const parsed = JSON.parse(s);
18124
+ if (typeof parsed === "object" && parsed !== null) return parsed;
18125
+ } catch {
18126
+ }
18127
+ return void 0;
18128
+ }
18129
+
18130
+ // ../../packages/persistence/src/internal/rows.ts
18131
+ function allRows(stmt, params) {
18132
+ if (params === void 0) return stmt.all();
18133
+ if (Array.isArray(params)) return stmt.all(...params);
18134
+ return stmt.all(params);
18135
+ }
18136
+ function getRow(stmt, params) {
18137
+ if (params === void 0) return stmt.get();
18138
+ if (Array.isArray(params)) return stmt.get(...params);
18139
+ return stmt.get(params);
18140
+ }
18141
+ function intToBool(raw) {
18142
+ return raw === 1 || raw === true;
18143
+ }
18144
+ function boolToInt(b) {
18145
+ return b ? 1 : 0;
18146
+ }
18147
+ function bindParams(row) {
18148
+ const out = {};
18149
+ for (const [key, value] of Object.entries(row)) {
18150
+ out[key] = value === void 0 ? null : value;
18151
+ }
18152
+ return out;
18153
+ }
18154
+ function countScalar(db, sql, params) {
18155
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18156
+ }
18157
+ function countBy(db, sql, params) {
18158
+ const map2 = /* @__PURE__ */ new Map();
18159
+ for (const row of allRows(db.prepare(sql), params)) {
18160
+ map2.set(row.k, row.n);
18161
+ }
18162
+ return map2;
18163
+ }
18164
+ function mapRowsTolerant(rows, map2) {
18165
+ const out = [];
18166
+ for (const row of rows) {
18167
+ try {
18168
+ out.push(map2(row));
18169
+ } catch {
18170
+ }
18171
+ }
18172
+ return out;
17797
18173
  }
17798
18174
 
17799
18175
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17845,15 +18221,11 @@ function encodeCursor(payload) {
17845
18221
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17846
18222
  }
17847
18223
  function decodeCursor(cursor) {
17848
- try {
17849
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
17850
- if (parsed !== null && typeof parsed === "object" && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
17851
- return parsed;
17852
- }
17853
- return null;
17854
- } catch {
17855
- return null;
18224
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18225
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18226
+ return parsed;
17856
18227
  }
18228
+ return null;
17857
18229
  }
17858
18230
  var DB_EVENT_TYPE_TO_KIND = {
17859
18231
  session: "session",
@@ -17870,15 +18242,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17870
18242
  };
17871
18243
  function safeParseStringArray(raw) {
17872
18244
  if (!raw) return [];
17873
- try {
17874
- const parsed = JSON.parse(raw);
17875
- return Array.isArray(parsed) ? parsed : [];
17876
- } catch {
17877
- return [];
17878
- }
17879
- }
17880
- function toBool(raw) {
17881
- return raw === 1 || raw === true;
18245
+ const parsed = safeJson(raw, null);
18246
+ return Array.isArray(parsed) ? parsed : [];
17882
18247
  }
17883
18248
  function toHarness(raw) {
17884
18249
  const parsed = Harness.safeParse(raw);
@@ -17931,8 +18296,8 @@ function buildAuditEvent(row) {
17931
18296
  severity: severityParsed?.success ? severityParsed.data : null,
17932
18297
  link: linkParsed?.success ? linkParsed.data : null,
17933
18298
  targetId: row.target_id,
17934
- internal: toBool(row.internal),
17935
- flagged: toBool(row.flagged)
18299
+ internal: intToBool(row.internal),
18300
+ flagged: intToBool(row.flagged)
17936
18301
  };
17937
18302
  }
17938
18303
  var TIMELINE_COLUMNS = `
@@ -17948,6 +18313,10 @@ var TIMELINE_COLUMNS = `
17948
18313
  json_extract(attributes, '$.internal') AS internal,
17949
18314
  json_extract(attributes, '$.flagged') AS flagged`;
17950
18315
  var SESSION_ROOT = `event_type = 'session'`;
18316
+ var HAS_ACTIVITY = `EXISTS (
18317
+ SELECT 1 FROM audit_events c
18318
+ WHERE c.root_session_id = audit_events.id
18319
+ AND c.event_type NOT IN ('hook', 'config_scan'))`;
17951
18320
  var SqliteActivityRepository = class {
17952
18321
  constructor(db, now = () => Date.now()) {
17953
18322
  this.db = db;
@@ -17958,12 +18327,16 @@ var SqliteActivityRepository = class {
17958
18327
  stats(tz) {
17959
18328
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
17960
18329
  const { startMs, endMs } = window;
17961
- const sessionsToday = this.db.prepare(
18330
+ const sessionsToday = countScalar(
18331
+ this.db,
17962
18332
  `SELECT count(*) AS n FROM audit_events
17963
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
17964
- ).get(startMs, endMs).n;
18333
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
18334
+ AND ${HAS_ACTIVITY}`,
18335
+ [startMs, endMs]
18336
+ );
17965
18337
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
17966
- const liveNow = this.db.prepare(
18338
+ const liveNow = countScalar(
18339
+ this.db,
17967
18340
  `SELECT count(*) AS n FROM audit_events s
17968
18341
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
17969
18342
  AND max(
@@ -17972,22 +18345,29 @@ var SqliteActivityRepository = class {
17972
18345
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
17973
18346
  s.started_at
17974
18347
  )
17975
- ) >= ?`
17976
- ).get(liveThreshold).n;
17977
- const toolCallsToday = this.db.prepare(
18348
+ ) >= ?`,
18349
+ [liveThreshold]
18350
+ );
18351
+ const toolCallsToday = countScalar(
18352
+ this.db,
17978
18353
  `SELECT count(*) AS n FROM audit_events
17979
- WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`
17980
- ).get(startMs, endMs).n;
17981
- const findingsToday = this.db.prepare(
18354
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18355
+ [startMs, endMs]
18356
+ );
18357
+ const findingsToday = countScalar(
18358
+ this.db,
17982
18359
  `SELECT count(*) AS n FROM inspection_findings f
17983
18360
  JOIN audit_events e ON e.id = f.audit_event_id
17984
- WHERE e.started_at >= ? AND e.started_at < ?`
17985
- ).get(startMs, endMs).n;
17986
- const egressToday = this.db.prepare(
18361
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18362
+ [startMs, endMs]
18363
+ );
18364
+ const egressToday = countScalar(
18365
+ this.db,
17987
18366
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
17988
18367
  FROM audit_events
17989
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
17990
- ).get(startMs, endMs).n;
18368
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18369
+ [startMs, endMs]
18370
+ );
17991
18371
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
17992
18372
  }
17993
18373
  listSessions(query) {
@@ -18009,7 +18389,7 @@ var SqliteActivityRepository = class {
18009
18389
  conditions.push("started_at <= ?");
18010
18390
  params.push(toMs);
18011
18391
  if (query.q) {
18012
- const pattern = `%${escapeLikePattern(query.q)}%`;
18392
+ const pattern = containsPattern(query.q);
18013
18393
  conditions.push(
18014
18394
  `(content LIKE ? ESCAPE '\\'
18015
18395
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18023,13 +18403,21 @@ var SqliteActivityRepository = class {
18023
18403
  );
18024
18404
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
18025
18405
  }
18406
+ const emptyCount = countScalar(
18407
+ this.db,
18408
+ `SELECT count(*) AS n FROM audit_events
18409
+ WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
18410
+ params
18411
+ );
18412
+ if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
18026
18413
  if (cursor) {
18027
18414
  conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
18028
18415
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18029
18416
  }
18030
18417
  const limit = query.limit;
18031
- const rows = this.db.prepare(
18032
- `SELECT id,
18418
+ const rows = allRows(
18419
+ this.db.prepare(
18420
+ `SELECT id,
18033
18421
  json_extract(attributes, '$.harness') AS harness,
18034
18422
  content AS title,
18035
18423
  json_extract(attributes, '$.project') AS project,
@@ -18042,7 +18430,9 @@ var SqliteActivityRepository = class {
18042
18430
  WHERE ${conditions.join(" AND ")}
18043
18431
  ORDER BY started_at DESC, id DESC
18044
18432
  LIMIT ?`
18045
- ).all(...params, limit + 1);
18433
+ ),
18434
+ [...params, limit + 1]
18435
+ );
18046
18436
  const hasMore = rows.length > limit;
18047
18437
  const page = hasMore ? rows.slice(0, limit) : rows;
18048
18438
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18056,11 +18446,12 @@ var SqliteActivityRepository = class {
18056
18446
  );
18057
18447
  const last = page[page.length - 1];
18058
18448
  const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18059
- return Promise.resolve({ items, nextCursor });
18449
+ return Promise.resolve({ items, nextCursor, emptyCount });
18060
18450
  }
18061
18451
  getSession(sessionId) {
18062
- const rootRow = this.db.prepare(
18063
- `SELECT id,
18452
+ const rootRow = getRow(
18453
+ this.db.prepare(
18454
+ `SELECT id,
18064
18455
  json_extract(attributes, '$.harness') AS harness,
18065
18456
  content AS title,
18066
18457
  json_extract(attributes, '$.project') AS project,
@@ -18077,47 +18468,66 @@ var SqliteActivityRepository = class {
18077
18468
  FROM audit_events
18078
18469
  WHERE id = ? AND event_type = 'session'
18079
18470
  LIMIT 1`
18080
- ).get(sessionId);
18471
+ ),
18472
+ [sessionId]
18473
+ );
18081
18474
  if (!rootRow) return Promise.resolve(null);
18082
- const timelineRows = this.db.prepare(
18083
- `SELECT ${TIMELINE_COLUMNS}
18475
+ const timelineRows = allRows(
18476
+ this.db.prepare(
18477
+ `SELECT ${TIMELINE_COLUMNS}
18084
18478
  FROM audit_events
18085
18479
  WHERE id = ? OR root_session_id = ?
18086
18480
  ORDER BY started_at ASC, id ASC`
18087
- ).all(sessionId, sessionId);
18481
+ ),
18482
+ [sessionId, sessionId]
18483
+ );
18088
18484
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18089
- const tokenRow = this.db.prepare(
18090
- `SELECT
18485
+ const tokenRow = getRow(
18486
+ this.db.prepare(
18487
+ `SELECT
18091
18488
  coalesce(sum(input_tokens), 0) AS input,
18092
18489
  coalesce(sum(output_tokens), 0) AS output,
18093
18490
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18094
18491
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18095
18492
  FROM audit_events
18096
18493
  WHERE root_session_id = ? AND event_type = 'llm_call'`
18097
- ).get(sessionId);
18098
- const primaryModel = this.db.prepare(
18099
- `SELECT model, provider FROM audit_events
18100
- WHERE root_session_id = ? AND event_type = 'llm_call'
18494
+ ),
18495
+ [sessionId]
18496
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18497
+ const primaryModel = getRow(
18498
+ this.db.prepare(
18499
+ `SELECT model, provider FROM audit_events
18500
+ WHERE root_session_id = ? AND event_type = 'llm_call'
18101
18501
  ORDER BY started_at ASC, id ASC
18102
18502
  LIMIT 1`
18103
- ).get(sessionId);
18104
- const toolRows = this.db.prepare(
18105
- `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18503
+ ),
18504
+ [sessionId]
18505
+ );
18506
+ const toolRows = allRows(
18507
+ this.db.prepare(
18508
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18106
18509
  count(*) AS n
18107
18510
  FROM audit_events
18108
18511
  WHERE root_session_id = ? AND event_type = 'tool_call'
18109
18512
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
18110
- ).all(sessionId);
18111
- const modelRows = this.db.prepare(
18112
- `SELECT DISTINCT model FROM audit_events
18513
+ ),
18514
+ [sessionId]
18515
+ );
18516
+ const modelRows = allRows(
18517
+ this.db.prepare(
18518
+ `SELECT DISTINCT model FROM audit_events
18113
18519
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18114
18520
  ORDER BY model`
18115
- ).all(sessionId);
18521
+ ),
18522
+ [sessionId]
18523
+ );
18116
18524
  const derivedModels = modelRows.map((r) => r.model);
18117
- const commits = this.db.prepare(
18525
+ const commits = countScalar(
18526
+ this.db,
18118
18527
  `SELECT count(*) AS n FROM audit_events
18119
- WHERE root_session_id = ? AND event_type = 'commit'`
18120
- ).get(sessionId).n;
18528
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18529
+ [sessionId]
18530
+ );
18121
18531
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18122
18532
  turns: 0,
18123
18533
  findings: 0,
@@ -18184,7 +18594,10 @@ var SqliteActivityRepository = class {
18184
18594
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18185
18595
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18186
18596
  );
18187
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18597
+ const rows = allRows(
18598
+ stmt,
18599
+ fromMs === void 0 ? void 0 : [fromMs]
18600
+ );
18188
18601
  const seen = /* @__PURE__ */ new Set();
18189
18602
  for (const row of rows) seen.add(toHarness(row.harness));
18190
18603
  return Promise.resolve([...seen]);
@@ -18207,23 +18620,23 @@ var SqliteActivityRepository = class {
18207
18620
  conditions.push("started_at >= ?");
18208
18621
  params.push(opts.fromMs);
18209
18622
  }
18210
- const rows = this.db.prepare(
18211
- `SELECT root_session_id AS sessionId, attributes
18623
+ const rows = allRows(
18624
+ this.db.prepare(
18625
+ `SELECT root_session_id AS sessionId, attributes
18212
18626
  FROM audit_events
18213
18627
  WHERE ${conditions.join(" AND ")}`
18214
- ).all(...params);
18215
- const leaves = [];
18216
- for (const row of rows) {
18217
- if (row.sessionId === null) continue;
18218
- try {
18219
- leaves.push({
18220
- sessionId: row.sessionId,
18221
- attributes: JSON.parse(row.attributes)
18222
- });
18223
- } catch {
18224
- }
18225
- }
18226
- return leaves;
18628
+ ),
18629
+ params
18630
+ );
18631
+ return mapRowsTolerant(
18632
+ rows.filter(
18633
+ (row) => row.sessionId !== null
18634
+ ),
18635
+ (row) => ({
18636
+ sessionId: row.sessionId,
18637
+ attributes: JSON.parse(row.attributes)
18638
+ })
18639
+ );
18227
18640
  }
18228
18641
  /**
18229
18642
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18237,57 +18650,72 @@ var SqliteActivityRepository = class {
18237
18650
  );
18238
18651
  if (sessionIds.length === 0) return result;
18239
18652
  const inClause = placeholders(sessionIds.length);
18240
- const lastActivityRows = this.db.prepare(
18241
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18653
+ const lastActivityRows = allRows(
18654
+ this.db.prepare(
18655
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18242
18656
  WHERE root_session_id IN (${inClause})
18243
18657
  GROUP BY root_session_id`
18244
- ).all(...sessionIds);
18658
+ ),
18659
+ sessionIds
18660
+ );
18245
18661
  for (const row of lastActivityRows) {
18246
18662
  if (row.id === null) continue;
18247
18663
  const entry = result.get(row.id);
18248
18664
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18249
18665
  }
18250
- const turnsRows = this.db.prepare(
18251
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18666
+ const turnsRows = allRows(
18667
+ this.db.prepare(
18668
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18252
18669
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18253
18670
  GROUP BY root_session_id`
18254
- ).all(...sessionIds);
18671
+ ),
18672
+ sessionIds
18673
+ );
18255
18674
  for (const row of turnsRows) {
18256
18675
  if (row.id === null) continue;
18257
18676
  const entry = result.get(row.id);
18258
18677
  if (entry) entry.turns = row.n;
18259
18678
  }
18260
- const runKeyRows = this.db.prepare(
18261
- `SELECT root_session_id AS id,
18679
+ const runKeyRows = allRows(
18680
+ this.db.prepare(
18681
+ `SELECT root_session_id AS id,
18262
18682
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18263
18683
  FROM audit_events
18264
18684
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18265
18685
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18266
18686
  GROUP BY root_session_id`
18267
- ).all(...sessionIds);
18687
+ ),
18688
+ sessionIds
18689
+ );
18268
18690
  for (const row of runKeyRows) {
18269
18691
  if (row.id === null) continue;
18270
18692
  const entry = result.get(row.id);
18271
18693
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18272
18694
  }
18273
- const findingsRows = this.db.prepare(
18274
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18695
+ const findingsRows = allRows(
18696
+ this.db.prepare(
18697
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18275
18698
  JOIN audit_events e ON e.id = f.audit_event_id
18276
18699
  WHERE e.root_session_id IN (${inClause})
18277
18700
  GROUP BY e.root_session_id`
18278
- ).all(...sessionIds);
18701
+ ),
18702
+ sessionIds
18703
+ );
18279
18704
  for (const row of findingsRows) {
18280
18705
  if (row.id === null) continue;
18281
18706
  const entry = result.get(row.id);
18282
18707
  if (entry) entry.findings = row.n;
18283
18708
  }
18284
- const sharesRows = this.db.prepare(
18285
- `SELECT root_session_id AS id,
18709
+ const sharesRows = allRows(
18710
+ this.db.prepare(
18711
+ `SELECT root_session_id AS id,
18286
18712
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18287
18713
  FROM audit_events
18288
18714
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18289
18715
  GROUP BY root_session_id`
18290
- ).all(...sessionIds);
18716
+ ),
18717
+ sessionIds
18718
+ );
18291
18719
  for (const row of sharesRows) {
18292
18720
  if (row.id === null) continue;
18293
18721
  const entry = result.get(row.id);
@@ -18337,33 +18765,28 @@ var SqliteAuditEventsRepository = class {
18337
18765
  // the caller fails open and drops the whole pass — recovered idempotently on the
18338
18766
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18339
18767
  runInTransaction(fn) {
18340
- this.db.exec("BEGIN");
18341
- try {
18342
- fn();
18343
- this.db.exec("COMMIT");
18344
- } catch (err) {
18345
- this.db.exec("ROLLBACK");
18346
- throw err;
18347
- }
18768
+ withTransaction(this.db, fn);
18348
18769
  }
18349
18770
  insertAuditEvent(input) {
18350
18771
  const row = toAuditEventRow(input);
18351
- this.insertStmt.run({
18352
- id: row.id,
18353
- parentId: row.parentId ?? null,
18354
- rootSessionId: row.rootSessionId ?? null,
18355
- eventType: row.eventType,
18356
- hostId: row.hostId ?? null,
18357
- harnessId: row.harnessId ?? null,
18358
- sourceProjectId: row.sourceProjectId ?? null,
18359
- startedAt: row.startedAt,
18360
- endedAt: row.endedAt ?? null,
18361
- severity: row.severity ?? null,
18362
- priority: row.priority ?? null,
18363
- content: row.content ?? null,
18364
- contentHash: row.contentHash ?? null,
18365
- attributes: row.attributes ?? null
18366
- });
18772
+ this.insertStmt.run(
18773
+ bindParams({
18774
+ id: row.id,
18775
+ parentId: row.parentId,
18776
+ rootSessionId: row.rootSessionId,
18777
+ eventType: row.eventType,
18778
+ hostId: row.hostId,
18779
+ harnessId: row.harnessId,
18780
+ sourceProjectId: row.sourceProjectId,
18781
+ startedAt: row.startedAt,
18782
+ endedAt: row.endedAt,
18783
+ severity: row.severity,
18784
+ priority: row.priority,
18785
+ content: row.content,
18786
+ contentHash: row.contentHash,
18787
+ attributes: row.attributes
18788
+ })
18789
+ );
18367
18790
  }
18368
18791
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18369
18792
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18377,22 +18800,24 @@ var SqliteAuditEventsRepository = class {
18377
18800
  const startedAt = isoToEpochMillis(input.startedAt);
18378
18801
  if (!Number.isFinite(startedAt)) return;
18379
18802
  const id = llmCallId(input.sessionId, input.messageId);
18380
- this.upsertLlmCallStmt.run({
18381
- id,
18382
- parentId: input.parentId,
18383
- rootSessionId: input.rootSessionId,
18384
- eventType: "llm_call",
18385
- hostId: null,
18386
- harnessId: null,
18387
- sourceProjectId: null,
18388
- startedAt,
18389
- endedAt: null,
18390
- severity: null,
18391
- priority: null,
18392
- content: null,
18393
- contentHash: null,
18394
- attributes: JSON.stringify(input.attributes)
18395
- });
18803
+ this.upsertLlmCallStmt.run(
18804
+ bindParams({
18805
+ id,
18806
+ parentId: input.parentId,
18807
+ rootSessionId: input.rootSessionId,
18808
+ eventType: "llm_call",
18809
+ hostId: null,
18810
+ harnessId: null,
18811
+ sourceProjectId: null,
18812
+ startedAt,
18813
+ endedAt: null,
18814
+ severity: null,
18815
+ priority: null,
18816
+ content: null,
18817
+ contentHash: null,
18818
+ attributes: JSON.stringify(input.attributes)
18819
+ })
18820
+ );
18396
18821
  }
18397
18822
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18398
18823
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18416,25 +18841,29 @@ var SqliteAuditEventsRepository = class {
18416
18841
  const startedAt = isoToEpochMillis(input.startedAt);
18417
18842
  if (!Number.isFinite(startedAt)) return;
18418
18843
  const id = toolCallId(input.sessionId, input.toolUseId);
18419
- this.insertStmt.run({
18420
- id,
18421
- parentId: input.parentId,
18422
- rootSessionId: input.rootSessionId,
18423
- eventType: "tool_call",
18424
- hostId: null,
18425
- harnessId: null,
18426
- sourceProjectId: null,
18427
- startedAt,
18428
- endedAt: null,
18429
- severity: null,
18430
- priority: null,
18431
- content: null,
18432
- contentHash: null,
18433
- attributes: JSON.stringify(input.attributes)
18434
- });
18844
+ this.insertStmt.run(
18845
+ bindParams({
18846
+ id,
18847
+ parentId: input.parentId,
18848
+ rootSessionId: input.rootSessionId,
18849
+ eventType: "tool_call",
18850
+ hostId: null,
18851
+ harnessId: null,
18852
+ sourceProjectId: null,
18853
+ startedAt,
18854
+ endedAt: null,
18855
+ severity: null,
18856
+ priority: null,
18857
+ content: null,
18858
+ contentHash: null,
18859
+ attributes: JSON.stringify(input.attributes)
18860
+ })
18861
+ );
18435
18862
  }
18436
18863
  findById(id) {
18437
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18864
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18865
+ id
18866
+ });
18438
18867
  }
18439
18868
  // Read the `provider` snapshotted onto a session root's attributes.
18440
18869
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18444,14 +18873,8 @@ var SqliteAuditEventsRepository = class {
18444
18873
  sessionProvider(sessionId) {
18445
18874
  const row = this.findById(sessionId);
18446
18875
  if (!row?.attributes) return void 0;
18447
- try {
18448
- const parsed = JSON.parse(row.attributes);
18449
- if (typeof parsed === "object" && parsed !== null) {
18450
- const provider = parsed.provider;
18451
- if (typeof provider === "string") return provider;
18452
- }
18453
- } catch {
18454
- }
18876
+ const provider = parseJsonObject(row.attributes)?.provider;
18877
+ if (typeof provider === "string") return provider;
18455
18878
  return void 0;
18456
18879
  }
18457
18880
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18460,11 +18883,13 @@ var SqliteAuditEventsRepository = class {
18460
18883
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18461
18884
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18462
18885
  llmCallLeaves() {
18463
- return this.db.prepare(
18464
- `SELECT root_session_id AS sessionId, attributes
18886
+ return allRows(
18887
+ this.db.prepare(
18888
+ `SELECT root_session_id AS sessionId, attributes
18465
18889
  FROM audit_events
18466
18890
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18467
- ).all();
18891
+ )
18892
+ );
18468
18893
  }
18469
18894
  };
18470
18895
 
@@ -18483,16 +18908,29 @@ var SqliteClassifiedDataRepository = class {
18483
18908
  upsert(input) {
18484
18909
  const id = classifiedDataId(input.class);
18485
18910
  const row = toClassifiedDataRow(input, id);
18486
- this.insertStmt.run({
18487
- id: row.id,
18488
- class: row.class,
18489
- label: row.label ?? null,
18490
- attributes: row.attributes ?? null
18491
- });
18911
+ this.insertStmt.run(
18912
+ bindParams({
18913
+ id: row.id,
18914
+ class: row.class,
18915
+ label: row.label,
18916
+ attributes: row.attributes
18917
+ })
18918
+ );
18492
18919
  return id;
18493
18920
  }
18494
18921
  };
18495
18922
 
18923
+ // ../../packages/persistence/src/repositories/config-scan.ts
18924
+ function latestConfigScan(db) {
18925
+ return getRow(
18926
+ db.prepare(
18927
+ `SELECT id, started_at, attributes FROM audit_events
18928
+ WHERE event_type = 'config_scan'
18929
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18930
+ )
18931
+ );
18932
+ }
18933
+
18496
18934
  // ../../packages/persistence/src/repositories/config-inventory.ts
18497
18935
  var SqliteConfigInventoryRepository = class {
18498
18936
  constructor(db) {
@@ -18500,7 +18938,7 @@ var SqliteConfigInventoryRepository = class {
18500
18938
  }
18501
18939
  db;
18502
18940
  report() {
18503
- const scan2 = this.latestScan();
18941
+ const scan2 = latestConfigScan(this.db);
18504
18942
  if (!scan2) {
18505
18943
  return {
18506
18944
  scannedAt: null,
@@ -18511,17 +18949,23 @@ var SqliteConfigInventoryRepository = class {
18511
18949
  topics: []
18512
18950
  };
18513
18951
  }
18514
- const rows = this.db.prepare(
18515
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18952
+ const rows = allRows(
18953
+ this.db.prepare(
18954
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18516
18955
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18517
18956
  ORDER BY object_type, title`
18518
- ).all({ startedAt: scan2.started_at });
18519
- const findings = this.db.prepare(
18520
- `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18957
+ ),
18958
+ { startedAt: scan2.started_at }
18959
+ );
18960
+ const findings = allRows(
18961
+ this.db.prepare(
18962
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18521
18963
  FROM inspection_findings f
18522
18964
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18523
18965
  WHERE f.audit_event_id = :scanId`
18524
- ).all({ scanId: scan2.id });
18966
+ ),
18967
+ { scanId: scan2.id }
18968
+ );
18525
18969
  const skills = [];
18526
18970
  const hooks = [];
18527
18971
  const mcpServers = [];
@@ -18550,7 +18994,9 @@ var SqliteConfigInventoryRepository = class {
18550
18994
  // schema note); an override whose asset is gone simply never matches. A row
18551
18995
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18552
18996
  trustOverrides() {
18553
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
18997
+ const rows = allRows(
18998
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
18999
+ );
18554
19000
  const map2 = /* @__PURE__ */ new Map();
18555
19001
  for (const row of rows) {
18556
19002
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18559,13 +19005,6 @@ var SqliteConfigInventoryRepository = class {
18559
19005
  }
18560
19006
  return map2;
18561
19007
  }
18562
- latestScan() {
18563
- return this.db.prepare(
18564
- `SELECT id, started_at, attributes FROM audit_events
18565
- WHERE event_type = 'config_scan'
18566
- ORDER BY started_at DESC, id DESC LIMIT 1`
18567
- ).get();
18568
- }
18569
19008
  };
18570
19009
  function toSkillItem(row, bag) {
18571
19010
  const item = {
@@ -18676,22 +19115,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18676
19115
  return topics;
18677
19116
  }
18678
19117
  function countScanErrors(attributes) {
18679
- if (!attributes) return 0;
18680
- try {
18681
- const parsed = JSON.parse(attributes);
18682
- const errors = parsed?.errors;
18683
- return typeof errors === "number" ? errors : 0;
18684
- } catch {
18685
- return 0;
18686
- }
19118
+ const errors = parseJsonObject(attributes)?.errors;
19119
+ return typeof errors === "number" ? errors : 0;
18687
19120
  }
18688
19121
  function parseBag(raw) {
18689
- try {
18690
- const parsed = JSON.parse(raw);
18691
- if (typeof parsed === "object" && parsed !== null) return parsed;
18692
- } catch {
18693
- }
18694
- return void 0;
19122
+ return parseJsonObject(raw);
18695
19123
  }
18696
19124
  function str(value) {
18697
19125
  return typeof value === "string" ? value : void 0;
@@ -18700,12 +19128,7 @@ function str(value) {
18700
19128
  // ../../packages/persistence/src/repositories/detections.ts
18701
19129
  var DAY_MS2 = 864e5;
18702
19130
  function parseRules(rulesJson) {
18703
- let raw;
18704
- try {
18705
- raw = JSON.parse(rulesJson);
18706
- } catch {
18707
- return [];
18708
- }
19131
+ const raw = safeJson(rulesJson, []);
18709
19132
  if (!Array.isArray(raw)) return [];
18710
19133
  const rules = [];
18711
19134
  for (const entry of raw) {
@@ -18724,11 +19147,13 @@ var SqliteDetectionsRepository = class {
18724
19147
  db;
18725
19148
  now;
18726
19149
  listDetections(query) {
18727
- const rows = this.db.prepare(
18728
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19150
+ const rows = allRows(
19151
+ this.db.prepare(
19152
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18729
19153
  rules_json AS rulesJson
18730
19154
  FROM installed_packs`
18731
- ).all();
19155
+ )
19156
+ );
18732
19157
  const available = this.availableByPack();
18733
19158
  const summaries = rows.map((r) => {
18734
19159
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18737,7 +19162,7 @@ var SqliteDetectionsRepository = class {
18737
19162
  packId: r.packId,
18738
19163
  version: r.version,
18739
19164
  name: r.name,
18740
- enabled: r.enabled === 1,
19165
+ enabled: intToBool(r.enabled),
18741
19166
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18742
19167
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18743
19168
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18754,19 +19179,23 @@ var SqliteDetectionsRepository = class {
18754
19179
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18755
19180
  // detail call; the table is a handful of rows).
18756
19181
  availableByPack() {
18757
- const rows = this.db.prepare(
18758
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19182
+ const rows = allRows(
19183
+ this.db.prepare(
19184
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18759
19185
  FROM available_packs`
18760
- ).all();
19186
+ )
19187
+ );
18761
19188
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18762
19189
  }
18763
19190
  getDetectionStats() {
18764
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
19191
+ const rows = allRows(
19192
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
19193
+ );
18765
19194
  let rules = 0;
18766
19195
  let active = 0;
18767
19196
  const ruleIds = /* @__PURE__ */ new Set();
18768
19197
  for (const r of rows) {
18769
- if (r.enabled === 1) active += 1;
19198
+ if (intToBool(r.enabled)) active += 1;
18770
19199
  const parsed = parseRules(r.rulesJson);
18771
19200
  rules += parsed.length;
18772
19201
  for (const rule of parsed) {
@@ -18784,12 +19213,15 @@ var SqliteDetectionsRepository = class {
18784
19213
  const parts = splitDetectionId(id);
18785
19214
  if (!parts) return Promise.resolve(null);
18786
19215
  const { namespace, packId } = parts;
18787
- const row = this.db.prepare(
18788
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19216
+ const row = getRow(
19217
+ this.db.prepare(
19218
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18789
19219
  rules_json AS rulesJson, updated_at AS updatedAt
18790
19220
  FROM installed_packs
18791
19221
  WHERE namespace = ? AND pack_id = ?`
18792
- ).get(namespace, packId);
19222
+ ),
19223
+ [namespace, packId]
19224
+ );
18793
19225
  if (!row) return Promise.resolve(null);
18794
19226
  const rules = parseRules(row.rulesJson);
18795
19227
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18807,7 +19239,7 @@ var SqliteDetectionsRepository = class {
18807
19239
  packId: row.packId,
18808
19240
  version: row.version,
18809
19241
  name: row.name,
18810
- enabled: row.enabled === 1,
19242
+ enabled: intToBool(row.enabled),
18811
19243
  rules,
18812
19244
  updatedAt: new Date(row.updatedAt),
18813
19245
  policyId: row.policyId
@@ -18822,13 +19254,14 @@ var SqliteDetectionsRepository = class {
18822
19254
  countFindingsLast30d(ruleIds) {
18823
19255
  if (ruleIds.length === 0) return 0;
18824
19256
  const since = this.now() - 30 * DAY_MS2;
18825
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18826
- const row = this.db.prepare(
18827
- `SELECT count(*) AS c
19257
+ const inClause = placeholders(ruleIds.length);
19258
+ return countScalar(
19259
+ this.db,
19260
+ `SELECT count(*) AS n
18828
19261
  FROM findings f JOIN events e ON e.id = f.event_id
18829
- WHERE e.occurred_at >= ? AND f.rule_id IN (${placeholders2})`
18830
- ).get(since, ...ruleIds);
18831
- return row.c;
19262
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19263
+ [since, ...ruleIds]
19264
+ );
18832
19265
  }
18833
19266
  };
18834
19267
 
@@ -18845,16 +19278,17 @@ var SqliteEventsRepository = class {
18845
19278
  insertStmt;
18846
19279
  insertEvent(event) {
18847
19280
  const row = toEventRow(event);
18848
- this.insertStmt.run({
18849
- id: row.id,
18850
- sourceTool: row.sourceTool,
18851
- kind: row.kind,
18852
- occurredAt: row.occurredAt,
18853
- contentHash: row.contentHash,
18854
- content: row.content,
18855
- // exactOptionalPropertyTypes: the column is nullable, never undefined.
18856
- metadata: row.metadata ?? null
18857
- });
19281
+ this.insertStmt.run(
19282
+ bindParams({
19283
+ id: row.id,
19284
+ sourceTool: row.sourceTool,
19285
+ kind: row.kind,
19286
+ occurredAt: row.occurredAt,
19287
+ contentHash: row.contentHash,
19288
+ content: row.content,
19289
+ metadata: row.metadata
19290
+ })
19291
+ );
18858
19292
  }
18859
19293
  // Every recorded event's content hash — the historical backfill loads this once
18860
19294
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18862,13 +19296,23 @@ var SqliteEventsRepository = class {
18862
19296
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18863
19297
  // async EventsReadPort contract.
18864
19298
  contentHashes() {
18865
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19299
+ const rows = allRows(
19300
+ this.db.prepare("SELECT content_hash FROM events")
19301
+ );
18866
19302
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18867
19303
  }
18868
19304
  };
18869
19305
 
18870
19306
  // ../../packages/persistence/src/repositories/exceptions.ts
18871
19307
  import { randomUUID } from "crypto";
19308
+
19309
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19310
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19311
+ function isUniqueConstraintError(err) {
19312
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19313
+ }
19314
+
19315
+ // ../../packages/persistence/src/repositories/exceptions.ts
18872
19316
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18873
19317
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18874
19318
  var DuplicateActiveExceptionError = class extends Error {
@@ -18889,10 +19333,6 @@ var AmbiguousExceptionIdError = class extends Error {
18889
19333
  this.name = "AmbiguousExceptionIdError";
18890
19334
  }
18891
19335
  };
18892
- var SQLITE_CONSTRAINT_UNIQUE = 2067;
18893
- function isUniqueConstraintError(err) {
18894
- return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
18895
- }
18896
19336
  var ACTIVE_PREDICATE = `revoked_at IS NULL
18897
19337
  AND (expires_at IS NULL OR expires_at > :now)
18898
19338
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -18948,10 +19388,11 @@ var SqliteExceptionsRepository = class {
18948
19388
  this.insertExceptionRow(id, input, now);
18949
19389
  } catch (err) {
18950
19390
  if (!isUniqueConstraintError(err)) throw err;
18951
- this.db.exec("BEGIN IMMEDIATE");
18952
- try {
18953
- const superseded = this.db.prepare(
18954
- `UPDATE exceptions
19391
+ withTransaction(
19392
+ this.db,
19393
+ () => {
19394
+ const superseded = this.db.prepare(
19395
+ `UPDATE exceptions
18955
19396
  SET revoked_at = :now, revoked_by = :revokedBy,
18956
19397
  revoke_reason = 'superseded by a new grant for the same value',
18957
19398
  updated_at = :now
@@ -18959,24 +19400,27 @@ var SqliteExceptionsRepository = class {
18959
19400
  AND key_version = :keyVersion AND revoked_at IS NULL
18960
19401
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
18961
19402
  OR (max_uses IS NOT NULL AND use_count >= max_uses))`
18962
- ).run({
18963
- now,
18964
- revokedBy: input.createdBy,
18965
- ruleId: input.ruleId,
18966
- valueFingerprint: input.valueFingerprint,
18967
- keyVersion: input.keyVersion
18968
- });
18969
- if (Number(superseded.changes) !== 1) {
18970
- throw new DuplicateActiveExceptionError(input.ruleId);
18971
- }
18972
- this.insertExceptionRow(id, input, now);
18973
- this.db.exec("COMMIT");
18974
- } catch (retryErr) {
18975
- this.db.exec("ROLLBACK");
18976
- throw retryErr;
18977
- }
19403
+ ).run({
19404
+ now,
19405
+ revokedBy: input.createdBy,
19406
+ ruleId: input.ruleId,
19407
+ valueFingerprint: input.valueFingerprint,
19408
+ keyVersion: input.keyVersion
19409
+ });
19410
+ if (Number(superseded.changes) !== 1) {
19411
+ throw new DuplicateActiveExceptionError(input.ruleId);
19412
+ }
19413
+ this.insertExceptionRow(id, input, now);
19414
+ },
19415
+ "IMMEDIATE"
19416
+ );
19417
+ }
19418
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19419
+ id
19420
+ });
19421
+ if (row === void 0) {
19422
+ throw new Error("exception row not found immediately after insert");
18978
19423
  }
18979
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
18980
19424
  return parseExceptionRow(row);
18981
19425
  }
18982
19426
  insertExceptionRow(id, input, now) {
@@ -19014,14 +19458,11 @@ var SqliteExceptionsRepository = class {
19014
19458
  */
19015
19459
  list(opts) {
19016
19460
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19017
- const rows = this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`).all(opts?.includeTerminal ? {} : { now: Date.now() });
19018
- const exceptions = [];
19019
- for (const row of rows) {
19020
- try {
19021
- exceptions.push(parseExceptionRow(row));
19022
- } catch {
19023
- }
19024
- }
19461
+ const rows = allRows(
19462
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19463
+ opts?.includeTerminal ? {} : { now: Date.now() }
19464
+ );
19465
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19025
19466
  return Promise.resolve(exceptions);
19026
19467
  }
19027
19468
  /**
@@ -19031,7 +19472,12 @@ var SqliteExceptionsRepository = class {
19031
19472
  */
19032
19473
  getByIdPrefix(prefix) {
19033
19474
  if (prefix.length === 0) return Promise.resolve(void 0);
19034
- const rows = this.db.prepare(String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`).all({ pattern: `${escapeLikePattern(prefix)}%` });
19475
+ const rows = allRows(
19476
+ this.db.prepare(
19477
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19478
+ ),
19479
+ { pattern: `${escapeLikePattern(prefix)}%` }
19480
+ );
19035
19481
  if (rows.length > 1) {
19036
19482
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19037
19483
  }
@@ -19073,30 +19519,27 @@ var SqliteExceptionsRepository = class {
19073
19519
  * a different (rotated-away) key never match, so they are excluded at read.
19074
19520
  */
19075
19521
  activeBundleEntries(keyVersion, now = Date.now()) {
19076
- const rows = this.db.prepare(
19077
- `SELECT * FROM exceptions
19522
+ const rows = allRows(
19523
+ this.db.prepare(
19524
+ `SELECT * FROM exceptions
19078
19525
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19079
19526
  ORDER BY created_at DESC, rowid DESC`
19080
- ).all({ keyVersion, now });
19081
- const entries = [];
19082
- for (const row of rows) {
19083
- try {
19084
- const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19085
- entries.push(
19086
- ExceptionBundleEntry.parse({
19087
- id: row.id,
19088
- ruleId: row.rule_id,
19089
- valueFingerprint: row.value_fingerprint,
19090
- keyVersion: row.key_version,
19091
- expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19092
- maxUses: row.max_uses,
19093
- useCount: row.use_count,
19094
- conditions
19095
- })
19096
- );
19097
- } catch {
19098
- }
19099
- }
19527
+ ),
19528
+ { keyVersion, now }
19529
+ );
19530
+ const entries = mapRowsTolerant(rows, (row) => {
19531
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19532
+ return ExceptionBundleEntry.parse({
19533
+ id: row.id,
19534
+ ruleId: row.rule_id,
19535
+ valueFingerprint: row.value_fingerprint,
19536
+ keyVersion: row.key_version,
19537
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19538
+ maxUses: row.max_uses,
19539
+ useCount: row.use_count,
19540
+ conditions
19541
+ });
19542
+ });
19100
19543
  return Promise.resolve(entries);
19101
19544
  }
19102
19545
  /**
@@ -19123,11 +19566,14 @@ var SqliteExceptionsRepository = class {
19123
19566
  }
19124
19567
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19125
19568
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19126
- const rows = this.db.prepare(
19127
- `SELECT * FROM blocked_detections
19569
+ const rows = allRows(
19570
+ this.db.prepare(
19571
+ `SELECT * FROM blocked_detections
19128
19572
  WHERE blocked_at > :cutoff
19129
19573
  ORDER BY blocked_at DESC, rowid DESC`
19130
- ).all({ cutoff: Date.now() - windowMs });
19574
+ ),
19575
+ { cutoff: Date.now() - windowMs }
19576
+ );
19131
19577
  return Promise.resolve(
19132
19578
  rows.map((row) => ({
19133
19579
  reference: row.reference,
@@ -19207,7 +19653,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19207
19653
  )`;
19208
19654
 
19209
19655
  // ../../packages/persistence/src/repositories/findings.ts
19210
- var GROUP_ROW_CAP = 2e3;
19656
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19657
+ var CONCAT_SEP = ",";
19658
+ var TUPLE_SEP = "|";
19659
+ function splitConcat(value) {
19660
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19661
+ }
19211
19662
  function deriveInstanceStatus(row) {
19212
19663
  return deriveFindingStatus({
19213
19664
  kind: row.kind,
@@ -19275,13 +19726,16 @@ var SqliteFindingsRepository = class {
19275
19726
  }
19276
19727
  recentFindings(opts) {
19277
19728
  const limit = opts?.limit ?? 50;
19278
- const rows = this.db.prepare(
19279
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19729
+ const rows = allRows(
19730
+ this.db.prepare(
19731
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19280
19732
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19281
19733
  FROM findings f JOIN events e ON e.id = f.event_id
19282
19734
  ORDER BY e.occurred_at DESC, f.rowid DESC
19283
19735
  LIMIT :limit`
19284
- ).all({ limit });
19736
+ ),
19737
+ { limit }
19738
+ );
19285
19739
  return Promise.resolve(
19286
19740
  rows.map((r) => ({
19287
19741
  id: r.id,
@@ -19298,26 +19752,94 @@ var SqliteFindingsRepository = class {
19298
19752
  }))
19299
19753
  );
19300
19754
  }
19755
+ /** Live-enforced findings recorded for one session — a bare COUNT over the
19756
+ * session-stamped events (served by idx_events_session_id), so the Activity
19757
+ * page can label its findings link without the grouped pipeline. */
19758
+ sessionFindingsCount(sessionId) {
19759
+ if (!sessionId) return Promise.resolve(0);
19760
+ return Promise.resolve(
19761
+ countScalar(
19762
+ this.db,
19763
+ `SELECT count(*) AS n FROM findings f
19764
+ JOIN events e ON e.id = f.event_id
19765
+ WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
19766
+ { sessionId }
19767
+ )
19768
+ );
19769
+ }
19770
+ /** Per-rule transcript firing tally for one session — reads the OTHER finding
19771
+ * store (inspection_findings, keyed to audit_events): every detection the
19772
+ * transcript pass recorded, counted per firing rather than per unique value.
19773
+ * Rides on session-scoped grouped responses so the findings view can
19774
+ * reconcile the Activity page's tally with the deduped groups it lists. */
19775
+ sessionFirings(sessionId) {
19776
+ return Object.fromEntries(
19777
+ countBy(
19778
+ this.db,
19779
+ `SELECT d.rule_id AS k, count(*) AS n
19780
+ FROM inspection_findings f
19781
+ JOIN audit_events e ON e.id = f.audit_event_id
19782
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19783
+ WHERE e.root_session_id = :sessionId
19784
+ GROUP BY d.rule_id`,
19785
+ { sessionId }
19786
+ )
19787
+ );
19788
+ }
19301
19789
  /**
19302
- * Grouped findings for the dashboard — joins findings⋈events (repo/file
19303
- * from event metadata), groups by ruleId, computes per-filter-excluded facets,
19790
+ * Grouped findings for the dashboard — joins findings⋈events (repo/file/
19791
+ * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19304
19792
  * applies the requested filters, and sorts by severity then recency. Filtering
19305
19793
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19306
19794
  * reflect the full filtered set; `items` is the requested
19307
- * page (default 100); no cursor (nextCursor is always null).
19795
+ * page (default 50); no cursor (nextCursor is always null).
19796
+ *
19797
+ * Two reads, neither of which materializes a row per finding:
19798
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19799
+ * the group and the filters need (count, providers, actions, statuses,
19800
+ * latest, search text);
19801
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19802
+ * populate `instances` for the table's expanded rows.
19803
+ * The aggregates carry raw DB values and are translated by the same
19804
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19805
+ * rule is ever restated in SQL.
19308
19806
  */
19309
19807
  listGroupedFindings(query) {
19310
- const rows = this.db.prepare(
19311
- `SELECT f.id, f.rule_id, f.category, f.severity, f.masked_match,
19312
- f.action_taken, f.confidence, e.occurred_at, e.source_tool,
19313
- json_extract(e.metadata, '$.repo') AS repo,
19314
- json_extract(e.metadata, '$.filePath') AS file,
19315
- e.kind AS kind, f.finding_key AS finding_key,
19316
- ${latestResolutionStatusSql("f")} AS latest_status
19317
- FROM findings f JOIN events e ON e.id = f.event_id
19318
- ORDER BY e.occurred_at DESC, f.id DESC
19319
- LIMIT :cap`
19320
- ).all({ cap: GROUP_ROW_CAP });
19808
+ const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
19809
+ const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
19810
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
19811
+ predicate: sessionPredicate,
19812
+ params: sessionParams
19813
+ });
19814
+ const rows = allRows(
19815
+ this.db.prepare(
19816
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19817
+ occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
19818
+ FROM (
19819
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19820
+ f.severity AS severity, f.masked_match AS masked_match,
19821
+ f.action_taken AS action_taken, f.confidence AS confidence,
19822
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19823
+ json_extract(e.metadata, '$.repo') AS repo,
19824
+ json_extract(e.metadata, '$.filePath') AS file,
19825
+ json_extract(e.metadata, '$.toolName') AS tool_name,
19826
+ e.kind AS kind, f.finding_key AS finding_key,
19827
+ latest.status AS latest_status,
19828
+ ROW_NUMBER() OVER (
19829
+ PARTITION BY f.rule_id
19830
+ ORDER BY e.occurred_at DESC, f.id DESC
19831
+ ) AS rn
19832
+ FROM findings f
19833
+ JOIN events e ON e.id = f.event_id
19834
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19835
+ ON latest.finding_key = f.finding_key
19836
+ ${sessionPredicate}
19837
+ )
19838
+ WHERE rn <= :cap
19839
+ ORDER BY occurred_at DESC, id DESC`
19840
+ ),
19841
+ { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
19842
+ );
19321
19843
  const groupable = rows.map((r) => ({
19322
19844
  id: r.id,
19323
19845
  ruleId: r.rule_id,
@@ -19330,9 +19852,10 @@ var SqliteFindingsRepository = class {
19330
19852
  sourceTool: r.source_tool,
19331
19853
  repo: r.repo ?? "",
19332
19854
  file: r.file ?? "",
19855
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
19333
19856
  status: deriveInstanceStatus(r)
19334
19857
  }));
19335
- const allGroups = buildFindingGroups(groupable);
19858
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19336
19859
  const filterOpts = {
19337
19860
  severity: query.severity,
19338
19861
  providers: query.provider,
@@ -19348,44 +19871,134 @@ var SqliteFindingsRepository = class {
19348
19871
  };
19349
19872
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
19350
19873
  const items = sorted.slice(0, limit);
19351
- return Promise.resolve({ totals, facets, items, nextCursor: null });
19874
+ return Promise.resolve({
19875
+ totals,
19876
+ facets,
19877
+ items,
19878
+ nextCursor: null,
19879
+ ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
19880
+ });
19881
+ }
19882
+ /**
19883
+ * One row per rule_id, folding EVERY instance of the group into the values
19884
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19885
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19886
+ *
19887
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19888
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19889
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19890
+ * status keeps the classifier itself in @akasecurity/schema, where
19891
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19892
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19893
+ * a group's row stays small however many findings it holds.
19894
+ *
19895
+ * `withSearchText` is the exception, and the one column here that does NOT
19896
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19897
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19898
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19899
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19900
+ * match an instance outside the preview, which searching the preview alone
19901
+ * would silently lose, so it is fetched only when the request actually
19902
+ * carries a `q`.
19903
+ */
19904
+ groupAggregates(withSearchText, scope) {
19905
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19906
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
19907
+ group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
19908
+ const rows = this.db.prepare(
19909
+ `SELECT f.rule_id AS rule_id,
19910
+ count(*) AS instance_count,
19911
+ max(e.occurred_at) AS latest_at,
19912
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19913
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19914
+ group_concat(DISTINCT (
19915
+ e.kind || '${TUPLE_SEP}' ||
19916
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19917
+ coalesce(latest.status, '')
19918
+ )) AS status_inputs
19919
+ ${searchTextColumns}
19920
+ FROM findings f
19921
+ JOIN events e ON e.id = f.event_id
19922
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19923
+ ON latest.finding_key = f.finding_key
19924
+ ${scope.predicate}
19925
+ GROUP BY f.rule_id`
19926
+ ).all(scope.params);
19927
+ return new Map(
19928
+ rows.map((r) => [
19929
+ r.rule_id,
19930
+ {
19931
+ instanceCount: r.instance_count,
19932
+ sourceTools: splitConcat(r.source_tools),
19933
+ actionsTaken: splitConcat(r.actions_taken),
19934
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19935
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19936
+ return {
19937
+ // deriveFindingStatus only distinguishes null from non-null here,
19938
+ // so the marker stands in for the key itself (never rendered).
19939
+ kind,
19940
+ findingKey: keyMarker === "" ? null : keyMarker,
19941
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19942
+ };
19943
+ }),
19944
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19945
+ // Free text only — joined and substring-matched, so group_concat's
19946
+ // commas need no unpicking (a repo/path containing one still matches).
19947
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
19948
+ // tell "no q this request" from "a group with no repo/file at all"
19949
+ // and skip priming a haystack nothing will read.
19950
+ ...withSearchText ? {
19951
+ searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
19952
+ } : {}
19953
+ }
19954
+ ])
19955
+ );
19352
19956
  }
19353
19957
  healthSummary() {
19354
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
19958
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19355
19959
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19356
- const grouped = this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken").all();
19960
+ const grouped = allRows(
19961
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
19962
+ );
19357
19963
  for (const row of grouped) {
19358
19964
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19359
19965
  }
19360
19966
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19361
- const sevRows = this.db.prepare(
19362
- `SELECT f.severity AS severity, count(*) AS c
19967
+ const sevRows = allRows(
19968
+ this.db.prepare(
19969
+ `SELECT f.severity AS severity, count(*) AS c
19363
19970
  FROM findings f
19364
19971
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19365
19972
  ON latest.finding_key = f.finding_key
19366
19973
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19367
19974
  GROUP BY f.severity`
19368
- ).all();
19975
+ )
19976
+ );
19369
19977
  for (const row of sevRows) {
19370
19978
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19371
19979
  }
19372
19980
  const categories = ENFORCEABLE_CATEGORIES;
19373
- const enabledRows = this.db.prepare(
19374
- `SELECT DISTINCT json_extract(target, '$.category') AS category
19981
+ const enabledRows = allRows(
19982
+ this.db.prepare(
19983
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19375
19984
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19376
- ).all();
19985
+ )
19986
+ );
19377
19987
  const enabled = new Set(enabledRows.map((r) => r.category));
19378
19988
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19379
19989
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19380
19990
  }
19381
19991
  activityByDay(days = 7) {
19382
19992
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19383
- const rows = this.db.prepare(
19384
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19993
+ const rows = allRows(
19994
+ this.db.prepare(
19995
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19385
19996
  FROM findings f JOIN events e ON e.id = f.event_id
19386
19997
  WHERE e.occurred_at >= :since
19387
19998
  GROUP BY day, f.action_taken`
19388
- ).all({ since });
19999
+ ),
20000
+ { since }
20001
+ );
19389
20002
  const buckets = /* @__PURE__ */ new Map();
19390
20003
  for (let i = 0; i < days; i++) {
19391
20004
  const day = isoDay(since + i * DAY_MS3);
@@ -19457,17 +20070,19 @@ var SqliteInspectionFindingsRepository = class {
19457
20070
  insertStmt;
19458
20071
  insertFinding(input) {
19459
20072
  const row = toInspectionFindingRow(input);
19460
- this.insertStmt.run({
19461
- id: row.id,
19462
- auditEventId: row.auditEventId,
19463
- inspectionDefinitionId: row.inspectionDefinitionId,
19464
- classifiedDataId: row.classifiedDataId ?? null,
19465
- spanStart: row.spanStart,
19466
- spanEnd: row.spanEnd,
19467
- maskedMatch: row.maskedMatch,
19468
- actionTaken: row.actionTaken,
19469
- confidence: row.confidence
19470
- });
20073
+ this.insertStmt.run(
20074
+ bindParams({
20075
+ id: row.id,
20076
+ auditEventId: row.auditEventId,
20077
+ inspectionDefinitionId: row.inspectionDefinitionId,
20078
+ classifiedDataId: row.classifiedDataId,
20079
+ spanStart: row.spanStart,
20080
+ spanEnd: row.spanEnd,
20081
+ maskedMatch: row.maskedMatch,
20082
+ actionTaken: row.actionTaken,
20083
+ confidence: row.confidence
20084
+ })
20085
+ );
19471
20086
  }
19472
20087
  };
19473
20088
 
@@ -19543,12 +20158,7 @@ function isMirrorDowngrade(incoming, stored) {
19543
20158
  }
19544
20159
  function ruleIdsOf(rulesJson) {
19545
20160
  const ids = /* @__PURE__ */ new Set();
19546
- let raw;
19547
- try {
19548
- raw = JSON.parse(rulesJson);
19549
- } catch {
19550
- return ids;
19551
- }
20161
+ const raw = safeJson(rulesJson, []);
19552
20162
  if (!Array.isArray(raw)) return ids;
19553
20163
  for (const entry of raw) {
19554
20164
  if (entry && typeof entry === "object") {
@@ -19621,47 +20231,48 @@ var SqliteInstalledPacksRepository = class {
19621
20231
  }));
19622
20232
  if (this.storedSignature() === inventorySignature(rows)) return;
19623
20233
  const now = Date.now();
19624
- this.db.exec("BEGIN IMMEDIATE");
19625
- try {
19626
- const mirror = this.mirrorState();
19627
- let behind = false;
19628
- for (const row of rows) {
19629
- const params = {
19630
- id: randomUUID2(),
19631
- namespace: row.namespace,
19632
- packId: row.packId,
19633
- version: row.version,
19634
- name: row.name,
19635
- rulesJson: row.rulesJson,
19636
- now
19637
- };
19638
- const stored = mirror.get(`${row.namespace}/${row.packId}`);
19639
- if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19640
- this.upsertAvailableStmt.run({
19641
- ...params,
20234
+ withTransaction(
20235
+ this.db,
20236
+ () => {
20237
+ const mirror = this.mirrorState();
20238
+ let behind = false;
20239
+ for (const row of rows) {
20240
+ const params = {
19642
20241
  id: randomUUID2(),
19643
- recordedBy: meta3?.recordedBy ?? null
19644
- });
19645
- } else {
19646
- behind = true;
20242
+ namespace: row.namespace,
20243
+ packId: row.packId,
20244
+ version: row.version,
20245
+ name: row.name,
20246
+ rulesJson: row.rulesJson,
20247
+ now
20248
+ };
20249
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
20250
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20251
+ this.upsertAvailableStmt.run({
20252
+ ...params,
20253
+ id: randomUUID2(),
20254
+ recordedBy: meta3?.recordedBy ?? null
20255
+ });
20256
+ } else {
20257
+ behind = true;
20258
+ }
20259
+ this.insertMissingStmt.run(params);
19647
20260
  }
19648
- this.insertMissingStmt.run(params);
19649
- }
19650
- if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
19651
- this.db.exec("COMMIT");
19652
- } catch (err) {
19653
- this.db.exec("ROLLBACK");
19654
- throw err;
19655
- }
20261
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20262
+ },
20263
+ "IMMEDIATE"
20264
+ );
19656
20265
  } catch {
19657
20266
  }
19658
20267
  }
19659
20268
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19660
20269
  // input to the downgrade guard. Read INSIDE the write transaction.
19661
20270
  mirrorState() {
19662
- const rows = this.db.prepare(
19663
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19664
- ).all();
20271
+ const rows = allRows(
20272
+ this.db.prepare(
20273
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20274
+ )
20275
+ );
19665
20276
  return new Map(
19666
20277
  rows.map((r) => [
19667
20278
  `${r.namespace}/${r.packId}`,
@@ -19673,7 +20284,9 @@ var SqliteInstalledPacksRepository = class {
19673
20284
  // (keys joined with '/', matching the detection id slug encoding — packId may
19674
20285
  // itself contain '/', but namespace may not, so the join is unambiguous).
19675
20286
  pruneAvailable(keep) {
19676
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20287
+ const rows = allRows(
20288
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20289
+ );
19677
20290
  const keepSet = new Set(keep);
19678
20291
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19679
20292
  for (const r of rows) {
@@ -19700,11 +20313,13 @@ var SqliteInstalledPacksRepository = class {
19700
20313
  if (this.db.isTransaction) {
19701
20314
  throw new Error("applyUpdate must not be called inside an open transaction");
19702
20315
  }
19703
- this.db.exec("BEGIN IMMEDIATE");
19704
- try {
19705
- this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
19706
- const res = this.db.prepare(
19707
- `UPDATE installed_packs SET
20316
+ let changed = false;
20317
+ withTransaction(
20318
+ this.db,
20319
+ () => {
20320
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20321
+ const res = this.db.prepare(
20322
+ `UPDATE installed_packs SET
19708
20323
  version = (SELECT a.version FROM available_packs a
19709
20324
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19710
20325
  name = (SELECT a.name FROM available_packs a
@@ -19715,17 +20330,13 @@ var SqliteInstalledPacksRepository = class {
19715
20330
  WHERE namespace = :namespace AND pack_id = :packId
19716
20331
  AND EXISTS (SELECT 1 FROM available_packs a
19717
20332
  WHERE a.namespace = :namespace AND a.pack_id = :packId)`
19718
- ).run({ namespace, packId, now: Date.now() });
19719
- this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
19720
- this.db.exec("COMMIT");
19721
- return Number(res.changes) > 0;
19722
- } catch (err) {
19723
- try {
19724
- this.db.exec("ROLLBACK");
19725
- } catch {
19726
- }
19727
- throw err;
19728
- }
20333
+ ).run({ namespace, packId, now: Date.now() });
20334
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20335
+ changed = Number(res.changes) > 0;
20336
+ },
20337
+ "IMMEDIATE"
20338
+ );
20339
+ return changed;
19729
20340
  }
19730
20341
  /**
19731
20342
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19739,9 +20350,11 @@ var SqliteInstalledPacksRepository = class {
19739
20350
  * JSON-level failure therefore counts as invalid.
19740
20351
  */
19741
20352
  installedRuleset() {
19742
- const rows = this.db.prepare(
19743
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19744
- ).all();
20353
+ const rows = allRows(
20354
+ this.db.prepare(
20355
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20356
+ )
20357
+ );
19745
20358
  const out = {
19746
20359
  installedPacks: rows.length,
19747
20360
  enabledPacks: 0,
@@ -19750,7 +20363,7 @@ var SqliteInstalledPacksRepository = class {
19750
20363
  ruleActions: /* @__PURE__ */ new Map()
19751
20364
  };
19752
20365
  for (const row of rows) {
19753
- if (row.enabled !== 1) continue;
20366
+ if (!intToBool(row.enabled)) continue;
19754
20367
  out.enabledPacks += 1;
19755
20368
  const action = policyIdToAction(row.policyId);
19756
20369
  let raw;
@@ -19785,9 +20398,11 @@ var SqliteInstalledPacksRepository = class {
19785
20398
  * running max would mask a genuinely-newer parseable stamp.
19786
20399
  */
19787
20400
  newestRecordedBinary() {
19788
- const rows = this.db.prepare(
19789
- `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
19790
- ).all();
20401
+ const rows = allRows(
20402
+ this.db.prepare(
20403
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20404
+ )
20405
+ );
19791
20406
  let newest = null;
19792
20407
  for (const row of rows) {
19793
20408
  const at = row.recordedBy.lastIndexOf("@");
@@ -19802,13 +20417,15 @@ var SqliteInstalledPacksRepository = class {
19802
20417
  return newest;
19803
20418
  }
19804
20419
  counts() {
19805
- const row = this.db.prepare(
19806
- `SELECT count(*) AS packs,
20420
+ const row = getRow(
20421
+ this.db.prepare(
20422
+ `SELECT count(*) AS packs,
19807
20423
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19808
20424
  coalesce(sum(enabled), 0) AS enabled
19809
20425
  FROM installed_packs`
19810
- ).get();
19811
- return Promise.resolve(row);
20426
+ )
20427
+ );
20428
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19812
20429
  }
19813
20430
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19814
20431
  // Back the Policies page's built-in catalog: how many
@@ -19821,26 +20438,29 @@ var SqliteInstalledPacksRepository = class {
19821
20438
  * attributed to Monitor, matching the Detections views.
19822
20439
  */
19823
20440
  countsByPolicyId() {
19824
- const rows = this.db.prepare(
19825
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20441
+ return countBy(
20442
+ this.db,
20443
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19826
20444
  FROM installed_packs
19827
- GROUP BY pid`
19828
- ).all();
19829
- return new Map(rows.map((r) => [r.pid, r.n]));
20445
+ GROUP BY k`
20446
+ );
19830
20447
  }
19831
20448
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19832
20449
  listByPolicyId(policyId) {
19833
- const rows = this.db.prepare(
19834
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20450
+ const rows = allRows(
20451
+ this.db.prepare(
20452
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19835
20453
  FROM installed_packs
19836
20454
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19837
20455
  ORDER BY name ASC`
19838
- ).all(policyId);
20456
+ ),
20457
+ [policyId]
20458
+ );
19839
20459
  return rows.map((r) => ({
19840
20460
  id: `${r.namespace}/${r.packId}`,
19841
20461
  name: r.name,
19842
20462
  ruleCount: parseRules(r.rulesJson).length,
19843
- enabled: r.enabled === 1
20463
+ enabled: intToBool(r.enabled)
19844
20464
  }));
19845
20465
  }
19846
20466
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19869,14 +20489,14 @@ var SqliteInstalledPacksRepository = class {
19869
20489
  const res = this.db.prepare(
19870
20490
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19871
20491
  WHERE namespace = :namespace AND pack_id = :packId`
19872
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20492
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19873
20493
  return Number(res.changes) > 0;
19874
20494
  }
19875
20495
  // Fingerprint of the recorded available mirror — compared against the
19876
20496
  // incoming inventory's signature to skip the write entirely when the running
19877
20497
  // binary's inventory hasn't changed since the last record.
19878
20498
  storedSignature() {
19879
- const rows = this.signatureStmt.all();
20499
+ const rows = allRows(this.signatureStmt);
19880
20500
  return inventorySignature(rows);
19881
20501
  }
19882
20502
  };
@@ -19904,42 +20524,48 @@ var SqliteInventoryRepository = class {
19904
20524
  upsert(input, now = Date.now()) {
19905
20525
  const id = inventoryId(input.objectType, input.identityKey);
19906
20526
  const row = toInventoryRow(input, id, now);
19907
- this.upsertStmt.run({
19908
- id: row.id,
19909
- objectType: row.objectType,
19910
- location: row.location ?? null,
19911
- title: row.title ?? null,
19912
- hostId: row.hostId ?? null,
19913
- attributes: row.attributes,
19914
- firstSeen: row.firstSeen,
19915
- lastSeen: row.lastSeen
19916
- });
20527
+ this.upsertStmt.run(
20528
+ bindParams({
20529
+ id: row.id,
20530
+ objectType: row.objectType,
20531
+ location: row.location,
20532
+ title: row.title,
20533
+ hostId: row.hostId,
20534
+ attributes: row.attributes,
20535
+ firstSeen: row.firstSeen,
20536
+ lastSeen: row.lastSeen
20537
+ })
20538
+ );
19917
20539
  return id;
19918
20540
  }
19919
20541
  // The full row, for round-trip assertions.
19920
20542
  findById(id) {
19921
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
19922
- return row;
20543
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
19923
20544
  }
19924
20545
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
19925
20546
  // served from the object_type index, never from audit_events.
19926
20547
  distinctTitles(objectType) {
19927
- const rows = this.db.prepare(
19928
- `SELECT DISTINCT title FROM inventory
20548
+ const rows = allRows(
20549
+ this.db.prepare(
20550
+ `SELECT DISTINCT title FROM inventory
19929
20551
  WHERE object_type = :objectType AND title IS NOT NULL
19930
20552
  ORDER BY title`
19931
- ).all({ objectType });
20553
+ ),
20554
+ { objectType }
20555
+ );
19932
20556
  return rows.map((r) => r.title);
19933
20557
  }
19934
20558
  // Distinct host os_version values — a facet served from an inventory index
19935
20559
  // over the generated column, never from the audit fact (confirm via EXPLAIN
19936
20560
  // QUERY PLAN).
19937
20561
  osVersions() {
19938
- const rows = this.db.prepare(
19939
- `SELECT DISTINCT os_version AS value FROM inventory
20562
+ const rows = allRows(
20563
+ this.db.prepare(
20564
+ `SELECT DISTINCT os_version AS value FROM inventory
19940
20565
  WHERE object_type = 'host' AND os_version IS NOT NULL
19941
20566
  ORDER BY value`
19942
- ).all();
20567
+ )
20568
+ );
19943
20569
  return rows.map((r) => r.value);
19944
20570
  }
19945
20571
  };
@@ -19959,14 +20585,6 @@ var EMPTY_PROJECT_AGG = {
19959
20585
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
19960
20586
  findingsCount: 0
19961
20587
  };
19962
- function safeJson(s, fallback) {
19963
- if (s == null) return fallback;
19964
- try {
19965
- return JSON.parse(s);
19966
- } catch {
19967
- return fallback;
19968
- }
19969
- }
19970
20588
  function resolveHarnessId(attrs, row) {
19971
20589
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
19972
20590
  return attrs.provider;
@@ -20162,30 +20780,49 @@ var SqliteInventoryAssetsRepository = class {
20162
20780
  configRowsCache;
20163
20781
  // ─── stats ─────────────────────────────────────────────────────────────────
20164
20782
  getInventoryStats() {
20165
- const byType = { project: 0, skill: 0, mcp: 0, hook: 0, config: 0 };
20166
- for (const r of this.db.prepare("SELECT asset_type AS t, count(*) AS n FROM inventory_asset GROUP BY asset_type").all()) {
20167
- if (r.t in byType) byType[r.t] = r.n;
20168
- }
20169
- byType.project = this.db.prepare(`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`).get().n;
20170
- const mcpTrust = { "known-good": 0, risky: 0, unapproved: 0 };
20171
- for (const r of this.db.prepare(
20172
- `SELECT coalesce(o.trust, a.trust) AS trust, count(*) AS n
20783
+ const typeCounts = countBy(
20784
+ this.db,
20785
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20786
+ );
20787
+ const byType = {
20788
+ project: 0,
20789
+ skill: typeCounts.get("skill") ?? 0,
20790
+ mcp: typeCounts.get("mcp") ?? 0,
20791
+ hook: typeCounts.get("hook") ?? 0,
20792
+ config: typeCounts.get("config") ?? 0
20793
+ };
20794
+ byType.project = countScalar(
20795
+ this.db,
20796
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20797
+ );
20798
+ const mcpTrustCounts = countBy(
20799
+ this.db,
20800
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20173
20801
  FROM inventory_asset a
20174
20802
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20175
20803
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20176
20804
  GROUP BY coalesce(o.trust, a.trust)`
20177
- ).all()) {
20178
- if (r.trust in mcpTrust) mcpTrust[r.trust] = r.n;
20179
- }
20180
- const harnesses = this.db.prepare(
20805
+ );
20806
+ const mcpTrust = {
20807
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20808
+ risky: mcpTrustCounts.get("risky") ?? 0,
20809
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20810
+ };
20811
+ const harnesses = countScalar(
20812
+ this.db,
20181
20813
  `SELECT count(*) AS n FROM inventory
20182
20814
  WHERE object_type = 'harness'
20183
- AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20184
- ).get({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }).n;
20185
- const flaggedAssets = this.db.prepare("SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'").get().n;
20186
- const flaggedProjects = this.db.prepare(
20815
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20816
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20817
+ );
20818
+ const flaggedAssets = countScalar(
20819
+ this.db,
20820
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20821
+ );
20822
+ const flaggedProjects = countScalar(
20823
+ this.db,
20187
20824
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20188
- ).get().n;
20825
+ );
20189
20826
  const configRows = this.configAssetRows();
20190
20827
  for (const r of configRows) {
20191
20828
  byType[r.assetType] += 1;
@@ -20442,12 +21079,15 @@ var SqliteInventoryAssetsRepository = class {
20442
21079
  }
20443
21080
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20444
21081
  fetchHarnessRows() {
20445
- return this.db.prepare(
20446
- `SELECT id, title, attributes, harness_version AS harnessVersion
21082
+ return allRows(
21083
+ this.db.prepare(
21084
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20447
21085
  FROM inventory
20448
21086
  WHERE object_type = 'harness'
20449
21087
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20450
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
21088
+ ),
21089
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
21090
+ );
20451
21091
  }
20452
21092
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20453
21093
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20457,12 +21097,13 @@ var SqliteInventoryAssetsRepository = class {
20457
21097
  const params = [...harnessInvIds];
20458
21098
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20459
21099
  if (q) {
20460
- const pat = `%${escapeLikePattern(q)}%`;
20461
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
21100
+ const pat = containsPattern(q);
21101
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20462
21102
  params.push(pat, pat);
20463
21103
  }
20464
- const rows = this.db.prepare(
20465
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
21104
+ const rows = allRows(
21105
+ this.db.prepare(
21106
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20466
21107
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20467
21108
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20468
21109
  FROM harness_asset ha
@@ -20470,7 +21111,9 @@ var SqliteInventoryAssetsRepository = class {
20470
21111
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20471
21112
  WHERE ${where}
20472
21113
  ORDER BY a.name ASC`
20473
- ).all(...params);
21114
+ ),
21115
+ params
21116
+ );
20474
21117
  for (const raw of rows) {
20475
21118
  const harnessInvId = raw.harnessInvId;
20476
21119
  const [asset] = this.mapAssetRows([raw]);
@@ -20489,21 +21132,24 @@ var SqliteInventoryAssetsRepository = class {
20489
21132
  params.push(...types);
20490
21133
  }
20491
21134
  if (q) {
20492
- const pat = `%${escapeLikePattern(q)}%`;
20493
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
21135
+ const pat = containsPattern(q);
21136
+ conditions.push(likeAny(["a.name", "a.sub"]));
20494
21137
  params.push(pat, pat);
20495
21138
  }
20496
21139
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20497
21140
  const sampleRows = this.mapAssetRows(
20498
- this.db.prepare(
20499
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21141
+ allRows(
21142
+ this.db.prepare(
21143
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20500
21144
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20501
21145
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20502
21146
  FROM inventory_asset a
20503
21147
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20504
21148
  ${where}
20505
21149
  ORDER BY a.name ASC`
20506
- ).all(...params)
21150
+ ),
21151
+ params
21152
+ )
20507
21153
  );
20508
21154
  const configRows = this.configAssetRows(q).filter(
20509
21155
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20512,14 +21158,17 @@ var SqliteInventoryAssetsRepository = class {
20512
21158
  }
20513
21159
  fetchAssetById(assetId) {
20514
21160
  const rows = this.mapAssetRows(
20515
- this.db.prepare(
20516
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21161
+ allRows(
21162
+ this.db.prepare(
21163
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20517
21164
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20518
21165
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20519
21166
  FROM inventory_asset a
20520
21167
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20521
21168
  WHERE a.id = ?`
20522
- ).all(assetId)
21169
+ ),
21170
+ [assetId]
21171
+ )
20523
21172
  );
20524
21173
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20525
21174
  }
@@ -20575,37 +21224,39 @@ var SqliteInventoryAssetsRepository = class {
20575
21224
  return rows;
20576
21225
  }
20577
21226
  latestConfigScanId() {
20578
- const row = this.db.prepare(
20579
- `SELECT id FROM audit_events WHERE event_type = 'config_scan'
20580
- ORDER BY started_at DESC, id DESC LIMIT 1`
20581
- ).get();
20582
- return row?.id ?? null;
21227
+ return latestConfigScan(this.db)?.id ?? null;
20583
21228
  }
20584
21229
  fetchProjects(q) {
20585
21230
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20586
21231
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20587
21232
  const params = [];
20588
21233
  if (q) {
20589
- const pat = `%${escapeLikePattern(q)}%`;
20590
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
21234
+ const pat = containsPattern(q);
21235
+ sql += ` AND ${likeAny(["name", "url"])}`;
20591
21236
  params.push(pat, pat);
20592
21237
  }
20593
21238
  sql += " ORDER BY name ASC";
20594
- return this.db.prepare(sql).all(...params);
21239
+ return allRows(this.db.prepare(sql), params);
20595
21240
  }
20596
21241
  fetchProjectById(projectId) {
20597
- return this.db.prepare(
20598
- "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20599
- ).get(projectId) ?? null;
21242
+ return getRow(
21243
+ this.db.prepare(
21244
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21245
+ ),
21246
+ [projectId]
21247
+ ) ?? null;
20600
21248
  }
20601
21249
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20602
21250
  fetchProjectsByIds(projectIds) {
20603
21251
  const map2 = /* @__PURE__ */ new Map();
20604
21252
  if (projectIds.length === 0) return map2;
20605
- const rows = this.db.prepare(
20606
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21253
+ const rows = allRows(
21254
+ this.db.prepare(
21255
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20607
21256
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20608
- ).all(...projectIds);
21257
+ ),
21258
+ projectIds
21259
+ );
20609
21260
  for (const r of rows) map2.set(r.id, r);
20610
21261
  return map2;
20611
21262
  }
@@ -20616,8 +21267,9 @@ var SqliteInventoryAssetsRepository = class {
20616
21267
  projectAggregates(projectIds) {
20617
21268
  const map2 = /* @__PURE__ */ new Map();
20618
21269
  if (projectIds.length === 0) return map2;
20619
- const rows = this.db.prepare(
20620
- `SELECT f.project_id AS projectId,
21270
+ const rows = allRows(
21271
+ this.db.prepare(
21272
+ `SELECT f.project_id AS projectId,
20621
21273
  coalesce(o.access, f.default_access) AS eff,
20622
21274
  count(*) AS n,
20623
21275
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20625,7 +21277,9 @@ var SqliteInventoryAssetsRepository = class {
20625
21277
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20626
21278
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20627
21279
  GROUP BY f.project_id, eff`
20628
- ).all(...projectIds);
21280
+ ),
21281
+ projectIds
21282
+ );
20629
21283
  for (const r of rows) {
20630
21284
  let agg = map2.get(r.projectId);
20631
21285
  if (!agg) {
@@ -20663,37 +21317,52 @@ var SqliteInventoryAssetsRepository = class {
20663
21317
  fetchProjectFilesUnder(projectId, prefix) {
20664
21318
  if (prefix === "") {
20665
21319
  return this.mapFileRows(
20666
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21320
+ allRows(
21321
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21322
+ [projectId]
21323
+ )
20667
21324
  );
20668
21325
  }
20669
21326
  return this.mapFileRows(
20670
- this.db.prepare(
20671
- this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
20672
- ).all(projectId, `${escapeLikePattern(prefix)}/%`)
21327
+ allRows(
21328
+ this.db.prepare(
21329
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21330
+ ),
21331
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21332
+ )
20673
21333
  );
20674
21334
  }
20675
21335
  fetchProjectFilesSearch(projectId, q) {
20676
- const pat = `%${escapeLikePattern(q)}%`;
21336
+ const pat = containsPattern(q);
20677
21337
  return this.mapFileRows(
20678
- this.db.prepare(
20679
- this.fileSelect(
20680
- "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
20681
- )
20682
- ).all(projectId, pat, pat)
21338
+ allRows(
21339
+ this.db.prepare(
21340
+ this.fileSelect(
21341
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21342
+ )
21343
+ ),
21344
+ [projectId, pat, pat]
21345
+ )
20683
21346
  );
20684
21347
  }
20685
21348
  fetchProjectFilesBlocked(projectId) {
20686
21349
  return this.mapFileRows(
20687
- this.db.prepare(
20688
- this.fileSelect(
20689
- "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
20690
- )
20691
- ).all(projectId)
21350
+ allRows(
21351
+ this.db.prepare(
21352
+ this.fileSelect(
21353
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21354
+ )
21355
+ ),
21356
+ [projectId]
21357
+ )
20692
21358
  );
20693
21359
  }
20694
21360
  fetchProjectFile(projectId, path) {
20695
21361
  const rows = this.mapFileRows(
20696
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21362
+ allRows(
21363
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21364
+ [projectId, path]
21365
+ )
20697
21366
  );
20698
21367
  return rows[0] ?? null;
20699
21368
  }
@@ -20707,39 +21376,32 @@ var SqlitePoliciesRepository = class {
20707
21376
  }
20708
21377
  db;
20709
21378
  readPolicies() {
20710
- const rows = this.db.prepare("SELECT * FROM policies").all();
20711
- const policies = [];
20712
- for (const row of rows) {
20713
- try {
20714
- const target = JSON.parse(row.target);
20715
- const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
20716
- policies.push(
20717
- Policy.parse({
20718
- id: row.id,
20719
- scope: row.scope,
20720
- target,
20721
- action: row.action,
20722
- enabled: row.enabled === 1,
20723
- customKeywords
20724
- })
20725
- );
20726
- } catch {
20727
- }
20728
- }
21379
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21380
+ const policies = mapRowsTolerant(rows, (row) => {
21381
+ const target = JSON.parse(row.target);
21382
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21383
+ return Policy.parse({
21384
+ id: row.id,
21385
+ scope: row.scope,
21386
+ target,
21387
+ action: row.action,
21388
+ enabled: intToBool(row.enabled),
21389
+ customKeywords
21390
+ });
21391
+ });
20729
21392
  return Promise.resolve(policies);
20730
21393
  }
20731
21394
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20732
21395
  // detection-type config exists from first run. Only when the table is empty,
20733
21396
  // so a user's edits are never clobbered.
20734
21397
  seedDefaults() {
20735
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21398
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20736
21399
  if (count > 0) return;
20737
21400
  const stmt = this.db.prepare(
20738
21401
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20739
21402
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20740
21403
  );
20741
- this.db.exec("BEGIN");
20742
- try {
21404
+ failOpenTransaction(this.db, () => {
20743
21405
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20744
21406
  stmt.run({
20745
21407
  id: randomUUID4(),
@@ -20748,10 +21410,41 @@ var SqlitePoliciesRepository = class {
20748
21410
  now: Date.now()
20749
21411
  });
20750
21412
  }
20751
- this.db.exec("COMMIT");
20752
- } catch {
20753
- this.db.exec("ROLLBACK");
20754
- }
21413
+ });
21414
+ }
21415
+ // Insert-or-update the single global per-category policy row, keyed on the
21416
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21417
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21418
+ // values), so the runtime's resolveAction reads rows written by either path
21419
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21420
+ // `id` and `created_at` are left exactly as they were.
21421
+ upsertCategoryAction(category, action) {
21422
+ const now = Date.now();
21423
+ this.db.prepare(
21424
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21425
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21426
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21427
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21428
+ }
21429
+ // Caps every global per-category policy currently set to block/redact down
21430
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21431
+ // Returns the number of rows changed.
21432
+ capCategoryActions() {
21433
+ const info = this.db.prepare(
21434
+ `UPDATE policies SET action='warn', updated_at=:now
21435
+ WHERE scope='global' AND action IN ('block','redact')
21436
+ AND json_extract(target,'$.category') IS NOT NULL`
21437
+ ).run({ now: Date.now() });
21438
+ return Number(info.changes);
21439
+ }
21440
+ // Read the current action for a single global per-category policy row, mirroring
21441
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21442
+ // row exists yet, so callers can distinguish an unset category from a set one.
21443
+ getCategoryAction(category) {
21444
+ const row = this.db.prepare(
21445
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21446
+ ).get({ category });
21447
+ return row?.action;
20755
21448
  }
20756
21449
  };
20757
21450
 
@@ -20827,7 +21520,7 @@ var SqliteProjectFilesRepository = class {
20827
21520
  maxStampStmt;
20828
21521
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20829
21522
  replaceForProject(projectId, scan2, now) {
20830
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21523
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20831
21524
  const stamp = Math.max(now, maxStamp + 1);
20832
21525
  for (const file2 of scan2.files) {
20833
21526
  this.upsertStmt.run({
@@ -20910,7 +21603,7 @@ var SqliteResolutionsRepository = class {
20910
21603
  }
20911
21604
  /** The newest disposition recorded for a finding key, or undefined if none. */
20912
21605
  latestByKey(key) {
20913
- const row = this.latestStmt.get({ findingKey: key });
21606
+ const row = getRow(this.latestStmt, { findingKey: key });
20914
21607
  if (!row) return void 0;
20915
21608
  return {
20916
21609
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -20927,7 +21620,7 @@ var SqliteResolutionsRepository = class {
20927
21620
  * the CLI) surfaces for that file.
20928
21621
  */
20929
21622
  openAtRestKeysForPath(path) {
20930
- const rows = this.openAtRestStmt.all({ path });
21623
+ const rows = allRows(this.openAtRestStmt, { path });
20931
21624
  return rows.map((r) => r.finding_key);
20932
21625
  }
20933
21626
  /**
@@ -20938,7 +21631,7 @@ var SqliteResolutionsRepository = class {
20938
21631
  * resolution row (see scan.ts).
20939
21632
  */
20940
21633
  resolvedAtRestKeysForPath(path) {
20941
- const rows = this.resolvedAtRestStmt.all({ path });
21634
+ const rows = allRows(this.resolvedAtRestStmt, { path });
20942
21635
  return rows.map((r) => r.finding_key);
20943
21636
  }
20944
21637
  };
@@ -20967,38 +21660,31 @@ var SqliteScanLedgerRepository = class {
20967
21660
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
20968
21661
  // older ruleset are simply absent, which reads as "never scanned".
20969
21662
  entriesForRuleset(rulesetHash) {
20970
- const rows = this.readStmt.all({ rulesetHash });
21663
+ const rows = allRows(this.readStmt, {
21664
+ rulesetHash
21665
+ });
20971
21666
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
20972
21667
  }
20973
21668
  upsertEntries(entries) {
20974
21669
  if (entries.length === 0) return;
20975
21670
  const scannedAt = Date.now();
20976
- try {
20977
- this.db.exec("BEGIN");
20978
- try {
20979
- for (const entry of entries) {
20980
- this.upsertStmt.run({
20981
- path: entry.path,
20982
- mtime: entry.mtime,
20983
- contentHash: entry.contentHash,
20984
- rulesetHash: entry.rulesetHash,
20985
- scannedAt
20986
- });
20987
- }
20988
- this.db.exec("COMMIT");
20989
- } catch (err) {
20990
- this.db.exec("ROLLBACK");
20991
- throw err;
21671
+ failOpenTransaction(this.db, () => {
21672
+ for (const entry of entries) {
21673
+ this.upsertStmt.run({
21674
+ path: entry.path,
21675
+ mtime: entry.mtime,
21676
+ contentHash: entry.contentHash,
21677
+ rulesetHash: entry.rulesetHash,
21678
+ scannedAt
21679
+ });
20992
21680
  }
20993
- } catch {
20994
- }
21681
+ });
20995
21682
  }
20996
21683
  };
20997
21684
 
20998
21685
  // ../../packages/persistence/src/repositories/security.ts
20999
21686
  var DAY_MS4 = 864e5;
21000
21687
  var SEVERITIES = ["critical", "high", "medium", "low"];
21001
- var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
21002
21688
  var ACTION_TO_KIND = {
21003
21689
  block: "blocked",
21004
21690
  redact: "redacted",
@@ -21013,8 +21699,14 @@ var SCAN_COVERAGE = [
21013
21699
  { provider: "copilot", coverage: 0, supported: false },
21014
21700
  { provider: "api", coverage: 0, supported: false }
21015
21701
  ];
21702
+ var GRANULARITY = {
21703
+ "7d": "day",
21704
+ "30d": "day",
21705
+ "3m": "week",
21706
+ "6m": "week"
21707
+ };
21016
21708
  function granularityFor(range) {
21017
- return range === "7d" || range === "30d" ? "day" : "week";
21709
+ return GRANULARITY[range];
21018
21710
  }
21019
21711
  function startOfUtcDay2(ms) {
21020
21712
  return Math.floor(ms / DAY_MS4) * DAY_MS4;
@@ -21068,8 +21760,9 @@ var SqliteSecurityRepository = class {
21068
21760
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21069
21761
  // double-counting a key that accumulated several append-only rows.
21070
21762
  severitySummary() {
21071
- const rows = this.db.prepare(
21072
- `SELECT f.severity AS severity,
21763
+ const rows = allRows(
21764
+ this.db.prepare(
21765
+ `SELECT f.severity AS severity,
21073
21766
  COUNT(*) AS count,
21074
21767
  SUM(CASE
21075
21768
  WHEN e.kind != 'code_change' THEN 1
@@ -21088,7 +21781,8 @@ var SqliteSecurityRepository = class {
21088
21781
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21089
21782
  ON latest.finding_key = f.finding_key
21090
21783
  GROUP BY f.severity`
21091
- ).all();
21784
+ )
21785
+ );
21092
21786
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21093
21787
  const bySeverity = SEVERITIES.map((severity) => ({
21094
21788
  severity,
@@ -21172,14 +21866,15 @@ var SqliteSecurityRepository = class {
21172
21866
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21173
21867
  const now = this.now();
21174
21868
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
21175
- const rows = this.db.prepare(
21176
- // first_detected_at is the PRESERVED first-detection time (set once on a
21177
- // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21178
- // measures from first sighting — not the latest re-scan's event, whose
21179
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21180
- // the parent event's occurred_at defends against any legacy/edge row the
21181
- // backfill left null.
21182
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21869
+ const rows = allRows(
21870
+ this.db.prepare(
21871
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21872
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21873
+ // measures from first sighting — not the latest re-scan's event, whose
21874
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21875
+ // the parent event's occurred_at defends against any legacy/edge row the
21876
+ // backfill left null.
21877
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21183
21878
  (
21184
21879
  SELECT fr.status FROM finding_resolution fr
21185
21880
  WHERE fr.finding_key = f.finding_key
@@ -21205,14 +21900,16 @@ var SqliteSecurityRepository = class {
21205
21900
  WHERE fr.finding_key = f.finding_key
21206
21901
  AND fr.resolved_at >= :windowStart
21207
21902
  )`
21208
- // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21209
- // any resolution activity at/after the window start — a row this method
21210
- // ultimately counts has its LATEST resolution inside the window, which
21211
- // implies such a row exists, so nothing wanted is dropped. The exact
21212
- // latest-wins + status/method + window gate stays in JS below,
21213
- // dialect-agnostic. Without this, a
21214
- // 7d request evaluated the store's entire trackable-findings history.
21215
- ).all({ windowStart });
21903
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21904
+ // any resolution activity at/after the window start — a row this method
21905
+ // ultimately counts has its LATEST resolution inside the window, which
21906
+ // implies such a row exists, so nothing wanted is dropped. The exact
21907
+ // latest-wins + status/method + window gate stays in JS below,
21908
+ // dialect-agnostic. Without this, a
21909
+ // 7d request evaluated the store's entire trackable-findings history.
21910
+ ),
21911
+ { windowStart }
21912
+ );
21216
21913
  const sums = /* @__PURE__ */ new Map();
21217
21914
  const counts = /* @__PURE__ */ new Map();
21218
21915
  for (const r of rows) {
@@ -21242,8 +21939,9 @@ var SqliteSecurityRepository = class {
21242
21939
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21243
21940
  const now = this.now();
21244
21941
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21245
- const rows = this.db.prepare(
21246
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21942
+ const rows = allRows(
21943
+ this.db.prepare(
21944
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21247
21945
  FROM findings f JOIN events e ON e.id = f.event_id
21248
21946
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21249
21947
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21251,7 +21949,9 @@ var SqliteSecurityRepository = class {
21251
21949
  GROUP BY repo
21252
21950
  ORDER BY c DESC, repo
21253
21951
  LIMIT :limit`
21254
- ).all({ from, to: now, limit });
21952
+ ),
21953
+ { from, to: now, limit }
21954
+ );
21255
21955
  const items = rows.map((r) => ({
21256
21956
  id: `repo_${r.repo}`,
21257
21957
  name: r.repo,
@@ -21273,8 +21973,9 @@ var SqliteSecurityRepository = class {
21273
21973
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21274
21974
  // capped at `limit`.
21275
21975
  recentlyResolved(limit = 20) {
21276
- const rows = this.db.prepare(
21277
- `SELECT f.finding_key AS finding_key,
21976
+ const rows = allRows(
21977
+ this.db.prepare(
21978
+ `SELECT f.finding_key AS finding_key,
21278
21979
  f.rule_id AS rule_id,
21279
21980
  f.severity AS severity,
21280
21981
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21308,7 +22009,9 @@ var SqliteSecurityRepository = class {
21308
22009
  ) IS NOT NULL
21309
22010
  ORDER BY latest_resolved_at DESC
21310
22011
  LIMIT :limit`
21311
- ).all({ limit });
22012
+ ),
22013
+ { limit }
22014
+ );
21312
22015
  const items = rows.map((r) => ({
21313
22016
  findingKey: r.finding_key,
21314
22017
  ruleId: r.rule_id,
@@ -21325,12 +22028,15 @@ var SqliteSecurityRepository = class {
21325
22028
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21326
22029
  // numeric and the JS aggregations bucket/split on ms directly.
21327
22030
  findingsInRange(fromMs, toMs) {
21328
- const rows = this.db.prepare(
21329
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22031
+ const rows = allRows(
22032
+ this.db.prepare(
22033
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21330
22034
  FROM findings f JOIN events e ON e.id = f.event_id
21331
22035
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21332
22036
  ORDER BY e.occurred_at`
21333
- ).all({ from: fromMs, to: toMs });
22037
+ ),
22038
+ { from: fromMs, to: toMs }
22039
+ );
21334
22040
  return rows.map((r) => ({
21335
22041
  occurredAt: r.occurred_at,
21336
22042
  severity: r.severity,
@@ -21344,12 +22050,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21344
22050
  var KIND_ORDER = ["provider", "internal", "ip"];
21345
22051
  var CALL_SITE_EMBED_CAP = 200;
21346
22052
  function parseNetwork(networkJson) {
21347
- if (!networkJson) return null;
21348
- try {
21349
- return JSON.parse(networkJson);
21350
- } catch {
21351
- return null;
21352
- }
22053
+ return safeJson(networkJson, null);
21353
22054
  }
21354
22055
  function toEndpointSummary(row) {
21355
22056
  return {
@@ -21436,27 +22137,39 @@ var SqliteSharesRepository = class {
21436
22137
  }
21437
22138
  db;
21438
22139
  stats() {
21439
- const scalar = (sql) => this.db.prepare(sql).get()?.n ?? 0;
21440
- const destinations = scalar("SELECT count(*) AS n FROM share_destination");
21441
- const endpoints = scalar("SELECT count(*) AS n FROM share_endpoint");
21442
- const callSites = scalar("SELECT count(*) AS n FROM share_call_site");
21443
- const insecure = scalar(
22140
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
22141
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
22142
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22143
+ const insecure = countScalar(
22144
+ this.db,
21444
22145
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21445
22146
  );
21446
- const needsReview = scalar(
22147
+ const needsReview = countScalar(
22148
+ this.db,
21447
22149
  `SELECT count(DISTINCT d.id) AS n
21448
22150
  FROM share_destination d
21449
22151
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21450
22152
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21451
22153
  );
21452
- const byKind = { provider: 0, internal: 0, ip: 0 };
21453
- for (const row of this.db.prepare("SELECT kind, count(*) AS n FROM share_destination GROUP BY kind").all()) {
21454
- byKind[row.kind] = row.n;
21455
- }
21456
- const byTrust = { recognized: 0, internal: 0, unverified: 0, ip: 0 };
21457
- for (const row of this.db.prepare("SELECT trust, count(*) AS n FROM share_destination GROUP BY trust").all()) {
21458
- byTrust[row.trust] = row.n;
21459
- }
22154
+ const kindCounts = countBy(
22155
+ this.db,
22156
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
22157
+ );
22158
+ const byKind = {
22159
+ provider: kindCounts.get("provider") ?? 0,
22160
+ internal: kindCounts.get("internal") ?? 0,
22161
+ ip: kindCounts.get("ip") ?? 0
22162
+ };
22163
+ const trustCounts = countBy(
22164
+ this.db,
22165
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
22166
+ );
22167
+ const byTrust = {
22168
+ recognized: trustCounts.get("recognized") ?? 0,
22169
+ internal: trustCounts.get("internal") ?? 0,
22170
+ unverified: trustCounts.get("unverified") ?? 0,
22171
+ ip: trustCounts.get("ip") ?? 0
22172
+ };
21460
22173
  return Promise.resolve({
21461
22174
  destinations,
21462
22175
  endpoints,
@@ -21572,7 +22285,7 @@ var SqliteSharesRepository = class {
21572
22285
  }
21573
22286
  let sql;
21574
22287
  if (q) {
21575
- const pattern = `%${escapeLikePattern(q)}%`;
22288
+ const pattern = containsPattern(q);
21576
22289
  conditions.push(
21577
22290
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21578
22291
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21592,24 +22305,31 @@ var SqliteSharesRepository = class {
21592
22305
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21593
22306
  ORDER BY d.created_at ASC, d.id ASC`;
21594
22307
  }
21595
- const rows = this.db.prepare(sql).all(...params);
22308
+ const rows = allRows(
22309
+ this.db.prepare(sql),
22310
+ params
22311
+ );
21596
22312
  return rows.map((r) => this.mapDestRow(r));
21597
22313
  }
21598
22314
  fetchDestinationById(destinationId) {
21599
- const row = this.db.prepare(
21600
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22315
+ const row = getRow(
22316
+ this.db.prepare(
22317
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21601
22318
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21602
22319
  o.decision AS overrideDecision
21603
22320
  FROM share_destination d
21604
22321
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21605
22322
  WHERE d.id = ?`
21606
- ).get(destinationId);
22323
+ ),
22324
+ [destinationId]
22325
+ );
21607
22326
  return row ? this.mapDestRow(row) : null;
21608
22327
  }
21609
22328
  fetchEndpoints(destinationIds) {
21610
22329
  if (destinationIds.length === 0) return [];
21611
- const rows = this.db.prepare(
21612
- `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22330
+ const rows = allRows(
22331
+ this.db.prepare(
22332
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21613
22333
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21614
22334
  count(c.id) AS callSiteCount
21615
22335
  FROM share_endpoint e
@@ -21617,7 +22337,9 @@ var SqliteSharesRepository = class {
21617
22337
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21618
22338
  GROUP BY e.id
21619
22339
  ORDER BY e.created_at ASC, e.id ASC`
21620
- ).all(...destinationIds);
22340
+ ),
22341
+ destinationIds
22342
+ );
21621
22343
  return rows.map((r) => ({
21622
22344
  id: r.id,
21623
22345
  destinationId: r.destinationId,
@@ -21642,13 +22364,16 @@ var SqliteSharesRepository = class {
21642
22364
  }
21643
22365
  fetchCallSites(endpointIds) {
21644
22366
  if (endpointIds.length === 0) return [];
21645
- const rows = this.db.prepare(
21646
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22367
+ const rows = allRows(
22368
+ this.db.prepare(
22369
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21647
22370
  project_id AS projectId
21648
22371
  FROM share_call_site
21649
22372
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21650
22373
  ORDER BY created_at ASC, id ASC`
21651
- ).all(...endpointIds);
22374
+ ),
22375
+ endpointIds
22376
+ );
21652
22377
  return rows.map((r) => ({
21653
22378
  id: r.id,
21654
22379
  endpointId: r.endpointId,
@@ -21684,27 +22409,36 @@ var SqliteSourceProjectRepository = class {
21684
22409
  upsert(input, now = Date.now()) {
21685
22410
  const id = sourceProjectId(input.url);
21686
22411
  const row = toSourceProjectRow(input, id, now);
21687
- this.upsertStmt.run({
21688
- id: row.id,
21689
- url: row.url ?? null,
21690
- name: row.name ?? null,
21691
- attributes: row.attributes,
21692
- firstSeen: row.firstSeen,
21693
- lastSeen: row.lastSeen
21694
- });
22412
+ this.upsertStmt.run(
22413
+ bindParams({
22414
+ id: row.id,
22415
+ url: row.url,
22416
+ name: row.name,
22417
+ attributes: row.attributes,
22418
+ firstSeen: row.firstSeen,
22419
+ lastSeen: row.lastSeen
22420
+ })
22421
+ );
21695
22422
  return id;
21696
22423
  }
21697
22424
  findById(id) {
21698
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22425
+ return getRow(
22426
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22427
+ {
22428
+ id
22429
+ }
22430
+ );
21699
22431
  }
21700
22432
  // Distinct project names — a filter facet, served from the source_project
21701
22433
  // table, never from the audit fact table.
21702
22434
  distinctNames() {
21703
- const rows = this.db.prepare(
21704
- `SELECT DISTINCT name FROM source_project
22435
+ const rows = allRows(
22436
+ this.db.prepare(
22437
+ `SELECT DISTINCT name FROM source_project
21705
22438
  WHERE name IS NOT NULL
21706
22439
  ORDER BY name`
21707
- ).all();
22440
+ )
22441
+ );
21708
22442
  return rows.map((r) => r.name);
21709
22443
  }
21710
22444
  };
@@ -21723,8 +22457,7 @@ function hasLegacySampleRows(db) {
21723
22457
  function purgeSampleData(db) {
21724
22458
  try {
21725
22459
  if (!hasLegacySampleRows(db)) return;
21726
- db.exec("BEGIN");
21727
- try {
22460
+ withTransaction(db, () => {
21728
22461
  db.exec(
21729
22462
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21730
22463
  SELECT e.id FROM share_endpoint e
@@ -21769,11 +22502,7 @@ function purgeSampleData(db) {
21769
22502
  value TEXT NOT NULL
21770
22503
  )`);
21771
22504
  db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
21772
- db.exec("COMMIT");
21773
- } catch (err) {
21774
- db.exec("ROLLBACK");
21775
- throw err;
21776
- }
22505
+ });
21777
22506
  } catch {
21778
22507
  }
21779
22508
  }
@@ -21792,7 +22521,7 @@ function openWithPragmas(file2) {
21792
22521
  function backupLegacyStore(file2) {
21793
22522
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21794
22523
  renameSync(file2, backup);
21795
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22524
+ for (const sidecar of walSidecars(file2)) {
21796
22525
  if (existsSync(sidecar)) rmSync(sidecar);
21797
22526
  }
21798
22527
  return backup;
@@ -21805,9 +22534,8 @@ function openLocalDatabase(dir) {
21805
22534
  db.close();
21806
22535
  const backup = backupLegacyStore(file2);
21807
22536
  db = openWithPragmas(file2);
21808
- process.stderr.write(
21809
- `[aka] Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.
21810
- `
22537
+ akaWarn(
22538
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21811
22539
  );
21812
22540
  }
21813
22541
  applyMigrations(db);
@@ -21835,96 +22563,77 @@ function openLocalDatabase(dir) {
21835
22563
  const configInventory = new SqliteConfigInventoryRepository(db);
21836
22564
  policies.seedDefaults();
21837
22565
  function recordCapture(event, detected) {
21838
- try {
21839
- db.exec("BEGIN");
21840
- try {
21841
- events.insertEvent(event);
21842
- const sessionId = event.metadata?.sessionId;
21843
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
21844
- db.exec("COMMIT");
21845
- } catch (err) {
21846
- db.exec("ROLLBACK");
21847
- throw err;
21848
- }
21849
- } catch {
21850
- }
22566
+ failOpenTransaction(db, () => {
22567
+ events.insertEvent(event);
22568
+ const sessionId = event.metadata?.sessionId;
22569
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22570
+ });
21851
22571
  }
21852
22572
  function ensureInventory(ctx) {
21853
22573
  const resolved = {};
21854
- try {
21855
- db.exec("BEGIN");
21856
- try {
21857
- const now = Date.now();
21858
- if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
21859
- if (ctx.harness) {
21860
- resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21861
- }
21862
- resolved.accountId = inventory.upsert(
21863
- linkHost(
21864
- {
21865
- objectType: "user",
21866
- identityKey: "local",
21867
- attributes: { source: "local" }
21868
- },
21869
- resolved.hostId
21870
- ),
21871
- now
21872
- );
21873
- if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
21874
- db.exec("COMMIT");
21875
- } catch (err) {
21876
- db.exec("ROLLBACK");
21877
- throw err;
21878
- }
21879
- } catch {
21880
- return {};
21881
- }
21882
- return resolved;
22574
+ const committed = failOpenTransaction(db, () => {
22575
+ const now = Date.now();
22576
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22577
+ if (ctx.harness) {
22578
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
22579
+ }
22580
+ resolved.accountId = inventory.upsert(
22581
+ linkHost(
22582
+ {
22583
+ objectType: "user",
22584
+ identityKey: "local",
22585
+ attributes: { source: "local" }
22586
+ },
22587
+ resolved.hostId
22588
+ ),
22589
+ now
22590
+ );
22591
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22592
+ });
22593
+ return committed ? resolved : {};
21883
22594
  }
21884
22595
  function recordConfigScan(record2) {
21885
- try {
21886
- db.exec("BEGIN");
21887
- try {
21888
- const now = isoToEpochMillis(record2.scanEvent.startedAt);
21889
- for (const item of record2.items) inventory.upsert(item, now);
21890
- auditEvents.insertAuditEvent(record2.scanEvent);
21891
- const definitionIds = /* @__PURE__ */ new Map();
21892
- for (const def of record2.definitions ?? []) {
21893
- definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
21894
- }
21895
- for (const finding of record2.findings ?? []) {
21896
- const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
21897
- if (!definitionId) continue;
21898
- inspectionFindings.insertFinding({
21899
- id: randomUUID8(),
21900
- auditEventId: record2.scanEvent.id,
21901
- inspectionDefinitionId: definitionId,
21902
- span: finding.span,
21903
- maskedMatch: finding.maskedMatch,
21904
- actionTaken: finding.actionTaken,
21905
- confidence: finding.confidence
21906
- });
21907
- }
21908
- db.exec("COMMIT");
21909
- } catch (err) {
21910
- db.exec("ROLLBACK");
21911
- throw err;
22596
+ failOpenTransaction(db, () => {
22597
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22598
+ for (const item of record2.items) inventory.upsert(item, now);
22599
+ auditEvents.insertAuditEvent(record2.scanEvent);
22600
+ const definitionIds = /* @__PURE__ */ new Map();
22601
+ for (const def of record2.definitions ?? []) {
22602
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22603
+ }
22604
+ for (const finding of record2.findings ?? []) {
22605
+ const definitionId = definitionIds.get(`${finding.ruleId}@${finding.version}`);
22606
+ if (!definitionId) continue;
22607
+ inspectionFindings.insertFinding({
22608
+ id: randomUUID8(),
22609
+ auditEventId: record2.scanEvent.id,
22610
+ inspectionDefinitionId: definitionId,
22611
+ span: finding.span,
22612
+ maskedMatch: finding.maskedMatch,
22613
+ actionTaken: finding.actionTaken,
22614
+ confidence: finding.confidence
22615
+ });
21912
22616
  }
21913
- } catch {
21914
- }
22617
+ });
21915
22618
  }
21916
22619
  function recordProjectFiles(projectId, scan2) {
21917
22620
  if (scan2.files.length === 0) return;
22621
+ failOpenTransaction(db, () => {
22622
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22623
+ });
22624
+ }
22625
+ async function transaction(fn) {
22626
+ db.exec("BEGIN");
21918
22627
  try {
21919
- db.exec("BEGIN");
22628
+ const result = await fn();
22629
+ db.exec("COMMIT");
22630
+ return result;
22631
+ } catch (err) {
21920
22632
  try {
21921
- projectFiles.replaceForProject(projectId, scan2, Date.now());
21922
- db.exec("COMMIT");
21923
- } catch (err) {
21924
22633
  db.exec("ROLLBACK");
21925
- throw err;
22634
+ } catch {
21926
22635
  }
21927
- } catch {
22636
+ throw err;
21928
22637
  }
21929
22638
  }
21930
22639
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -21943,8 +22652,7 @@ function openLocalDatabase(dir) {
21943
22652
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
21944
22653
  });
21945
22654
  if (stale.length === 0) return;
21946
- db.exec("BEGIN");
21947
- try {
22655
+ withTransaction(db, () => {
21948
22656
  for (const { id } of stale) {
21949
22657
  db.prepare(
21950
22658
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -21956,11 +22664,7 @@ function openLocalDatabase(dir) {
21956
22664
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
21957
22665
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
21958
22666
  }
21959
- db.exec("COMMIT");
21960
- } catch (err) {
21961
- db.exec("ROLLBACK");
21962
- throw err;
21963
- }
22667
+ });
21964
22668
  } catch {
21965
22669
  }
21966
22670
  }
@@ -22002,6 +22706,7 @@ function openLocalDatabase(dir) {
22002
22706
  purgeSampleData: () => {
22003
22707
  purgeSampleData(db);
22004
22708
  },
22709
+ transaction,
22005
22710
  close: () => {
22006
22711
  db.close();
22007
22712
  }
@@ -22122,12 +22827,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22122
22827
  }
22123
22828
  }
22124
22829
  function readJson(file2) {
22830
+ let text;
22125
22831
  try {
22126
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22127
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22832
+ text = readFileSync2(file2, "utf8");
22128
22833
  } catch {
22129
22834
  return null;
22130
22835
  }
22836
+ return parseJsonObject(text) ?? null;
22837
+ }
22838
+
22839
+ // ../../packages/persistence/src/warn-era-cap.ts
22840
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22841
+ import { join as join5 } from "path";
22842
+ var MARKER = "warn-era-capped";
22843
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22844
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22845
+ const marker = join5(dataDir2, MARKER);
22846
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22847
+ const capped = db.policies.capCategoryActions();
22848
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22849
+ `, { mode: DATA_FILE_MODE });
22850
+ return { capped };
22131
22851
  }
22132
22852
 
22133
22853
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22212,23 +22932,30 @@ function resolveProviderSafe() {
22212
22932
  // ../../packages/plugin-sdk/src/config-inventory.ts
22213
22933
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22214
22934
  import { homedir as homedir2 } from "os";
22215
- import { basename as basename2, join as join6 } from "path";
22935
+ import { basename as basename2, join as join7 } from "path";
22936
+
22937
+ // ../../packages/detections/src/escape-regexp.ts
22938
+ function escapeRegExp(value) {
22939
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22940
+ }
22941
+
22942
+ // ../../packages/detections/src/matchers/limits.ts
22943
+ var MAX_MATCHES_PER_RULE = 1e4;
22216
22944
 
22217
22945
  // ../../packages/detections/src/matchers/keyword.ts
22218
22946
  var KeywordMatcher2 = class {
22219
22947
  match(text, rule) {
22220
22948
  if (rule.matcher.type !== "keyword") return [];
22221
22949
  const { keywords, caseSensitive } = rule.matcher;
22222
- const haystack = caseSensitive ? text : text.toLowerCase();
22223
22950
  const spans = [];
22224
22951
  for (const kw of keywords) {
22225
- const needle = caseSensitive ? kw : kw.toLowerCase();
22226
- let idx = 0;
22227
- while (idx < haystack.length) {
22228
- const pos = haystack.indexOf(needle, idx);
22229
- if (pos === -1) break;
22230
- spans.push({ start: pos, end: pos + kw.length });
22231
- idx = pos + 1;
22952
+ if (kw.length === 0) continue;
22953
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22954
+ const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
22955
+ let m;
22956
+ while ((m = re.exec(text)) !== null) {
22957
+ spans.push({ start: m.index, end: m.index + m[0].length });
22958
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22232
22959
  }
22233
22960
  }
22234
22961
  return spans;
@@ -22236,7 +22963,6 @@ var KeywordMatcher2 = class {
22236
22963
  };
22237
22964
 
22238
22965
  // ../../packages/detections/src/matchers/regex.ts
22239
- var MAX_MATCHES_PER_RULE = 1e4;
22240
22966
  var RegexMatcher2 = class {
22241
22967
  match(text, rule) {
22242
22968
  if (rule.matcher.type !== "regex") return [];
@@ -22322,9 +23048,6 @@ function registerPack(pack) {
22322
23048
  function getLoadedRules() {
22323
23049
  return [...packs.values()].flatMap((p) => p.rules);
22324
23050
  }
22325
- function escapeRegExp(value) {
22326
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22327
- }
22328
23051
  function isCorroborated(candidate, candidates, text) {
22329
23052
  const req = candidate.rule.requiresNearby;
22330
23053
  if (!req) return true;
@@ -23106,7 +23829,6 @@ var db_table_name_default = {
23106
23829
  "SELECT * FROM ",
23107
23830
  "SELECT COUNT(*) FROM ",
23108
23831
  "INSERT INTO ",
23109
- "UPDATE ",
23110
23832
  "DELETE FROM ",
23111
23833
  "CREATE TABLE ",
23112
23834
  "ALTER TABLE ",
@@ -24591,8 +25313,8 @@ function scanText(text) {
24591
25313
  }
24592
25314
 
24593
25315
  // ../../packages/plugin-sdk/src/repo.ts
24594
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
24595
- import { basename, dirname, isAbsolute, join as join5, sep as sep2 } from "path";
25316
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25317
+ import { basename, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24596
25318
  function resolveRepoIdentity(cwd) {
24597
25319
  try {
24598
25320
  const root = findGitRoot(cwd);
@@ -24625,32 +25347,32 @@ function resolveRepoNwo(cwd) {
24625
25347
  function findGitRoot(start) {
24626
25348
  let dir = start;
24627
25349
  for (; ; ) {
24628
- if (existsSync2(join5(dir, ".git"))) return dir;
25350
+ if (existsSync3(join6(dir, ".git"))) return dir;
24629
25351
  const parent = dirname(dir);
24630
25352
  if (parent === dir) return void 0;
24631
25353
  dir = parent;
24632
25354
  }
24633
25355
  }
24634
25356
  function resolveGitContext(root) {
24635
- const dotGit = join5(root, ".git");
25357
+ const dotGit = join6(root, ".git");
24636
25358
  try {
24637
25359
  if (statSync(dotGit).isDirectory()) {
24638
- return { configPath: join5(dotGit, "config"), headRoot: root };
25360
+ return { configPath: join6(dotGit, "config"), headRoot: root };
24639
25361
  }
24640
25362
  } catch {
24641
25363
  return void 0;
24642
25364
  }
24643
25365
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
24644
25366
  if (!target) return void 0;
24645
- const gitdir = isAbsolute(target) ? target : join5(root, target);
24646
- if (existsSync2(join5(gitdir, "config"))) {
24647
- return { configPath: join5(gitdir, "config"), headRoot: root };
25367
+ const gitdir = isAbsolute(target) ? target : join6(root, target);
25368
+ if (existsSync3(join6(gitdir, "config"))) {
25369
+ return { configPath: join6(gitdir, "config"), headRoot: root };
24648
25370
  }
24649
- const commonRaw = safeRead(join5(gitdir, "commondir"))?.trim();
25371
+ const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
24650
25372
  if (!commonRaw) return void 0;
24651
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join5(gitdir, commonRaw);
25373
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
24652
25374
  const headRoot = basename(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
24653
- return { configPath: join5(commonGitDir, "config"), headRoot };
25375
+ return { configPath: join6(commonGitDir, "config"), headRoot };
24654
25376
  }
24655
25377
  function safeRead(path) {
24656
25378
  try {
@@ -24767,16 +25489,57 @@ function resolveInventoryContext(input) {
24767
25489
  }
24768
25490
 
24769
25491
  // ../../packages/plugin-sdk/src/nudge.ts
24770
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24771
- import { join as join7 } from "path";
25492
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25493
+ import { join as join8 } from "path";
24772
25494
 
24773
25495
  // ../../packages/plugin-sdk/src/project-files.ts
24774
25496
  var import_ignore = __toESM(require_ignore(), 1);
24775
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
24776
- import { basename as basename3, join as join8, relative, sep as sep3 } from "path";
25497
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25498
+ import { basename as basename3, join as join9, relative, sep as sep3 } from "path";
25499
+
25500
+ // ../../packages/plugin-sdk/src/raw-egress.ts
25501
+ var RawEgressError = class extends Error {
25502
+ constructor(message) {
25503
+ super(message);
25504
+ this.name = "RawEgressError";
25505
+ }
25506
+ };
25507
+ var MIN_RAW_LEN = 4;
25508
+ function maskContextSlice(slice, sliceStart, hits) {
25509
+ const findings = [];
25510
+ for (const h of hits) {
25511
+ const start = Math.max(0, h.span.start - sliceStart);
25512
+ const end = Math.min(slice.length, h.span.end - sliceStart);
25513
+ if (end > start) {
25514
+ findings.push({
25515
+ ruleId: "raw-egress",
25516
+ category: "secret",
25517
+ severity: "critical",
25518
+ span: { start, end },
25519
+ rawMatch: "",
25520
+ confidence: 1
25521
+ });
25522
+ }
25523
+ }
25524
+ const masked = findings.length > 0 ? redact(slice, findings) : slice;
25525
+ for (const h of hits) {
25526
+ if (h.rawMatch.length >= MIN_RAW_LEN && masked.includes(h.rawMatch)) {
25527
+ throw new RawEgressError("raw match survived context masking");
25528
+ }
25529
+ }
25530
+ return masked;
25531
+ }
25532
+ function safeMaskedMatch(rawMatch) {
25533
+ const masked = maskMatch(rawMatch);
25534
+ if (masked === rawMatch || rawMatch.length >= MIN_RAW_LEN && masked.includes(rawMatch)) {
25535
+ return "***";
25536
+ }
25537
+ return masked;
25538
+ }
24777
25539
 
24778
25540
  // ../../packages/plugin-sdk/src/runtime.ts
24779
25541
  import { randomUUID as randomUUID10 } from "crypto";
25542
+ var ENFORCEMENT_CEILING_ENABLED = false;
24780
25543
  var ACTION_PRIORITY = ["block", "redact", "warn", "log", "allow"];
24781
25544
  function entryIsActive(entry, now) {
24782
25545
  if (entry.expiresAt !== null && Date.parse(entry.expiresAt) <= now) return false;
@@ -24849,17 +25612,22 @@ function createPluginRuntime(gateway, settings, opts) {
24849
25612
  const fallback = DEFAULT_ACTIONS[category];
24850
25613
  return fallback ?? "log";
24851
25614
  }
25615
+ function actionForFinding(finding, excepted) {
25616
+ if (excepted?.has(finding)) return "allow";
25617
+ const action = resolveAction(finding.ruleId, finding.category);
25618
+ if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
25619
+ return "warn";
25620
+ }
25621
+ return action;
25622
+ }
24852
25623
  function decide(findings, text, excepted) {
24853
25624
  if (findings.length === 0) return { action: "log", text, findings: [] };
24854
- const actionFor = (finding) => excepted?.has(finding) ? "allow" : resolveAction(finding.ruleId, finding.category);
25625
+ const actionFor = (finding) => actionForFinding(finding, excepted);
24855
25626
  let worst = "log";
24856
25627
  for (const finding of findings) {
24857
25628
  const action = actionFor(finding);
24858
25629
  if (ACTION_PRIORITY.indexOf(action) < ACTION_PRIORITY.indexOf(worst)) worst = action;
24859
25630
  }
24860
- if (policyMode === "warn" && (worst === "block" || worst === "redact")) {
24861
- return { action: "warn", text, findings };
24862
- }
24863
25631
  if (worst === "block") return { action: "block", text: null, findings };
24864
25632
  if (worst === "redact") {
24865
25633
  const redactFindings = findings.filter((f) => actionFor(f) === "redact");
@@ -24879,7 +25647,7 @@ function createPluginRuntime(gateway, settings, opts) {
24879
25647
  const excepted = /* @__PURE__ */ new Set();
24880
25648
  const exceptionIds = [];
24881
25649
  try {
24882
- if (policyMode === "warn") return { excepted, exceptionIds };
25650
+ if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn") return { excepted, exceptionIds };
24883
25651
  const enforced = findings.filter((f) => {
24884
25652
  const action = resolveAction(f.ruleId, f.category);
24885
25653
  return action === "block" || action === "redact";
@@ -24932,8 +25700,8 @@ function createPluginRuntime(gateway, settings, opts) {
24932
25700
  if (!key) return references;
24933
25701
  const seen = /* @__PURE__ */ new Set();
24934
25702
  for (const finding of decision.findings) {
24935
- const action = resolveAction(finding.ruleId, finding.category);
24936
- if (action !== "block" && action !== "redact" || excepted.has(finding)) continue;
25703
+ const action = actionForFinding(finding, excepted);
25704
+ if (action !== "block" && action !== "redact") continue;
24937
25705
  const fp = fingerprintOf(key, finding, fpCache);
24938
25706
  const pair = `${finding.ruleId}:${fp}`;
24939
25707
  if (seen.has(pair)) continue;
@@ -25022,7 +25790,7 @@ function createPluginRuntime(gateway, settings, opts) {
25022
25790
  severity: match.severity,
25023
25791
  span: match.span,
25024
25792
  maskedMatch,
25025
- actionTaken: excepted.has(match) ? "allow" : decision.action,
25793
+ actionTaken: actionForFinding(match, excepted),
25026
25794
  confidence: match.confidence,
25027
25795
  ...findingKey ? { findingKey } : {}
25028
25796
  };
@@ -25050,9 +25818,12 @@ function createPluginRuntime(gateway, settings, opts) {
25050
25818
  return { processText, capture, rulesetFingerprint, close };
25051
25819
  }
25052
25820
 
25821
+ // ../../packages/plugin-sdk/src/suppressions.ts
25822
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25823
+
25053
25824
  // ../../packages/plugin-sdk/src/throttle.ts
25054
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
25055
- import { join as join9 } from "path";
25825
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25826
+ import { join as join10 } from "path";
25056
25827
 
25057
25828
  // ../../packages/plugin-runtime/src/standalone-gateway.ts
25058
25829
  import { randomUUID as randomUUID11 } from "crypto";
@@ -25245,6 +26016,14 @@ var StandaloneDataGateway = class {
25245
26016
  sweepTerminalExceptions(retentionMs) {
25246
26017
  return this.db.exceptions.sweepTerminal(retentionMs);
25247
26018
  }
26019
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
26020
+ // from SessionStart, not part of the DataGateway port. Returns the number
26021
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
26022
+ // already-capped one).
26023
+ capWarnEraEnforcement(policyMode) {
26024
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
26025
+ return { capped };
26026
+ }
25248
26027
  // One project-file scan → the local project_file tree (one transaction inside
25249
26028
  // the LocalDatabase, fail-open there). Like the sweep above, this is
25250
26029
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -25344,9 +26123,9 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
25344
26123
  // src/history/transcripts.ts
25345
26124
  import { readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
25346
26125
  import { homedir as homedir3 } from "os";
25347
- import { join as join10 } from "path";
25348
- function transcriptsDir() {
25349
- return join10(homedir3(), ".claude", "projects");
26126
+ import { join as join11 } from "path";
26127
+ function transcriptsDir(home) {
26128
+ return join11(home ?? homedir3(), ".claude", "projects");
25350
26129
  }
25351
26130
  function isRecord(value) {
25352
26131
  return typeof value === "object" && value !== null;
@@ -25362,7 +26141,7 @@ function extractText(content) {
25362
26141
  }
25363
26142
  return parts.join("\n");
25364
26143
  }
25365
- function parseTranscript(jsonl, sinceMs = 0) {
26144
+ function parseTranscript(jsonl, sinceMs = 0, beforeMs = Infinity, filePath = "") {
25366
26145
  const out = [];
25367
26146
  for (const line of jsonl.split("\n")) {
25368
26147
  const trimmed = line.trim();
@@ -25380,11 +26159,12 @@ function parseTranscript(jsonl, sinceMs = 0) {
25380
26159
  const occurredMs = Date.parse(occurredAt);
25381
26160
  if (Number.isNaN(occurredMs)) continue;
25382
26161
  if (sinceMs > 0 && occurredMs < sinceMs) continue;
26162
+ if (occurredMs >= beforeMs) continue;
25383
26163
  const message = rec.message;
25384
26164
  if (!isRecord(message)) continue;
25385
26165
  const text = extractText(message.content);
25386
26166
  if (text.trim() === "") continue;
25387
- out.push({ kind: rec.type === "user" ? "prompt" : "response", text, occurredAt });
26167
+ out.push({ kind: rec.type === "user" ? "prompt" : "response", text, occurredAt, filePath });
25388
26168
  }
25389
26169
  return out;
25390
26170
  }
@@ -25585,7 +26365,7 @@ function parseTranscriptToolCalls(jsonl, sinceMs = 0) {
25585
26365
  return out;
25586
26366
  }
25587
26367
  var DAY_MS5 = 24 * 60 * 60 * 1e3;
25588
- function* iterateFileContents(dir) {
26368
+ function* iterateFileContents(dir, excludeSessionId) {
25589
26369
  let projects;
25590
26370
  try {
25591
26371
  projects = readdirSync3(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -25593,7 +26373,7 @@ function* iterateFileContents(dir) {
25593
26373
  return;
25594
26374
  }
25595
26375
  for (const project of projects) {
25596
- const projectDir = join10(dir, project);
26376
+ const projectDir = join11(dir, project);
25597
26377
  let files;
25598
26378
  try {
25599
26379
  files = readdirSync3(projectDir).filter((name) => name.endsWith(".jsonl"));
@@ -25601,13 +26381,16 @@ function* iterateFileContents(dir) {
25601
26381
  continue;
25602
26382
  }
25603
26383
  for (const file2 of files) {
26384
+ if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
26385
+ continue;
26386
+ const filePath = join11(projectDir, file2);
25604
26387
  let content;
25605
26388
  try {
25606
- content = readFileSync7(join10(projectDir, file2), "utf8");
26389
+ content = readFileSync7(filePath, "utf8");
25607
26390
  } catch {
25608
26391
  continue;
25609
26392
  }
25610
- yield content;
26393
+ yield { content, filePath };
25611
26394
  }
25612
26395
  }
25613
26396
  }
@@ -25617,12 +26400,16 @@ function windowStartMs(opts) {
25617
26400
  }
25618
26401
  function* iterateHistory(opts = {}) {
25619
26402
  const sinceMs = windowStartMs(opts);
25620
- for (const content of iterateFileContents(opts.dir ?? transcriptsDir()))
25621
- yield* parseTranscript(content, sinceMs);
26403
+ const beforeMs = opts.beforeMs ?? Infinity;
26404
+ for (const { content, filePath } of iterateFileContents(
26405
+ opts.dir ?? transcriptsDir(),
26406
+ opts.excludeSessionId
26407
+ ))
26408
+ yield* parseTranscript(content, sinceMs, beforeMs, filePath);
25622
26409
  }
25623
26410
  function* iterateUsageAndToolCalls(opts = {}) {
25624
26411
  const sinceMs = windowStartMs(opts);
25625
- for (const content of iterateFileContents(opts.dir ?? transcriptsDir())) {
26412
+ for (const { content } of iterateFileContents(opts.dir ?? transcriptsDir())) {
25626
26413
  yield {
25627
26414
  usage: parseTranscriptUsage(content, sinceMs),
25628
26415
  toolCalls: parseTranscriptToolCalls(content, sinceMs)
@@ -25631,7 +26418,36 @@ function* iterateUsageAndToolCalls(opts = {}) {
25631
26418
  }
25632
26419
 
25633
26420
  // src/history/scan.ts
25634
- async function scanHistory(config2, opts = {}) {
26421
+ var CONTEXT_RADIUS = 120;
26422
+ function redactOverlapping(rawContext, contextStart, others) {
26423
+ if (others.length === 0) return rawContext;
26424
+ try {
26425
+ return maskContextSlice(rawContext, contextStart, others);
26426
+ } catch {
26427
+ let safe = rawContext;
26428
+ for (const other of others) {
26429
+ if (other.rawMatch.length > 0) safe = safe.split(other.rawMatch).join("[REDACTED]");
26430
+ }
26431
+ return safe;
26432
+ }
26433
+ }
26434
+ function buildTriageHit(text, f, otherFindings = [], filePath = "") {
26435
+ const start = Math.max(0, f.span.start - CONTEXT_RADIUS);
26436
+ const end = Math.min(text.length, f.span.end + CONTEXT_RADIUS);
26437
+ const rawContext = text.slice(start, end);
26438
+ const overlapping = otherFindings.filter((o) => o.span.start < end && o.span.end > start);
26439
+ return {
26440
+ ruleId: f.ruleId,
26441
+ category: f.category,
26442
+ severity: f.severity,
26443
+ maskedMatch: safeMaskedMatch(f.rawMatch),
26444
+ rawMatch: f.rawMatch,
26445
+ context: redactOverlapping(rawContext, start, overlapping),
26446
+ confidence: f.confidence,
26447
+ ...filePath !== "" ? { filePath } : {}
26448
+ };
26449
+ }
26450
+ async function scanHistory(config2, opts = {}, onHit) {
25635
26451
  const windowDays = opts.windowDays ?? 30;
25636
26452
  if (config2.settings.historicalAccess !== "full") {
25637
26453
  return { consented: false, scanned: 0, skipped: 0, findings: 0, bySeverity: {}, windowDays };
@@ -25667,6 +26483,14 @@ async function scanHistory(config2, opts = {}) {
25667
26483
  for (const finding of result.findings) {
25668
26484
  findings++;
25669
26485
  bySeverity[finding.severity] = (bySeverity[finding.severity] ?? 0) + 1;
26486
+ if (onHit) {
26487
+ const otherFindings = result.findings.filter((other) => other !== finding);
26488
+ const hit = buildTriageHit(message.text, finding, otherFindings, message.filePath);
26489
+ try {
26490
+ onHit(hit);
26491
+ } catch {
26492
+ }
26493
+ }
25670
26494
  }
25671
26495
  }
25672
26496
  } finally {
@@ -25684,9 +26508,9 @@ import {
25684
26508
  openSync,
25685
26509
  readFileSync as readFileSync8,
25686
26510
  readSync,
25687
- writeFileSync as writeFileSync5
26511
+ writeFileSync as writeFileSync6
25688
26512
  } from "fs";
25689
- import { join as join11 } from "path";
26513
+ import { join as join12 } from "path";
25690
26514
 
25691
26515
  // src/history/usage.ts
25692
26516
  var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";
@@ -25929,25 +26753,116 @@ function fenced(body) {
25929
26753
  }
25930
26754
 
25931
26755
  // src/backfill.ts
25932
- try {
25933
- const cfg = loadConfig();
25934
- if (cfg.settings.historicalAccess !== "full") {
25935
- process.stdout.write("Historical scan skipped \u2014 full review was not granted.\n");
25936
- process.exit(0);
25937
- }
25938
- const summary = await scanHistory(cfg);
26756
+ var TRIAGE_STATUSES = ["complete", "complete:no-history", "skipped:no-consent"];
26757
+ function triageSentinel(count, status) {
26758
+ return JSON.stringify({ done: true, count, status }) + "\n";
26759
+ }
26760
+ async function runBackfill(deps) {
26761
+ const { triage, io } = deps;
25939
26762
  try {
25940
- await reconcileHistory(cfg);
25941
- } catch {
25942
- }
25943
- const heading = "\u2713 Historical scan complete";
25944
- const scope = `Scanned ${String(summary.scanned)} messages from the last ${String(summary.windowDays)} days of Claude Code history.`;
25945
- const result = summary.findings > 0 ? `Found ${String(summary.findings)} pre-install finding${summary.findings === 1 ? "" : "s"} \u2014 review them with /findings.` : "No new pre-install secrets found in your history.";
25946
- process.stdout.write(`${fenced([heading, "", indent(scope), "", indent(result)].join("\n"))}
26763
+ const cfg = deps.loadConfig();
26764
+ if (cfg.settings.historicalAccess !== "full") {
26765
+ if (triage) {
26766
+ io.stdout(triageSentinel(0, "skipped:no-consent"));
26767
+ } else {
26768
+ io.stdout("Historical scan skipped \u2014 full review was not granted.\n");
26769
+ }
26770
+ return;
26771
+ }
26772
+ let fpKey = null;
26773
+ if (triage) {
26774
+ try {
26775
+ fpKey = loadOrCreateFingerprintKey(cfg.dataDir);
26776
+ } catch {
26777
+ fpKey = null;
26778
+ }
26779
+ }
26780
+ let count = 0;
26781
+ let onHitError;
26782
+ const summary = await deps.scanHistory(
26783
+ cfg,
26784
+ deps.guard ?? {},
26785
+ triage ? (hit) => {
26786
+ if (onHitError !== void 0) return;
26787
+ try {
26788
+ const enriched = {
26789
+ ...hit,
26790
+ id: String(count),
26791
+ valueFingerprint: fpKey ? fingerprintValue(fpKey, hit.rawMatch) : void 0,
26792
+ keyVersion: fpKey?.version
26793
+ };
26794
+ const validated = TriageHit.safeParse(enriched);
26795
+ if (!validated.success) {
26796
+ throw new Error("enriched triage hit failed TriageHit validation");
26797
+ }
26798
+ io.stdout(JSON.stringify(validated.data) + "\n");
26799
+ count += 1;
26800
+ } catch (err) {
26801
+ onHitError = err;
26802
+ }
26803
+ } : void 0
26804
+ );
26805
+ if (onHitError !== void 0) {
26806
+ throw onHitError instanceof Error ? onHitError : new Error(typeof onHitError === "string" ? onHitError : "triage stream write failed");
26807
+ }
26808
+ try {
26809
+ await deps.reconcileHistory(cfg);
26810
+ } catch {
26811
+ }
26812
+ if (triage) {
26813
+ const status = summary.scanned === 0 && summary.skipped === 0 ? "complete:no-history" : "complete";
26814
+ io.stdout(triageSentinel(count, status));
26815
+ } else {
26816
+ const heading = "\u2713 Historical scan complete";
26817
+ const scope = `Scanned ${String(summary.scanned)} messages from the last ${String(summary.windowDays)} days of Claude Code history.`;
26818
+ const result = summary.findings > 0 ? `Found ${String(summary.findings)} pre-install finding${summary.findings === 1 ? "" : "s"} \u2014 review them with /findings.` : "No new pre-install secrets found in your history.";
26819
+ io.stdout(`${fenced([heading, "", indent(scope), "", indent(result)].join("\n"))}
25947
26820
  `);
25948
- } catch {
25949
- process.stdout.write(
25950
- "AKA could not scan your history right now. It will still protect everything from here on.\n"
25951
- );
26821
+ }
26822
+ } catch (err) {
26823
+ if (triage) {
26824
+ io.stderr(`aka backfill --triage: history scan failed: ${String(err)}
26825
+ `);
26826
+ io.fail();
26827
+ } else {
26828
+ io.stdout(
26829
+ "AKA could not scan your history right now. It will still protect everything from here on.\n"
26830
+ );
26831
+ }
26832
+ }
26833
+ }
26834
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
26835
+ const triage = process.argv.includes("--triage");
26836
+ const startedAt = Date.now();
26837
+ const sessionId = process.env.CLAUDE_CODE_BRIDGE_SESSION_ID;
26838
+ await runBackfill({
26839
+ triage,
26840
+ io: {
26841
+ stdout: (chunk) => process.stdout.write(chunk),
26842
+ stderr: (chunk) => process.stderr.write(chunk),
26843
+ fail: () => {
26844
+ process.exitCode = 1;
26845
+ }
26846
+ },
26847
+ loadConfig: () => loadConfig(),
26848
+ scanHistory,
26849
+ reconcileHistory: (cfg) => reconcileHistory(cfg),
26850
+ guard: {
26851
+ beforeMs: startedAt,
26852
+ ...sessionId ? { excludeSessionId: sessionId } : {}
26853
+ }
26854
+ });
26855
+ if (process.stdout.writableLength > 0) {
26856
+ await new Promise((resolve) => {
26857
+ process.stdout.write("", () => {
26858
+ resolve();
26859
+ });
26860
+ });
26861
+ }
26862
+ process.exit(process.exitCode ?? 0);
25952
26863
  }
25953
- process.exit(0);
26864
+ export {
26865
+ TRIAGE_STATUSES,
26866
+ runBackfill,
26867
+ triageSentinel
26868
+ };