@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.
@@ -541,6 +541,10 @@ var SQLITE_MIGRATIONS = [
541
541
  {
542
542
  tag: "0009_findings_path_expression_index",
543
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"
544
548
  }
545
549
  ];
546
550
 
@@ -643,6 +647,12 @@ var defaultCostModel = {
643
647
  }
644
648
  };
645
649
 
650
+ // ../../packages/schema/src/token/format.ts
651
+ var COMPACT = new Intl.NumberFormat("en-US", {
652
+ notation: "compact",
653
+ maximumFractionDigits: 1
654
+ });
655
+
646
656
  // ../../packages/schema/src/token/token-report.ts
647
657
  var num = (value) => value ?? 0;
648
658
  function costUsageOf(a) {
@@ -15314,6 +15324,12 @@ var FindingInstance = external_exports.object({
15314
15324
  provider: FindingProvider,
15315
15325
  repo: external_exports.string(),
15316
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(),
15317
15333
  // Effective action: override.action ?? actionTaken, translated to FindingAction.
15318
15334
  action: FindingAction,
15319
15335
  detectedAt: external_exports.iso.datetime(),
@@ -15370,6 +15386,9 @@ var ListGroupedFindingsQuery = external_exports.object({
15370
15386
  provider: external_exports.array(FindingProvider).optional(),
15371
15387
  action: external_exports.array(FindingAction).optional(),
15372
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(),
15373
15392
  groupBy: external_exports.literal("type").optional(),
15374
15393
  limit: external_exports.coerce.number().int().min(1).max(100).optional(),
15375
15394
  cursor: external_exports.string().optional()
@@ -15381,7 +15400,13 @@ var ListGroupedFindingsResponse = external_exports.object({
15381
15400
  }),
15382
15401
  facets: FindingFacets,
15383
15402
  items: external_exports.array(FindingGroup),
15384
- 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()
15385
15410
  }).meta({ id: "ListGroupedFindingsResponse" });
15386
15411
  var ApplyFindingActionRequest = external_exports.object({
15387
15412
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
@@ -15433,6 +15458,11 @@ var AuditEventType = external_exports.enum([
15433
15458
  "prompt",
15434
15459
  "response",
15435
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",
15436
15466
  // One row per config-inventory scan, hung off the session root. It is the
15437
15467
  // fact the posture inspection findings reference (findings require an
15438
15468
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15787,6 +15817,10 @@ var ListActivitySessionsQuery = external_exports.object({
15787
15817
  from: external_exports.union([external_exports.iso.date(), external_exports.iso.datetime()]).optional(),
15788
15818
  /** Upper bound on startedAt; omitted defaults to now. */
15789
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(),
15790
15824
  /** Page size, 1–100; out-of-range values are a 400. `z.coerce` — query params arrive as strings. */
15791
15825
  limit: external_exports.coerce.number().int().min(1).max(100).default(50),
15792
15826
  /** Opaque pagination cursor (most-recent first). */
@@ -15795,7 +15829,11 @@ var ListActivitySessionsQuery = external_exports.object({
15795
15829
  var ListActivitySessionsResponse = external_exports.object({
15796
15830
  items: external_exports.array(ActivitySessionSummary),
15797
15831
  /** `null` once the last page is reached. */
15798
- 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()
15799
15837
  }).meta({ id: "ListActivitySessionsResponse" });
15800
15838
  var ListSessionEventsQuery = external_exports.object({
15801
15839
  /** Default 100, range 1–500. */
@@ -15824,12 +15862,18 @@ var ActivityOverviewResponse = external_exports.object({
15824
15862
  }).meta({ id: "ActivityOverviewResponse" });
15825
15863
 
15826
15864
  // ../../packages/schema/src/zod/event.ts
15827
- 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" });
15828
15866
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15829
15867
  var EventMetadata = external_exports.object({
15830
15868
  sessionId: external_exports.string().optional(),
15831
15869
  repo: external_exports.string().optional(),
15832
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(),
15833
15877
  // Set (true) by the worktree scanner when the file is excluded by the
15834
15878
  // repo's .gitignore. Gitignored files ARE still scanned — local scratch and
15835
15879
  // generated code can leak real secrets — but the provenance is recorded so
@@ -16137,7 +16181,7 @@ var DetectionException = external_exports.object({
16137
16181
  justification: external_exports.string().min(1),
16138
16182
  conditions: ExceptionConditions.nullable(),
16139
16183
  createdBy: external_exports.string(),
16140
- 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"]),
16141
16185
  createdAt: external_exports.iso.datetime(),
16142
16186
  updatedAt: external_exports.iso.datetime(),
16143
16187
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16161,7 +16205,10 @@ var ExceptionBundleEntry = DetectionException.pick({
16161
16205
  var MatcherType = external_exports.enum(["keyword", "regex", "validator"]).meta({ id: "MatcherType" });
16162
16206
  var KeywordMatcher = external_exports.object({
16163
16207
  type: external_exports.literal("keyword"),
16164
- 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),
16165
16212
  caseSensitive: external_exports.boolean().default(false)
16166
16213
  });
16167
16214
  function isValidRegex(pattern, flags) {
@@ -16306,20 +16353,26 @@ var PolicyBundle = external_exports.object({
16306
16353
  customKeywords: external_exports.array(external_exports.string()),
16307
16354
  fetchedAt: external_exports.iso.datetime()
16308
16355
  }).meta({ id: "PolicyBundle" });
16309
- var DEFAULT_ACTIONS = {
16310
- secret: "block",
16311
- pii: "redact",
16312
- financial: "redact",
16313
- phi: "redact",
16314
- code_context: "warn",
16315
- code_flaw: "warn",
16316
- custom: "warn",
16317
- // Config-posture findings only observe today (they land in
16318
- // inspection_findings, outside the live-capture enforcement path).
16319
- config: "warn"
16320
- };
16321
16356
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16322
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
+ }
16323
16376
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16324
16377
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16325
16378
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16346,6 +16399,12 @@ var BUILTIN_POLICY_SPECS = {
16346
16399
  description: "Refuse the request entirely whenever any rule in this detection matches."
16347
16400
  }
16348
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
+ );
16349
16408
  var BUILTIN_POLICIES = Object.fromEntries(
16350
16409
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16351
16410
  );
@@ -16398,6 +16457,10 @@ var ListEventsResponse = external_exports.object({
16398
16457
  items: external_exports.array(Event),
16399
16458
  nextCursor: external_exports.string().nullable()
16400
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" });
16401
16464
  var ListFindingsQuery = external_exports.object({
16402
16465
  cursor: external_exports.string().optional(),
16403
16466
  limit: external_exports.coerce.number().int().min(1).max(LIST_QUERY_MAX_LIMIT).default(50),
@@ -16866,10 +16929,8 @@ function toApiProvider(sourceTool) {
16866
16929
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16867
16930
  }
16868
16931
  var STATUS_PRECEDENCE = ["open", "handled", "dismissed", "resolved"];
16869
- function deriveGroupStatus(instances) {
16870
- const statuses = new Set(
16871
- instances.map((i) => i.status).filter((s) => s !== void 0)
16872
- );
16932
+ function foldGroupStatus(instanceStatuses) {
16933
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16873
16934
  if (statuses.size === 0) return void 0;
16874
16935
  for (const candidate of STATUS_PRECEDENCE) {
16875
16936
  if (statuses.has(candidate)) return candidate;
@@ -16887,6 +16948,7 @@ function deriveFindingStatus(row) {
16887
16948
  function buildFindingGroups(rows, opts = {}) {
16888
16949
  const overrides = opts.overrides;
16889
16950
  const packNames = opts.packNames;
16951
+ const aggregates = opts.aggregates;
16890
16952
  const byRuleId = /* @__PURE__ */ new Map();
16891
16953
  for (const row of rows) {
16892
16954
  const existing = byRuleId.get(row.ruleId);
@@ -16902,23 +16964,27 @@ function buildFindingGroups(rows, opts = {}) {
16902
16964
  provider: toApiProvider(r.sourceTool),
16903
16965
  repo: r.repo,
16904
16966
  file: r.file,
16967
+ ...r.toolName === void 0 ? {} : { toolName: r.toolName },
16905
16968
  action: toApiAction(effectiveDbAction),
16906
16969
  detectedAt: r.occurredAt,
16907
16970
  confidence: r.confidence,
16908
16971
  status: r.status
16909
16972
  };
16910
16973
  });
16911
- const latestDetectedAt = ruleRows.reduce(
16974
+ const agg = aggregates?.get(ruleId);
16975
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16912
16976
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16913
16977
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16914
16978
  );
16915
16979
  const seenProviders = /* @__PURE__ */ new Set();
16916
- const providers = instances.map((i) => i.provider).filter((p) => {
16980
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16917
16981
  if (seenProviders.has(p)) return false;
16918
16982
  seenProviders.add(p);
16919
16983
  return true;
16920
16984
  });
16921
- const actionSet = new Set(instances.map((i) => i.action));
16985
+ const actionSet = new Set(
16986
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16987
+ );
16922
16988
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16923
16989
  const severity = ruleRows[0]?.severity ?? "low";
16924
16990
  const detection = {
@@ -16932,8 +16998,10 @@ function buildFindingGroups(rows, opts = {}) {
16932
16998
  contextPrefix: ""
16933
16999
  // empty (pending privacy review)
16934
17000
  };
16935
- const status = deriveGroupStatus(instances);
16936
- groups.push({
17001
+ const status = foldGroupStatus(
17002
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
17003
+ );
17004
+ const group = {
16937
17005
  id: ruleId,
16938
17006
  category: apiCategory,
16939
17007
  subtype: ruleId,
@@ -16942,21 +17010,26 @@ function buildFindingGroups(rows, opts = {}) {
16942
17010
  match,
16943
17011
  detection,
16944
17012
  policy,
16945
- instanceCount: instances.length,
17013
+ instanceCount: agg?.instanceCount ?? instances.length,
16946
17014
  providers,
16947
17015
  aggregateAction,
16948
17016
  latestDetectedAt,
16949
17017
  instances,
16950
17018
  status
16951
- });
17019
+ };
17020
+ if (agg) {
17021
+ actionsCache.set(group, [...actionSet]);
17022
+ if (agg.searchText !== void 0) {
17023
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
17024
+ }
17025
+ }
17026
+ groups.push(group);
16952
17027
  }
16953
17028
  return groups;
16954
17029
  }
16955
17030
  var haystackCache = /* @__PURE__ */ new WeakMap();
16956
- function groupHaystack(g) {
16957
- const cached2 = haystackCache.get(g);
16958
- if (cached2 !== void 0) return cached2;
16959
- const haystack = [
17031
+ function buildHaystack(g, extra) {
17032
+ return [
16960
17033
  g.subtype,
16961
17034
  g.category,
16962
17035
  g.match.maskedValue,
@@ -16964,11 +17037,26 @@ function groupHaystack(g) {
16964
17037
  g.id,
16965
17038
  ...g.instances.map((i) => i.repo),
16966
17039
  ...g.instances.map((i) => i.file),
16967
- ...g.instances.map((i) => i.id)
17040
+ ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
17041
+ ...g.instances.map((i) => i.id),
17042
+ ...extra === void 0 ? [] : [extra]
16968
17043
  ].join(" ").toLowerCase();
17044
+ }
17045
+ function groupHaystack(g) {
17046
+ const cached2 = haystackCache.get(g);
17047
+ if (cached2 !== void 0) return cached2;
17048
+ const haystack = buildHaystack(g);
16969
17049
  haystackCache.set(g, haystack);
16970
17050
  return haystack;
16971
17051
  }
17052
+ var actionsCache = /* @__PURE__ */ new WeakMap();
17053
+ function groupActions(g) {
17054
+ const cached2 = actionsCache.get(g);
17055
+ if (cached2 !== void 0) return cached2;
17056
+ const actions = [...new Set(g.instances.map((i) => i.action))];
17057
+ actionsCache.set(g, actions);
17058
+ return actions;
17059
+ }
16972
17060
  function applyFindingFilters(groups, opts) {
16973
17061
  let filtered = groups;
16974
17062
  if (opts.severity && opts.severity.length > 0) {
@@ -16981,7 +17069,7 @@ function applyFindingFilters(groups, opts) {
16981
17069
  }
16982
17070
  if (opts.actions && opts.actions.length > 0) {
16983
17071
  const actionSet = new Set(opts.actions);
16984
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
17072
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16985
17073
  }
16986
17074
  if (opts.subtype && opts.subtype.length > 0) {
16987
17075
  const subtypeSet = new Set(opts.subtype);
@@ -17033,8 +17121,7 @@ function computeFindingFacets(allGroups, opts) {
17033
17121
  });
17034
17122
  const actionMap = /* @__PURE__ */ new Map();
17035
17123
  for (const g of forAction) {
17036
- const actionSet = new Set(g.instances.map((i) => i.action));
17037
- for (const a of actionSet) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17124
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17038
17125
  }
17039
17126
  const forSubtype = applyFindingFilters(allGroups, {
17040
17127
  providers: opts.providers,
@@ -17217,6 +17304,71 @@ var ProjectFilesScan = external_exports.object({
17217
17304
  scannedAt: external_exports.string()
17218
17305
  });
17219
17306
 
17307
+ // ../../packages/schema/src/zod/ranges.ts
17308
+ var TIME_RANGES = ["7d", "30d", "3m", "6m"];
17309
+ var TimeRange = external_exports.enum(TIME_RANGES).meta({ id: "TimeRange" });
17310
+ var DEFAULT_TIME_RANGE = "7d";
17311
+ var RANGE_DAYS = {
17312
+ "7d": 7,
17313
+ "30d": 30,
17314
+ "3m": 90,
17315
+ "6m": 180
17316
+ };
17317
+ var TIME_RANGE_OR_DEFAULT = TimeRange.catch(DEFAULT_TIME_RANGE);
17318
+
17319
+ // ../../packages/schema/src/zod/remediation.ts
17320
+ var SecretFindingState = external_exports.enum(["still-valid", "unknown", "invalid"]);
17321
+ var MaskedFindingLocation = external_exports.object({
17322
+ filePath: external_exports.string(),
17323
+ span: Span.optional()
17324
+ }).strict();
17325
+ var MaskedSecretFinding = external_exports.object({
17326
+ provider: external_exports.string(),
17327
+ maskedToken: external_exports.string(),
17328
+ where: MaskedFindingLocation,
17329
+ state: SecretFindingState,
17330
+ observedAt: external_exports.iso.datetime().optional()
17331
+ }).strict();
17332
+ var RotationChecklistEntry = external_exports.object({
17333
+ provider: external_exports.string(),
17334
+ maskedToken: external_exports.string(),
17335
+ consolePath: external_exports.string(),
17336
+ occurrenceSpread: external_exports.number().int().positive()
17337
+ }).strict();
17338
+ var RemediationOption = external_exports.enum([
17339
+ "redact-rotation-checklist",
17340
+ "redact-only",
17341
+ "set-secret-redact",
17342
+ "leave"
17343
+ ]);
17344
+ var RemediationEntrySource = external_exports.enum(["first-run", "pre-push", "secret-scan"]);
17345
+ var RemediationEntryContext = external_exports.object({
17346
+ entrySource: RemediationEntrySource
17347
+ }).strict();
17348
+ var RemediationOptionChoice = external_exports.object({
17349
+ id: RemediationOption,
17350
+ label: external_exports.string()
17351
+ });
17352
+ var BatchedRemediationDecision = external_exports.object({
17353
+ kind: external_exports.literal("decision"),
17354
+ entrySource: RemediationEntrySource,
17355
+ secretCount: external_exports.number().int().positive(),
17356
+ prompt: external_exports.string(),
17357
+ options: external_exports.tuple([
17358
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-rotation-checklist") }),
17359
+ RemediationOptionChoice.extend({ id: external_exports.literal("redact-only") }),
17360
+ RemediationOptionChoice.extend({ id: external_exports.literal("set-secret-redact") }),
17361
+ RemediationOptionChoice.extend({ id: external_exports.literal("leave") })
17362
+ ])
17363
+ });
17364
+ var NoRemediationDecision = external_exports.object({
17365
+ kind: external_exports.literal("no-decision")
17366
+ });
17367
+ var BatchedRemediation = external_exports.discriminatedUnion("kind", [
17368
+ BatchedRemediationDecision,
17369
+ NoRemediationDecision
17370
+ ]);
17371
+
17220
17372
  // ../../packages/schema/src/zod/rule-test.ts
17221
17373
  var TestRulesRequest = external_exports.object({
17222
17374
  rules: external_exports.array(Rule).min(1).max(100),
@@ -17275,10 +17427,8 @@ var SeveritySummaryResponse = external_exports.object({
17275
17427
  // All four severity levels are always present (count may be 0).
17276
17428
  bySeverity: external_exports.array(SeveritySummaryItem)
17277
17429
  }).meta({ id: "SeveritySummaryResponse" });
17278
- var SECURITY_RANGES = ["7d", "30d", "3m", "6m"];
17279
- var SecurityRange = external_exports.enum(SECURITY_RANGES).meta({ id: "SecurityRange" });
17280
17430
  var SecurityRangeQuery = external_exports.object({
17281
- range: external_exports.enum(SECURITY_RANGES).default("30d")
17431
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE)
17282
17432
  });
17283
17433
  var EnforcementActionKind = external_exports.enum(["blocked", "redacted", "warned"]).meta({ id: "EnforcementActionKind" });
17284
17434
  var EnforcementAction = external_exports.object({
@@ -17288,7 +17438,7 @@ var EnforcementAction = external_exports.object({
17288
17438
  delta: external_exports.number().int()
17289
17439
  }).meta({ id: "EnforcementAction" });
17290
17440
  var EnforcementActionsResponse = external_exports.object({
17291
- range: SecurityRange,
17441
+ range: TimeRange,
17292
17442
  // Sum of actions[].count in the window.
17293
17443
  total: external_exports.number().int().nonnegative(),
17294
17444
  // One entry per kind, always all three present (count may be 0).
@@ -17303,7 +17453,7 @@ var FindingsTimeseriesPoint = external_exports.object({
17303
17453
  medium: external_exports.number().int().nonnegative()
17304
17454
  }).meta({ id: "FindingsTimeseriesPoint" });
17305
17455
  var FindingsTimeseriesResponse = external_exports.object({
17306
- range: SecurityRange,
17456
+ range: TimeRange,
17307
17457
  granularity: TimeseriesGranularity,
17308
17458
  points: external_exports.array(FindingsTimeseriesPoint)
17309
17459
  }).meta({ id: "FindingsTimeseriesResponse" });
@@ -17318,7 +17468,7 @@ var MttrTrendPoint = external_exports.object({
17318
17468
  })
17319
17469
  }).meta({ id: "MttrTrendPoint" });
17320
17470
  var MttrTrendResponse = external_exports.object({
17321
- range: SecurityRange,
17471
+ range: TimeRange,
17322
17472
  granularity: TimeseriesGranularity,
17323
17473
  points: external_exports.array(MttrTrendPoint)
17324
17474
  }).meta({ id: "MttrTrendResponse" });
@@ -17345,11 +17495,11 @@ var TopSource = external_exports.object({
17345
17495
  findingsCount: external_exports.number().int().nonnegative()
17346
17496
  }).meta({ id: "TopSource" });
17347
17497
  var TopSourcesResponse = external_exports.object({
17348
- range: SecurityRange,
17498
+ range: TimeRange,
17349
17499
  items: external_exports.array(TopSource)
17350
17500
  }).meta({ id: "TopSourcesResponse" });
17351
17501
  var TopSourcesQuery = external_exports.object({
17352
- range: external_exports.enum(SECURITY_RANGES).default("30d"),
17502
+ range: external_exports.enum(TIME_RANGES).default(DEFAULT_TIME_RANGE),
17353
17503
  limit: external_exports.coerce.number().int().min(1).max(50).default(5),
17354
17504
  // Omit for both kinds.
17355
17505
  kind: external_exports.enum(SOURCE_KINDS).optional()
@@ -17362,7 +17512,7 @@ var ScanCoverageProvider = external_exports.object({
17362
17512
  supported: external_exports.boolean()
17363
17513
  }).meta({ id: "ScanCoverageProvider" });
17364
17514
  var ScanCoverageResponse = external_exports.object({
17365
- range: SecurityRange,
17515
+ range: TimeRange,
17366
17516
  providers: external_exports.array(ScanCoverageProvider)
17367
17517
  }).meta({ id: "ScanCoverageResponse" });
17368
17518
  var SubjectType = external_exports.enum(["repo", "user", "team", "policy", "share", "rule"]).meta({
@@ -17411,6 +17561,114 @@ var ApplyRecommendedActionResponse = external_exports.object({
17411
17561
  var DismissRecommendedActionResponse = external_exports.object({ id: external_exports.string(), status: external_exports.literal("dismissed") }).meta({ id: "DismissRecommendedActionResponse" });
17412
17562
  var RecommendedActionIdParam = external_exports.object({ id: external_exports.string() });
17413
17563
 
17564
+ // ../../packages/schema/src/zod/triage.ts
17565
+ var TriageHit = external_exports.object({
17566
+ ruleId: external_exports.string(),
17567
+ category: DetectionCategory,
17568
+ severity: Severity,
17569
+ maskedMatch: external_exports.string(),
17570
+ rawMatch: external_exports.string(),
17571
+ context: external_exports.string(),
17572
+ filePath: external_exports.string().optional(),
17573
+ confidence: external_exports.number().min(0).max(1),
17574
+ id: external_exports.string().optional(),
17575
+ valueFingerprint: external_exports.string().optional(),
17576
+ keyVersion: external_exports.number().int().nonnegative().optional()
17577
+ });
17578
+ var TriagePolicy = BuiltinPolicyId;
17579
+ var TriageCategoryRec = external_exports.object({
17580
+ category: DetectionCategory,
17581
+ action: TriagePolicy,
17582
+ reasoning: external_exports.string(),
17583
+ genuineCount: external_exports.number().int().nonnegative(),
17584
+ fpCount: external_exports.number().int().nonnegative(),
17585
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17586
+ // this array's length — enforced by the consumer, not this schema.
17587
+ fpIds: external_exports.array(external_exports.string())
17588
+ });
17589
+ var TriageRecommendation = external_exports.object({
17590
+ perCategory: external_exports.array(TriageCategoryRec),
17591
+ notes: external_exports.string()
17592
+ });
17593
+
17594
+ // ../../packages/schema/src/zod/setup-frame.ts
17595
+ var CalibrationCounts = external_exports.object({
17596
+ total: external_exports.number().int().nonnegative(),
17597
+ important: external_exports.number().int().nonnegative(),
17598
+ routine: external_exports.number().int().nonnegative()
17599
+ }).refine((c) => c.total === c.important + c.routine, {
17600
+ message: "total must equal important + routine",
17601
+ path: ["total"]
17602
+ });
17603
+ var FalsePositivePatternValue = external_exports.object({
17604
+ ruleId: external_exports.string(),
17605
+ category: DetectionCategory,
17606
+ valueFingerprint: external_exports.string(),
17607
+ keyVersion: external_exports.number().int().nonnegative()
17608
+ });
17609
+ var FalsePositivePatternGroup = external_exports.object({
17610
+ pattern: external_exports.string(),
17611
+ count: external_exports.number().int().nonnegative(),
17612
+ values: external_exports.array(FalsePositivePatternValue).min(1)
17613
+ });
17614
+ var CalibrationFindingKind = external_exports.object({
17615
+ category: DetectionCategory,
17616
+ count: external_exports.number().int().nonnegative(),
17617
+ egress: external_exports.boolean()
17618
+ });
17619
+ var CalibrationFrame = external_exports.object({
17620
+ counts: CalibrationCounts,
17621
+ routineCategories: external_exports.array(DetectionCategory),
17622
+ surfacedCategories: external_exports.array(DetectionCategory),
17623
+ findingKinds: external_exports.array(CalibrationFindingKind),
17624
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId),
17625
+ maskedFindings: external_exports.array(MaskedSecretFinding).optional(),
17626
+ falsePositivePatterns: external_exports.array(FalsePositivePatternGroup).optional()
17627
+ });
17628
+ var CalibrationPreviewCategory = TriageCategoryRec.pick({
17629
+ category: true,
17630
+ genuineCount: true,
17631
+ fpCount: true
17632
+ }).extend({
17633
+ egress: external_exports.boolean()
17634
+ });
17635
+ var CalibrationPreview = external_exports.object({
17636
+ categories: external_exports.array(CalibrationPreviewCategory),
17637
+ posture: external_exports.record(DetectionCategory, BuiltinPolicyId)
17638
+ });
17639
+ var CalibrationResult = external_exports.object({
17640
+ frame: CalibrationFrame,
17641
+ copy: external_exports.string()
17642
+ });
17643
+ var FirstRunCalibration = external_exports.enum(["scan", "floor"]);
17644
+ var SetupHandoffOption = external_exports.object({
17645
+ id: external_exports.enum(["enter-remediation", "open-dashboard", "not-now"]),
17646
+ label: external_exports.string()
17647
+ });
17648
+ var DashboardHandoffOptions = external_exports.tuple([
17649
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17650
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17651
+ ]);
17652
+ var ComposedRemediationOptions = external_exports.tuple([
17653
+ SetupHandoffOption.extend({ id: external_exports.literal("enter-remediation") }),
17654
+ SetupHandoffOption.extend({ id: external_exports.literal("open-dashboard") }),
17655
+ SetupHandoffOption.extend({ id: external_exports.literal("not-now") })
17656
+ ]);
17657
+ var SetupHandoffOffer = external_exports.object({
17658
+ worthALook: external_exports.number().int().nonnegative(),
17659
+ liveKeys: external_exports.number().int().nonnegative().optional(),
17660
+ options: external_exports.union([DashboardHandoffOptions, ComposedRemediationOptions])
17661
+ }).refine(
17662
+ (o) => o.options.some((opt) => opt.id === "enter-remediation") === (o.liveKeys ?? 0) > 0,
17663
+ {
17664
+ message: "the chain-entry option is present exactly when liveKeys > 0",
17665
+ path: ["options"]
17666
+ }
17667
+ ).refine((o) => (o.liveKeys ?? 0) <= o.worthALook, {
17668
+ message: "liveKeys is a subset of worthALook and cannot exceed it",
17669
+ path: ["liveKeys"]
17670
+ });
17671
+
17414
17672
  // ../../packages/schema/src/zod/shares.ts
17415
17673
  var DestinationKind = external_exports.enum(["provider", "internal", "ip"]).meta({ id: "DestinationKind" });
17416
17674
  var Transport = external_exports.enum(["https", "http", "sftp", "grpc", "smtp"]).meta({ id: "Transport" });
@@ -17596,6 +17854,102 @@ function reviewSeverityRank(reasons) {
17596
17854
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17597
17855
  }
17598
17856
 
17857
+ // ../../packages/persistence/src/internal/sql-text.ts
17858
+ function escapeLikePattern(s) {
17859
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17860
+ }
17861
+ function placeholders(n) {
17862
+ return Array.from({ length: n }, () => "?").join(", ");
17863
+ }
17864
+ function containsPattern(q) {
17865
+ return `%${escapeLikePattern(q)}%`;
17866
+ }
17867
+ function likeAny(exprs) {
17868
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17869
+ }
17870
+
17871
+ // ../../packages/persistence/src/internal/transactions.ts
17872
+ var savepointSeq = 0;
17873
+ function withTransaction(db, fn, mode = "DEFERRED") {
17874
+ if (db.isTransaction) {
17875
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17876
+ db.exec(`SAVEPOINT ${savepoint}`);
17877
+ try {
17878
+ fn();
17879
+ db.exec(`RELEASE ${savepoint}`);
17880
+ } catch (error51) {
17881
+ try {
17882
+ db.exec(`ROLLBACK TO ${savepoint}`);
17883
+ db.exec(`RELEASE ${savepoint}`);
17884
+ } catch {
17885
+ }
17886
+ throw error51;
17887
+ }
17888
+ return;
17889
+ }
17890
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17891
+ try {
17892
+ fn();
17893
+ db.exec("COMMIT");
17894
+ } catch (error51) {
17895
+ try {
17896
+ db.exec("ROLLBACK");
17897
+ } catch {
17898
+ }
17899
+ throw error51;
17900
+ }
17901
+ }
17902
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17903
+ const nested = db.isTransaction;
17904
+ try {
17905
+ withTransaction(db, fn, mode);
17906
+ return true;
17907
+ } catch (error51) {
17908
+ if (!db.isTransaction && nested) throw error51;
17909
+ return false;
17910
+ }
17911
+ }
17912
+
17913
+ // ../../packages/persistence/src/internal/warn.ts
17914
+ function akaWarn(message2) {
17915
+ process.stderr.write(`[aka] ${message2}
17916
+ `);
17917
+ }
17918
+
17919
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17920
+ function evidenceObjects(sql) {
17921
+ const objects = [];
17922
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17923
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17924
+ objects.push({ kind: "table", name: m[1] });
17925
+ }
17926
+ }
17927
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17928
+ if (m[1] !== void 0 && m[2] !== void 0) {
17929
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17930
+ }
17931
+ }
17932
+ return objects;
17933
+ }
17934
+ function schemaObjectExists(db, kind, name) {
17935
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17936
+ return row !== void 0;
17937
+ }
17938
+ function indexExists(db, name) {
17939
+ return schemaObjectExists(db, "index", name);
17940
+ }
17941
+ function columnNames(db, table, opts) {
17942
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17943
+ const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
17944
+ return columns.map((c) => c.name);
17945
+ }
17946
+ function evidenceExists(db, object2) {
17947
+ if (object2.kind === "column") {
17948
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17949
+ }
17950
+ return schemaObjectExists(db, "table", object2.name);
17951
+ }
17952
+
17599
17953
  // ../../packages/persistence/src/ids.ts
17600
17954
  import { createHash } from "crypto";
17601
17955
  function sha256Hex(input) {
@@ -17632,28 +17986,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17632
17986
  }
17633
17987
 
17634
17988
  // ../../packages/persistence/src/migrations.ts
17635
- function evidenceObjects(sql) {
17636
- const objects = [];
17637
- for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17638
- if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17639
- objects.push({ kind: "table", name: m[1] });
17640
- }
17641
- }
17642
- for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17643
- if (m[1] !== void 0 && m[2] !== void 0) {
17644
- objects.push({ kind: "column", table: m[1], name: m[2] });
17645
- }
17646
- }
17647
- return objects;
17648
- }
17649
- function evidenceExists(db, object2) {
17650
- if (object2.kind === "column") {
17651
- const columns = db.prepare(`PRAGMA table_xinfo(${object2.table})`).all();
17652
- return columns.some((c) => c.name === object2.name);
17653
- }
17654
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(object2.name);
17655
- return row !== void 0;
17656
- }
17657
17989
  function describeObject(object2) {
17658
17990
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17659
17991
  }
@@ -17664,10 +17996,6 @@ function createdIndexName(statement) {
17664
17996
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17665
17997
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17666
17998
  }
17667
- function indexExists(db, name) {
17668
- const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1").get(name);
17669
- return row !== void 0;
17670
- }
17671
17999
  function applyMigrations(db) {
17672
18000
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17673
18001
  db.exec(
@@ -17686,44 +18014,39 @@ function applyMigrations(db) {
17686
18014
  const present = evidence.filter((o) => evidenceExists(db, o));
17687
18015
  if (present.length > 0 && present.length < evidence.length) {
17688
18016
  const missing = evidence.filter((o) => !present.includes(o));
17689
- const message2 = `[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.`;
17690
- process.stderr.write(`${message2}
17691
- `);
17692
- throw new Error(message2);
18017
+ const message2 = `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.`;
18018
+ akaWarn(message2);
18019
+ throw new Error(`[aka] ${message2}`);
17693
18020
  }
17694
18021
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17695
18022
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17696
18023
  const statements = splitStatements(migration.sql);
17697
18024
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17698
18025
  try {
17699
- db.exec("BEGIN IMMEDIATE");
17700
- try {
17701
- for (const statement of statements) {
17702
- const indexName = createdIndexName(statement);
17703
- if (indexName === void 0) {
17704
- if (alreadyApplied) continue;
17705
- } else if (indexExists(db, indexName)) {
17706
- continue;
18026
+ withTransaction(
18027
+ db,
18028
+ () => {
18029
+ for (const statement of statements) {
18030
+ const indexName = createdIndexName(statement);
18031
+ if (indexName === void 0) {
18032
+ if (alreadyApplied) continue;
18033
+ } else if (indexExists(db, indexName)) {
18034
+ continue;
18035
+ }
18036
+ db.exec(statement);
17707
18037
  }
17708
- db.exec(statement);
17709
- }
17710
- if (wantsFkOff && !alreadyApplied) {
17711
- const violations = db.prepare("PRAGMA foreign_key_check").all();
17712
- if (violations.length > 0) {
17713
- throw new Error(
17714
- `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17715
- );
18038
+ if (wantsFkOff && !alreadyApplied) {
18039
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
18040
+ if (violations.length > 0) {
18041
+ throw new Error(
18042
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
18043
+ );
18044
+ }
17716
18045
  }
17717
- }
17718
- record2.run(migration.tag, Date.now());
17719
- db.exec("COMMIT");
17720
- } catch (error51) {
17721
- try {
17722
- db.exec("ROLLBACK");
17723
- } catch {
17724
- }
17725
- throw error51;
17726
- }
18046
+ record2.run(migration.tag, Date.now());
18047
+ },
18048
+ "IMMEDIATE"
18049
+ );
17727
18050
  } finally {
17728
18051
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17729
18052
  }
@@ -17766,8 +18089,7 @@ var TOKEN_USAGE_COLUMNS = [
17766
18089
  }
17767
18090
  ];
17768
18091
  function ensureTokenUsageColumns(db) {
17769
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17770
- const existing = new Set(columns.map((c) => c.name));
18092
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17771
18093
  for (const column of TOKEN_USAGE_COLUMNS) {
17772
18094
  if (!existing.has(column.name)) {
17773
18095
  db.exec(column.ddl);
@@ -17805,47 +18127,39 @@ function reconcileSourceProjectIds(db) {
17805
18127
  repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
17806
18128
  }));
17807
18129
  const deleteLegacy = db.prepare("DELETE FROM source_project WHERE id = ?");
17808
- db.exec("BEGIN IMMEDIATE");
17809
- try {
17810
- for (const { row, canonicalId } of legacy) {
17811
- foldProject.run(
17812
- canonicalId,
17813
- row.url,
17814
- row.name,
17815
- row.attributes,
17816
- row.firstSeen,
17817
- row.lastSeen
17818
- );
17819
- repointAudit.run(canonicalId, row.id);
17820
- for (const { dropCollisions, repoint } of pathTables) {
17821
- dropCollisions.run(row.id, canonicalId);
17822
- repoint.run(canonicalId, row.id);
18130
+ withTransaction(
18131
+ db,
18132
+ () => {
18133
+ for (const { row, canonicalId } of legacy) {
18134
+ foldProject.run(
18135
+ canonicalId,
18136
+ row.url,
18137
+ row.name,
18138
+ row.attributes,
18139
+ row.firstSeen,
18140
+ row.lastSeen
18141
+ );
18142
+ repointAudit.run(canonicalId, row.id);
18143
+ for (const { dropCollisions, repoint } of pathTables) {
18144
+ dropCollisions.run(row.id, canonicalId);
18145
+ repoint.run(canonicalId, row.id);
18146
+ }
18147
+ repointCallSite.run(canonicalId, row.id);
18148
+ deleteLegacy.run(row.id);
17823
18149
  }
17824
- repointCallSite.run(canonicalId, row.id);
17825
- deleteLegacy.run(row.id);
17826
- }
17827
- db.exec("COMMIT");
17828
- } catch (error51) {
17829
- try {
17830
- db.exec("ROLLBACK");
17831
- } catch {
17832
- }
17833
- throw error51;
17834
- }
18150
+ },
18151
+ "IMMEDIATE"
18152
+ );
17835
18153
  } catch (error51) {
17836
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17837
- `);
18154
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17838
18155
  }
17839
18156
  }
17840
18157
  function isForeignSqliteLineage(db) {
17841
- const tenantsTable = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tenants' LIMIT 1").get();
17842
- if (tenantsTable !== void 0) return true;
17843
- const eventsColumns = db.prepare("PRAGMA table_info(events)").all();
17844
- return eventsColumns.some((c) => c.name === "tenant_id");
18158
+ if (schemaObjectExists(db, "table", "tenants")) return true;
18159
+ return columnNames(db, "events").includes("tenant_id");
17845
18160
  }
17846
18161
  function ensureSyncedAtColumn(db, table) {
17847
- const columns = db.prepare(`PRAGMA table_info(${table})`).all();
17848
- if (!columns.some((c) => c.name === "synced_at")) {
18162
+ if (!columnNames(db, table).includes("synced_at")) {
17849
18163
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
17850
18164
  }
17851
18165
  }
@@ -17896,8 +18210,11 @@ function ensureDataDirSync(dir) {
17896
18210
  } catch {
17897
18211
  }
17898
18212
  }
18213
+ function walSidecars(file2) {
18214
+ return [`${file2}-wal`, `${file2}-shm`];
18215
+ }
17899
18216
  function tightenPerms(file2) {
17900
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
18217
+ for (const path of [file2, ...walSidecars(file2)]) {
17901
18218
  try {
17902
18219
  chmodSync(path, DATA_FILE_MODE);
17903
18220
  } catch {
@@ -17905,12 +18222,68 @@ function tightenPerms(file2) {
17905
18222
  }
17906
18223
  }
17907
18224
 
17908
- // ../../packages/persistence/src/repositories/sql-utils.ts
17909
- function escapeLikePattern(s) {
17910
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
18225
+ // ../../packages/persistence/src/internal/json.ts
18226
+ function safeJson(s, fallback) {
18227
+ if (s == null) return fallback;
18228
+ try {
18229
+ return JSON.parse(s);
18230
+ } catch {
18231
+ return fallback;
18232
+ }
17911
18233
  }
17912
- function placeholders(n) {
17913
- return Array.from({ length: n }, () => "?").join(", ");
18234
+ function parseJsonObject(s) {
18235
+ if (s == null) return void 0;
18236
+ try {
18237
+ const parsed = JSON.parse(s);
18238
+ if (typeof parsed === "object" && parsed !== null) return parsed;
18239
+ } catch {
18240
+ }
18241
+ return void 0;
18242
+ }
18243
+
18244
+ // ../../packages/persistence/src/internal/rows.ts
18245
+ function allRows(stmt, params) {
18246
+ if (params === void 0) return stmt.all();
18247
+ if (Array.isArray(params)) return stmt.all(...params);
18248
+ return stmt.all(params);
18249
+ }
18250
+ function getRow(stmt, params) {
18251
+ if (params === void 0) return stmt.get();
18252
+ if (Array.isArray(params)) return stmt.get(...params);
18253
+ return stmt.get(params);
18254
+ }
18255
+ function intToBool(raw) {
18256
+ return raw === 1 || raw === true;
18257
+ }
18258
+ function boolToInt(b) {
18259
+ return b ? 1 : 0;
18260
+ }
18261
+ function bindParams(row) {
18262
+ const out = {};
18263
+ for (const [key, value] of Object.entries(row)) {
18264
+ out[key] = value === void 0 ? null : value;
18265
+ }
18266
+ return out;
18267
+ }
18268
+ function countScalar(db, sql, params) {
18269
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18270
+ }
18271
+ function countBy(db, sql, params) {
18272
+ const map2 = /* @__PURE__ */ new Map();
18273
+ for (const row of allRows(db.prepare(sql), params)) {
18274
+ map2.set(row.k, row.n);
18275
+ }
18276
+ return map2;
18277
+ }
18278
+ function mapRowsTolerant(rows, map2) {
18279
+ const out = [];
18280
+ for (const row of rows) {
18281
+ try {
18282
+ out.push(map2(row));
18283
+ } catch {
18284
+ }
18285
+ }
18286
+ return out;
17914
18287
  }
17915
18288
 
17916
18289
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17962,15 +18335,11 @@ function encodeCursor(payload) {
17962
18335
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17963
18336
  }
17964
18337
  function decodeCursor(cursor) {
17965
- try {
17966
- const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
17967
- if (parsed !== null && typeof parsed === "object" && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
17968
- return parsed;
17969
- }
17970
- return null;
17971
- } catch {
17972
- return null;
18338
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18339
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18340
+ return parsed;
17973
18341
  }
18342
+ return null;
17974
18343
  }
17975
18344
  var DB_EVENT_TYPE_TO_KIND = {
17976
18345
  session: "session",
@@ -17987,15 +18356,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17987
18356
  };
17988
18357
  function safeParseStringArray(raw) {
17989
18358
  if (!raw) return [];
17990
- try {
17991
- const parsed = JSON.parse(raw);
17992
- return Array.isArray(parsed) ? parsed : [];
17993
- } catch {
17994
- return [];
17995
- }
17996
- }
17997
- function toBool(raw) {
17998
- return raw === 1 || raw === true;
18359
+ const parsed = safeJson(raw, null);
18360
+ return Array.isArray(parsed) ? parsed : [];
17999
18361
  }
18000
18362
  function toHarness(raw) {
18001
18363
  const parsed = Harness.safeParse(raw);
@@ -18048,8 +18410,8 @@ function buildAuditEvent(row) {
18048
18410
  severity: severityParsed?.success ? severityParsed.data : null,
18049
18411
  link: linkParsed?.success ? linkParsed.data : null,
18050
18412
  targetId: row.target_id,
18051
- internal: toBool(row.internal),
18052
- flagged: toBool(row.flagged)
18413
+ internal: intToBool(row.internal),
18414
+ flagged: intToBool(row.flagged)
18053
18415
  };
18054
18416
  }
18055
18417
  var TIMELINE_COLUMNS = `
@@ -18065,6 +18427,10 @@ var TIMELINE_COLUMNS = `
18065
18427
  json_extract(attributes, '$.internal') AS internal,
18066
18428
  json_extract(attributes, '$.flagged') AS flagged`;
18067
18429
  var SESSION_ROOT = `event_type = 'session'`;
18430
+ var HAS_ACTIVITY = `EXISTS (
18431
+ SELECT 1 FROM audit_events c
18432
+ WHERE c.root_session_id = audit_events.id
18433
+ AND c.event_type NOT IN ('hook', 'config_scan'))`;
18068
18434
  var SqliteActivityRepository = class {
18069
18435
  constructor(db, now = () => Date.now()) {
18070
18436
  this.db = db;
@@ -18075,12 +18441,16 @@ var SqliteActivityRepository = class {
18075
18441
  stats(tz) {
18076
18442
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
18077
18443
  const { startMs, endMs } = window;
18078
- const sessionsToday = this.db.prepare(
18444
+ const sessionsToday = countScalar(
18445
+ this.db,
18079
18446
  `SELECT count(*) AS n FROM audit_events
18080
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
18081
- ).get(startMs, endMs).n;
18447
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?
18448
+ AND ${HAS_ACTIVITY}`,
18449
+ [startMs, endMs]
18450
+ );
18082
18451
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
18083
- const liveNow = this.db.prepare(
18452
+ const liveNow = countScalar(
18453
+ this.db,
18084
18454
  `SELECT count(*) AS n FROM audit_events s
18085
18455
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
18086
18456
  AND max(
@@ -18089,22 +18459,29 @@ var SqliteActivityRepository = class {
18089
18459
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
18090
18460
  s.started_at
18091
18461
  )
18092
- ) >= ?`
18093
- ).get(liveThreshold).n;
18094
- const toolCallsToday = this.db.prepare(
18462
+ ) >= ?`,
18463
+ [liveThreshold]
18464
+ );
18465
+ const toolCallsToday = countScalar(
18466
+ this.db,
18095
18467
  `SELECT count(*) AS n FROM audit_events
18096
- WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`
18097
- ).get(startMs, endMs).n;
18098
- const findingsToday = this.db.prepare(
18468
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18469
+ [startMs, endMs]
18470
+ );
18471
+ const findingsToday = countScalar(
18472
+ this.db,
18099
18473
  `SELECT count(*) AS n FROM inspection_findings f
18100
18474
  JOIN audit_events e ON e.id = f.audit_event_id
18101
- WHERE e.started_at >= ? AND e.started_at < ?`
18102
- ).get(startMs, endMs).n;
18103
- const egressToday = this.db.prepare(
18475
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18476
+ [startMs, endMs]
18477
+ );
18478
+ const egressToday = countScalar(
18479
+ this.db,
18104
18480
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
18105
18481
  FROM audit_events
18106
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
18107
- ).get(startMs, endMs).n;
18482
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18483
+ [startMs, endMs]
18484
+ );
18108
18485
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18109
18486
  }
18110
18487
  listSessions(query) {
@@ -18126,7 +18503,7 @@ var SqliteActivityRepository = class {
18126
18503
  conditions.push("started_at <= ?");
18127
18504
  params.push(toMs);
18128
18505
  if (query.q) {
18129
- const pattern = `%${escapeLikePattern(query.q)}%`;
18506
+ const pattern = containsPattern(query.q);
18130
18507
  conditions.push(
18131
18508
  `(content LIKE ? ESCAPE '\\'
18132
18509
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18140,13 +18517,21 @@ var SqliteActivityRepository = class {
18140
18517
  );
18141
18518
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
18142
18519
  }
18520
+ const emptyCount = countScalar(
18521
+ this.db,
18522
+ `SELECT count(*) AS n FROM audit_events
18523
+ WHERE ${[...conditions, `NOT ${HAS_ACTIVITY}`].join(" AND ")}`,
18524
+ params
18525
+ );
18526
+ if (query.excludeEmpty) conditions.push(HAS_ACTIVITY);
18143
18527
  if (cursor) {
18144
18528
  conditions.push("(started_at < ? OR (started_at = ? AND id < ?))");
18145
18529
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18146
18530
  }
18147
18531
  const limit = query.limit;
18148
- const rows = this.db.prepare(
18149
- `SELECT id,
18532
+ const rows = allRows(
18533
+ this.db.prepare(
18534
+ `SELECT id,
18150
18535
  json_extract(attributes, '$.harness') AS harness,
18151
18536
  content AS title,
18152
18537
  json_extract(attributes, '$.project') AS project,
@@ -18159,7 +18544,9 @@ var SqliteActivityRepository = class {
18159
18544
  WHERE ${conditions.join(" AND ")}
18160
18545
  ORDER BY started_at DESC, id DESC
18161
18546
  LIMIT ?`
18162
- ).all(...params, limit + 1);
18547
+ ),
18548
+ [...params, limit + 1]
18549
+ );
18163
18550
  const hasMore = rows.length > limit;
18164
18551
  const page = hasMore ? rows.slice(0, limit) : rows;
18165
18552
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18173,11 +18560,12 @@ var SqliteActivityRepository = class {
18173
18560
  );
18174
18561
  const last = page[page.length - 1];
18175
18562
  const nextCursor = hasMore && last ? encodeCursor({ startedAtMs: last.started_at, id: last.id }) : null;
18176
- return Promise.resolve({ items, nextCursor });
18563
+ return Promise.resolve({ items, nextCursor, emptyCount });
18177
18564
  }
18178
18565
  getSession(sessionId) {
18179
- const rootRow = this.db.prepare(
18180
- `SELECT id,
18566
+ const rootRow = getRow(
18567
+ this.db.prepare(
18568
+ `SELECT id,
18181
18569
  json_extract(attributes, '$.harness') AS harness,
18182
18570
  content AS title,
18183
18571
  json_extract(attributes, '$.project') AS project,
@@ -18194,47 +18582,66 @@ var SqliteActivityRepository = class {
18194
18582
  FROM audit_events
18195
18583
  WHERE id = ? AND event_type = 'session'
18196
18584
  LIMIT 1`
18197
- ).get(sessionId);
18585
+ ),
18586
+ [sessionId]
18587
+ );
18198
18588
  if (!rootRow) return Promise.resolve(null);
18199
- const timelineRows = this.db.prepare(
18200
- `SELECT ${TIMELINE_COLUMNS}
18589
+ const timelineRows = allRows(
18590
+ this.db.prepare(
18591
+ `SELECT ${TIMELINE_COLUMNS}
18201
18592
  FROM audit_events
18202
18593
  WHERE id = ? OR root_session_id = ?
18203
18594
  ORDER BY started_at ASC, id ASC`
18204
- ).all(sessionId, sessionId);
18595
+ ),
18596
+ [sessionId, sessionId]
18597
+ );
18205
18598
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18206
- const tokenRow = this.db.prepare(
18207
- `SELECT
18599
+ const tokenRow = getRow(
18600
+ this.db.prepare(
18601
+ `SELECT
18208
18602
  coalesce(sum(input_tokens), 0) AS input,
18209
18603
  coalesce(sum(output_tokens), 0) AS output,
18210
18604
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18211
18605
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18212
18606
  FROM audit_events
18213
18607
  WHERE root_session_id = ? AND event_type = 'llm_call'`
18214
- ).get(sessionId);
18215
- const primaryModel = this.db.prepare(
18216
- `SELECT model, provider FROM audit_events
18608
+ ),
18609
+ [sessionId]
18610
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18611
+ const primaryModel = getRow(
18612
+ this.db.prepare(
18613
+ `SELECT model, provider FROM audit_events
18217
18614
  WHERE root_session_id = ? AND event_type = 'llm_call'
18218
18615
  ORDER BY started_at ASC, id ASC
18219
18616
  LIMIT 1`
18220
- ).get(sessionId);
18221
- const toolRows = this.db.prepare(
18222
- `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18617
+ ),
18618
+ [sessionId]
18619
+ );
18620
+ const toolRows = allRows(
18621
+ this.db.prepare(
18622
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18223
18623
  count(*) AS n
18224
18624
  FROM audit_events
18225
18625
  WHERE root_session_id = ? AND event_type = 'tool_call'
18226
18626
  GROUP BY coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool'))`
18227
- ).all(sessionId);
18228
- const modelRows = this.db.prepare(
18229
- `SELECT DISTINCT model FROM audit_events
18627
+ ),
18628
+ [sessionId]
18629
+ );
18630
+ const modelRows = allRows(
18631
+ this.db.prepare(
18632
+ `SELECT DISTINCT model FROM audit_events
18230
18633
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18231
18634
  ORDER BY model`
18232
- ).all(sessionId);
18635
+ ),
18636
+ [sessionId]
18637
+ );
18233
18638
  const derivedModels = modelRows.map((r) => r.model);
18234
- const commits = this.db.prepare(
18639
+ const commits = countScalar(
18640
+ this.db,
18235
18641
  `SELECT count(*) AS n FROM audit_events
18236
- WHERE root_session_id = ? AND event_type = 'commit'`
18237
- ).get(sessionId).n;
18642
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18643
+ [sessionId]
18644
+ );
18238
18645
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18239
18646
  turns: 0,
18240
18647
  findings: 0,
@@ -18301,7 +18708,10 @@ var SqliteActivityRepository = class {
18301
18708
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18302
18709
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18303
18710
  );
18304
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18711
+ const rows = allRows(
18712
+ stmt,
18713
+ fromMs === void 0 ? void 0 : [fromMs]
18714
+ );
18305
18715
  const seen = /* @__PURE__ */ new Set();
18306
18716
  for (const row of rows) seen.add(toHarness(row.harness));
18307
18717
  return Promise.resolve([...seen]);
@@ -18324,23 +18734,23 @@ var SqliteActivityRepository = class {
18324
18734
  conditions.push("started_at >= ?");
18325
18735
  params.push(opts.fromMs);
18326
18736
  }
18327
- const rows = this.db.prepare(
18328
- `SELECT root_session_id AS sessionId, attributes
18737
+ const rows = allRows(
18738
+ this.db.prepare(
18739
+ `SELECT root_session_id AS sessionId, attributes
18329
18740
  FROM audit_events
18330
18741
  WHERE ${conditions.join(" AND ")}`
18331
- ).all(...params);
18332
- const leaves = [];
18333
- for (const row of rows) {
18334
- if (row.sessionId === null) continue;
18335
- try {
18336
- leaves.push({
18337
- sessionId: row.sessionId,
18338
- attributes: JSON.parse(row.attributes)
18339
- });
18340
- } catch {
18341
- }
18342
- }
18343
- return leaves;
18742
+ ),
18743
+ params
18744
+ );
18745
+ return mapRowsTolerant(
18746
+ rows.filter(
18747
+ (row) => row.sessionId !== null
18748
+ ),
18749
+ (row) => ({
18750
+ sessionId: row.sessionId,
18751
+ attributes: JSON.parse(row.attributes)
18752
+ })
18753
+ );
18344
18754
  }
18345
18755
  /**
18346
18756
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18354,57 +18764,72 @@ var SqliteActivityRepository = class {
18354
18764
  );
18355
18765
  if (sessionIds.length === 0) return result;
18356
18766
  const inClause = placeholders(sessionIds.length);
18357
- const lastActivityRows = this.db.prepare(
18358
- `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18767
+ const lastActivityRows = allRows(
18768
+ this.db.prepare(
18769
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18359
18770
  WHERE root_session_id IN (${inClause})
18360
18771
  GROUP BY root_session_id`
18361
- ).all(...sessionIds);
18772
+ ),
18773
+ sessionIds
18774
+ );
18362
18775
  for (const row of lastActivityRows) {
18363
18776
  if (row.id === null) continue;
18364
18777
  const entry = result.get(row.id);
18365
18778
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18366
18779
  }
18367
- const turnsRows = this.db.prepare(
18368
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18780
+ const turnsRows = allRows(
18781
+ this.db.prepare(
18782
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18369
18783
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18370
18784
  GROUP BY root_session_id`
18371
- ).all(...sessionIds);
18785
+ ),
18786
+ sessionIds
18787
+ );
18372
18788
  for (const row of turnsRows) {
18373
18789
  if (row.id === null) continue;
18374
18790
  const entry = result.get(row.id);
18375
18791
  if (entry) entry.turns = row.n;
18376
18792
  }
18377
- const runKeyRows = this.db.prepare(
18378
- `SELECT root_session_id AS id,
18793
+ const runKeyRows = allRows(
18794
+ this.db.prepare(
18795
+ `SELECT root_session_id AS id,
18379
18796
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18380
18797
  FROM audit_events
18381
18798
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18382
18799
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18383
18800
  GROUP BY root_session_id`
18384
- ).all(...sessionIds);
18801
+ ),
18802
+ sessionIds
18803
+ );
18385
18804
  for (const row of runKeyRows) {
18386
18805
  if (row.id === null) continue;
18387
18806
  const entry = result.get(row.id);
18388
18807
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18389
18808
  }
18390
- const findingsRows = this.db.prepare(
18391
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18809
+ const findingsRows = allRows(
18810
+ this.db.prepare(
18811
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18392
18812
  JOIN audit_events e ON e.id = f.audit_event_id
18393
18813
  WHERE e.root_session_id IN (${inClause})
18394
18814
  GROUP BY e.root_session_id`
18395
- ).all(...sessionIds);
18815
+ ),
18816
+ sessionIds
18817
+ );
18396
18818
  for (const row of findingsRows) {
18397
18819
  if (row.id === null) continue;
18398
18820
  const entry = result.get(row.id);
18399
18821
  if (entry) entry.findings = row.n;
18400
18822
  }
18401
- const sharesRows = this.db.prepare(
18402
- `SELECT root_session_id AS id,
18823
+ const sharesRows = allRows(
18824
+ this.db.prepare(
18825
+ `SELECT root_session_id AS id,
18403
18826
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18404
18827
  FROM audit_events
18405
18828
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18406
18829
  GROUP BY root_session_id`
18407
- ).all(...sessionIds);
18830
+ ),
18831
+ sessionIds
18832
+ );
18408
18833
  for (const row of sharesRows) {
18409
18834
  if (row.id === null) continue;
18410
18835
  const entry = result.get(row.id);
@@ -18454,33 +18879,28 @@ var SqliteAuditEventsRepository = class {
18454
18879
  // the caller fails open and drops the whole pass — recovered idempotently on the
18455
18880
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18456
18881
  runInTransaction(fn) {
18457
- this.db.exec("BEGIN");
18458
- try {
18459
- fn();
18460
- this.db.exec("COMMIT");
18461
- } catch (err) {
18462
- this.db.exec("ROLLBACK");
18463
- throw err;
18464
- }
18882
+ withTransaction(this.db, fn);
18465
18883
  }
18466
18884
  insertAuditEvent(input) {
18467
18885
  const row = toAuditEventRow(input);
18468
- this.insertStmt.run({
18469
- id: row.id,
18470
- parentId: row.parentId ?? null,
18471
- rootSessionId: row.rootSessionId ?? null,
18472
- eventType: row.eventType,
18473
- hostId: row.hostId ?? null,
18474
- harnessId: row.harnessId ?? null,
18475
- sourceProjectId: row.sourceProjectId ?? null,
18476
- startedAt: row.startedAt,
18477
- endedAt: row.endedAt ?? null,
18478
- severity: row.severity ?? null,
18479
- priority: row.priority ?? null,
18480
- content: row.content ?? null,
18481
- contentHash: row.contentHash ?? null,
18482
- attributes: row.attributes ?? null
18483
- });
18886
+ this.insertStmt.run(
18887
+ bindParams({
18888
+ id: row.id,
18889
+ parentId: row.parentId,
18890
+ rootSessionId: row.rootSessionId,
18891
+ eventType: row.eventType,
18892
+ hostId: row.hostId,
18893
+ harnessId: row.harnessId,
18894
+ sourceProjectId: row.sourceProjectId,
18895
+ startedAt: row.startedAt,
18896
+ endedAt: row.endedAt,
18897
+ severity: row.severity,
18898
+ priority: row.priority,
18899
+ content: row.content,
18900
+ contentHash: row.contentHash,
18901
+ attributes: row.attributes
18902
+ })
18903
+ );
18484
18904
  }
18485
18905
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18486
18906
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18494,22 +18914,24 @@ var SqliteAuditEventsRepository = class {
18494
18914
  const startedAt = isoToEpochMillis(input.startedAt);
18495
18915
  if (!Number.isFinite(startedAt)) return;
18496
18916
  const id = llmCallId(input.sessionId, input.messageId);
18497
- this.upsertLlmCallStmt.run({
18498
- id,
18499
- parentId: input.parentId,
18500
- rootSessionId: input.rootSessionId,
18501
- eventType: "llm_call",
18502
- hostId: null,
18503
- harnessId: null,
18504
- sourceProjectId: null,
18505
- startedAt,
18506
- endedAt: null,
18507
- severity: null,
18508
- priority: null,
18509
- content: null,
18510
- contentHash: null,
18511
- attributes: JSON.stringify(input.attributes)
18512
- });
18917
+ this.upsertLlmCallStmt.run(
18918
+ bindParams({
18919
+ id,
18920
+ parentId: input.parentId,
18921
+ rootSessionId: input.rootSessionId,
18922
+ eventType: "llm_call",
18923
+ hostId: null,
18924
+ harnessId: null,
18925
+ sourceProjectId: null,
18926
+ startedAt,
18927
+ endedAt: null,
18928
+ severity: null,
18929
+ priority: null,
18930
+ content: null,
18931
+ contentHash: null,
18932
+ attributes: JSON.stringify(input.attributes)
18933
+ })
18934
+ );
18513
18935
  }
18514
18936
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18515
18937
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18533,25 +18955,29 @@ var SqliteAuditEventsRepository = class {
18533
18955
  const startedAt = isoToEpochMillis(input.startedAt);
18534
18956
  if (!Number.isFinite(startedAt)) return;
18535
18957
  const id = toolCallId(input.sessionId, input.toolUseId);
18536
- this.insertStmt.run({
18537
- id,
18538
- parentId: input.parentId,
18539
- rootSessionId: input.rootSessionId,
18540
- eventType: "tool_call",
18541
- hostId: null,
18542
- harnessId: null,
18543
- sourceProjectId: null,
18544
- startedAt,
18545
- endedAt: null,
18546
- severity: null,
18547
- priority: null,
18548
- content: null,
18549
- contentHash: null,
18550
- attributes: JSON.stringify(input.attributes)
18551
- });
18958
+ this.insertStmt.run(
18959
+ bindParams({
18960
+ id,
18961
+ parentId: input.parentId,
18962
+ rootSessionId: input.rootSessionId,
18963
+ eventType: "tool_call",
18964
+ hostId: null,
18965
+ harnessId: null,
18966
+ sourceProjectId: null,
18967
+ startedAt,
18968
+ endedAt: null,
18969
+ severity: null,
18970
+ priority: null,
18971
+ content: null,
18972
+ contentHash: null,
18973
+ attributes: JSON.stringify(input.attributes)
18974
+ })
18975
+ );
18552
18976
  }
18553
18977
  findById(id) {
18554
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18978
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18979
+ id
18980
+ });
18555
18981
  }
18556
18982
  // Read the `provider` snapshotted onto a session root's attributes.
18557
18983
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18561,14 +18987,8 @@ var SqliteAuditEventsRepository = class {
18561
18987
  sessionProvider(sessionId) {
18562
18988
  const row = this.findById(sessionId);
18563
18989
  if (!row?.attributes) return void 0;
18564
- try {
18565
- const parsed = JSON.parse(row.attributes);
18566
- if (typeof parsed === "object" && parsed !== null) {
18567
- const provider = parsed.provider;
18568
- if (typeof provider === "string") return provider;
18569
- }
18570
- } catch {
18571
- }
18990
+ const provider = parseJsonObject(row.attributes)?.provider;
18991
+ if (typeof provider === "string") return provider;
18572
18992
  return void 0;
18573
18993
  }
18574
18994
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18577,11 +18997,13 @@ var SqliteAuditEventsRepository = class {
18577
18997
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18578
18998
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18579
18999
  llmCallLeaves() {
18580
- return this.db.prepare(
18581
- `SELECT root_session_id AS sessionId, attributes
19000
+ return allRows(
19001
+ this.db.prepare(
19002
+ `SELECT root_session_id AS sessionId, attributes
18582
19003
  FROM audit_events
18583
19004
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18584
- ).all();
19005
+ )
19006
+ );
18585
19007
  }
18586
19008
  };
18587
19009
 
@@ -18600,16 +19022,29 @@ var SqliteClassifiedDataRepository = class {
18600
19022
  upsert(input) {
18601
19023
  const id = classifiedDataId(input.class);
18602
19024
  const row = toClassifiedDataRow(input, id);
18603
- this.insertStmt.run({
18604
- id: row.id,
18605
- class: row.class,
18606
- label: row.label ?? null,
18607
- attributes: row.attributes ?? null
18608
- });
19025
+ this.insertStmt.run(
19026
+ bindParams({
19027
+ id: row.id,
19028
+ class: row.class,
19029
+ label: row.label,
19030
+ attributes: row.attributes
19031
+ })
19032
+ );
18609
19033
  return id;
18610
19034
  }
18611
19035
  };
18612
19036
 
19037
+ // ../../packages/persistence/src/repositories/config-scan.ts
19038
+ function latestConfigScan(db) {
19039
+ return getRow(
19040
+ db.prepare(
19041
+ `SELECT id, started_at, attributes FROM audit_events
19042
+ WHERE event_type = 'config_scan'
19043
+ ORDER BY started_at DESC, id DESC LIMIT 1`
19044
+ )
19045
+ );
19046
+ }
19047
+
18613
19048
  // ../../packages/persistence/src/repositories/config-inventory.ts
18614
19049
  var SqliteConfigInventoryRepository = class {
18615
19050
  constructor(db) {
@@ -18617,7 +19052,7 @@ var SqliteConfigInventoryRepository = class {
18617
19052
  }
18618
19053
  db;
18619
19054
  report() {
18620
- const scan2 = this.latestScan();
19055
+ const scan2 = latestConfigScan(this.db);
18621
19056
  if (!scan2) {
18622
19057
  return {
18623
19058
  scannedAt: null,
@@ -18628,17 +19063,23 @@ var SqliteConfigInventoryRepository = class {
18628
19063
  topics: []
18629
19064
  };
18630
19065
  }
18631
- const rows = this.db.prepare(
18632
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
19066
+ const rows = allRows(
19067
+ this.db.prepare(
19068
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18633
19069
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18634
19070
  ORDER BY object_type, title`
18635
- ).all({ startedAt: scan2.started_at });
18636
- const findings = this.db.prepare(
18637
- `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
19071
+ ),
19072
+ { startedAt: scan2.started_at }
19073
+ );
19074
+ const findings = allRows(
19075
+ this.db.prepare(
19076
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18638
19077
  FROM inspection_findings f
18639
19078
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18640
19079
  WHERE f.audit_event_id = :scanId`
18641
- ).all({ scanId: scan2.id });
19080
+ ),
19081
+ { scanId: scan2.id }
19082
+ );
18642
19083
  const skills = [];
18643
19084
  const hooks = [];
18644
19085
  const mcpServers = [];
@@ -18667,7 +19108,9 @@ var SqliteConfigInventoryRepository = class {
18667
19108
  // schema note); an override whose asset is gone simply never matches. A row
18668
19109
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18669
19110
  trustOverrides() {
18670
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
19111
+ const rows = allRows(
19112
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
19113
+ );
18671
19114
  const map2 = /* @__PURE__ */ new Map();
18672
19115
  for (const row of rows) {
18673
19116
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18676,13 +19119,6 @@ var SqliteConfigInventoryRepository = class {
18676
19119
  }
18677
19120
  return map2;
18678
19121
  }
18679
- latestScan() {
18680
- return this.db.prepare(
18681
- `SELECT id, started_at, attributes FROM audit_events
18682
- WHERE event_type = 'config_scan'
18683
- ORDER BY started_at DESC, id DESC LIMIT 1`
18684
- ).get();
18685
- }
18686
19122
  };
18687
19123
  function toSkillItem(row, bag) {
18688
19124
  const item = {
@@ -18793,22 +19229,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18793
19229
  return topics;
18794
19230
  }
18795
19231
  function countScanErrors(attributes) {
18796
- if (!attributes) return 0;
18797
- try {
18798
- const parsed = JSON.parse(attributes);
18799
- const errors = parsed?.errors;
18800
- return typeof errors === "number" ? errors : 0;
18801
- } catch {
18802
- return 0;
18803
- }
19232
+ const errors = parseJsonObject(attributes)?.errors;
19233
+ return typeof errors === "number" ? errors : 0;
18804
19234
  }
18805
19235
  function parseBag(raw) {
18806
- try {
18807
- const parsed = JSON.parse(raw);
18808
- if (typeof parsed === "object" && parsed !== null) return parsed;
18809
- } catch {
18810
- }
18811
- return void 0;
19236
+ return parseJsonObject(raw);
18812
19237
  }
18813
19238
  function str(value) {
18814
19239
  return typeof value === "string" ? value : void 0;
@@ -18817,12 +19242,7 @@ function str(value) {
18817
19242
  // ../../packages/persistence/src/repositories/detections.ts
18818
19243
  var DAY_MS2 = 864e5;
18819
19244
  function parseRules(rulesJson) {
18820
- let raw;
18821
- try {
18822
- raw = JSON.parse(rulesJson);
18823
- } catch {
18824
- return [];
18825
- }
19245
+ const raw = safeJson(rulesJson, []);
18826
19246
  if (!Array.isArray(raw)) return [];
18827
19247
  const rules = [];
18828
19248
  for (const entry of raw) {
@@ -18841,11 +19261,13 @@ var SqliteDetectionsRepository = class {
18841
19261
  db;
18842
19262
  now;
18843
19263
  listDetections(query) {
18844
- const rows = this.db.prepare(
18845
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19264
+ const rows = allRows(
19265
+ this.db.prepare(
19266
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18846
19267
  rules_json AS rulesJson
18847
19268
  FROM installed_packs`
18848
- ).all();
19269
+ )
19270
+ );
18849
19271
  const available = this.availableByPack();
18850
19272
  const summaries = rows.map((r) => {
18851
19273
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18854,7 +19276,7 @@ var SqliteDetectionsRepository = class {
18854
19276
  packId: r.packId,
18855
19277
  version: r.version,
18856
19278
  name: r.name,
18857
- enabled: r.enabled === 1,
19279
+ enabled: intToBool(r.enabled),
18858
19280
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18859
19281
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18860
19282
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18871,19 +19293,23 @@ var SqliteDetectionsRepository = class {
18871
19293
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18872
19294
  // detail call; the table is a handful of rows).
18873
19295
  availableByPack() {
18874
- const rows = this.db.prepare(
18875
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19296
+ const rows = allRows(
19297
+ this.db.prepare(
19298
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18876
19299
  FROM available_packs`
18877
- ).all();
19300
+ )
19301
+ );
18878
19302
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18879
19303
  }
18880
19304
  getDetectionStats() {
18881
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
19305
+ const rows = allRows(
19306
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
19307
+ );
18882
19308
  let rules = 0;
18883
19309
  let active = 0;
18884
19310
  const ruleIds = /* @__PURE__ */ new Set();
18885
19311
  for (const r of rows) {
18886
- if (r.enabled === 1) active += 1;
19312
+ if (intToBool(r.enabled)) active += 1;
18887
19313
  const parsed = parseRules(r.rulesJson);
18888
19314
  rules += parsed.length;
18889
19315
  for (const rule of parsed) {
@@ -18901,12 +19327,15 @@ var SqliteDetectionsRepository = class {
18901
19327
  const parts = splitDetectionId(id);
18902
19328
  if (!parts) return Promise.resolve(null);
18903
19329
  const { namespace, packId } = parts;
18904
- const row = this.db.prepare(
18905
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19330
+ const row = getRow(
19331
+ this.db.prepare(
19332
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18906
19333
  rules_json AS rulesJson, updated_at AS updatedAt
18907
19334
  FROM installed_packs
18908
19335
  WHERE namespace = ? AND pack_id = ?`
18909
- ).get(namespace, packId);
19336
+ ),
19337
+ [namespace, packId]
19338
+ );
18910
19339
  if (!row) return Promise.resolve(null);
18911
19340
  const rules = parseRules(row.rulesJson);
18912
19341
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18924,7 +19353,7 @@ var SqliteDetectionsRepository = class {
18924
19353
  packId: row.packId,
18925
19354
  version: row.version,
18926
19355
  name: row.name,
18927
- enabled: row.enabled === 1,
19356
+ enabled: intToBool(row.enabled),
18928
19357
  rules,
18929
19358
  updatedAt: new Date(row.updatedAt),
18930
19359
  policyId: row.policyId
@@ -18939,13 +19368,14 @@ var SqliteDetectionsRepository = class {
18939
19368
  countFindingsLast30d(ruleIds) {
18940
19369
  if (ruleIds.length === 0) return 0;
18941
19370
  const since = this.now() - 30 * DAY_MS2;
18942
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18943
- const row = this.db.prepare(
18944
- `SELECT count(*) AS c
19371
+ const inClause = placeholders(ruleIds.length);
19372
+ return countScalar(
19373
+ this.db,
19374
+ `SELECT count(*) AS n
18945
19375
  FROM findings f JOIN events e ON e.id = f.event_id
18946
- WHERE e.occurred_at >= ? AND f.rule_id IN (${placeholders2})`
18947
- ).get(since, ...ruleIds);
18948
- return row.c;
19376
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19377
+ [since, ...ruleIds]
19378
+ );
18949
19379
  }
18950
19380
  };
18951
19381
 
@@ -18962,16 +19392,17 @@ var SqliteEventsRepository = class {
18962
19392
  insertStmt;
18963
19393
  insertEvent(event) {
18964
19394
  const row = toEventRow(event);
18965
- this.insertStmt.run({
18966
- id: row.id,
18967
- sourceTool: row.sourceTool,
18968
- kind: row.kind,
18969
- occurredAt: row.occurredAt,
18970
- contentHash: row.contentHash,
18971
- content: row.content,
18972
- // exactOptionalPropertyTypes: the column is nullable, never undefined.
18973
- metadata: row.metadata ?? null
18974
- });
19395
+ this.insertStmt.run(
19396
+ bindParams({
19397
+ id: row.id,
19398
+ sourceTool: row.sourceTool,
19399
+ kind: row.kind,
19400
+ occurredAt: row.occurredAt,
19401
+ contentHash: row.contentHash,
19402
+ content: row.content,
19403
+ metadata: row.metadata
19404
+ })
19405
+ );
18975
19406
  }
18976
19407
  // Every recorded event's content hash — the historical backfill loads this once
18977
19408
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18979,13 +19410,23 @@ var SqliteEventsRepository = class {
18979
19410
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18980
19411
  // async EventsReadPort contract.
18981
19412
  contentHashes() {
18982
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19413
+ const rows = allRows(
19414
+ this.db.prepare("SELECT content_hash FROM events")
19415
+ );
18983
19416
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18984
19417
  }
18985
19418
  };
18986
19419
 
18987
19420
  // ../../packages/persistence/src/repositories/exceptions.ts
18988
19421
  import { randomUUID } from "crypto";
19422
+
19423
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19424
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19425
+ function isUniqueConstraintError(err) {
19426
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19427
+ }
19428
+
19429
+ // ../../packages/persistence/src/repositories/exceptions.ts
18989
19430
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18990
19431
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18991
19432
  var DuplicateActiveExceptionError = class extends Error {
@@ -19006,10 +19447,6 @@ var AmbiguousExceptionIdError = class extends Error {
19006
19447
  this.name = "AmbiguousExceptionIdError";
19007
19448
  }
19008
19449
  };
19009
- var SQLITE_CONSTRAINT_UNIQUE = 2067;
19010
- function isUniqueConstraintError(err) {
19011
- return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19012
- }
19013
19450
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19014
19451
  AND (expires_at IS NULL OR expires_at > :now)
19015
19452
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -19065,10 +19502,11 @@ var SqliteExceptionsRepository = class {
19065
19502
  this.insertExceptionRow(id, input, now);
19066
19503
  } catch (err) {
19067
19504
  if (!isUniqueConstraintError(err)) throw err;
19068
- this.db.exec("BEGIN IMMEDIATE");
19069
- try {
19070
- const superseded = this.db.prepare(
19071
- `UPDATE exceptions
19505
+ withTransaction(
19506
+ this.db,
19507
+ () => {
19508
+ const superseded = this.db.prepare(
19509
+ `UPDATE exceptions
19072
19510
  SET revoked_at = :now, revoked_by = :revokedBy,
19073
19511
  revoke_reason = 'superseded by a new grant for the same value',
19074
19512
  updated_at = :now
@@ -19076,24 +19514,27 @@ var SqliteExceptionsRepository = class {
19076
19514
  AND key_version = :keyVersion AND revoked_at IS NULL
19077
19515
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
19078
19516
  OR (max_uses IS NOT NULL AND use_count >= max_uses))`
19079
- ).run({
19080
- now,
19081
- revokedBy: input.createdBy,
19082
- ruleId: input.ruleId,
19083
- valueFingerprint: input.valueFingerprint,
19084
- keyVersion: input.keyVersion
19085
- });
19086
- if (Number(superseded.changes) !== 1) {
19087
- throw new DuplicateActiveExceptionError(input.ruleId);
19088
- }
19089
- this.insertExceptionRow(id, input, now);
19090
- this.db.exec("COMMIT");
19091
- } catch (retryErr) {
19092
- this.db.exec("ROLLBACK");
19093
- throw retryErr;
19094
- }
19517
+ ).run({
19518
+ now,
19519
+ revokedBy: input.createdBy,
19520
+ ruleId: input.ruleId,
19521
+ valueFingerprint: input.valueFingerprint,
19522
+ keyVersion: input.keyVersion
19523
+ });
19524
+ if (Number(superseded.changes) !== 1) {
19525
+ throw new DuplicateActiveExceptionError(input.ruleId);
19526
+ }
19527
+ this.insertExceptionRow(id, input, now);
19528
+ },
19529
+ "IMMEDIATE"
19530
+ );
19531
+ }
19532
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19533
+ id
19534
+ });
19535
+ if (row === void 0) {
19536
+ throw new Error("exception row not found immediately after insert");
19095
19537
  }
19096
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
19097
19538
  return parseExceptionRow(row);
19098
19539
  }
19099
19540
  insertExceptionRow(id, input, now) {
@@ -19131,14 +19572,11 @@ var SqliteExceptionsRepository = class {
19131
19572
  */
19132
19573
  list(opts) {
19133
19574
  const where = opts?.includeTerminal ? "" : `WHERE ${ACTIVE_PREDICATE}`;
19134
- const rows = this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`).all(opts?.includeTerminal ? {} : { now: Date.now() });
19135
- const exceptions = [];
19136
- for (const row of rows) {
19137
- try {
19138
- exceptions.push(parseExceptionRow(row));
19139
- } catch {
19140
- }
19141
- }
19575
+ const rows = allRows(
19576
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19577
+ opts?.includeTerminal ? {} : { now: Date.now() }
19578
+ );
19579
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19142
19580
  return Promise.resolve(exceptions);
19143
19581
  }
19144
19582
  /**
@@ -19148,7 +19586,12 @@ var SqliteExceptionsRepository = class {
19148
19586
  */
19149
19587
  getByIdPrefix(prefix) {
19150
19588
  if (prefix.length === 0) return Promise.resolve(void 0);
19151
- const rows = this.db.prepare(String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`).all({ pattern: `${escapeLikePattern(prefix)}%` });
19589
+ const rows = allRows(
19590
+ this.db.prepare(
19591
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19592
+ ),
19593
+ { pattern: `${escapeLikePattern(prefix)}%` }
19594
+ );
19152
19595
  if (rows.length > 1) {
19153
19596
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19154
19597
  }
@@ -19190,30 +19633,27 @@ var SqliteExceptionsRepository = class {
19190
19633
  * a different (rotated-away) key never match, so they are excluded at read.
19191
19634
  */
19192
19635
  activeBundleEntries(keyVersion, now = Date.now()) {
19193
- const rows = this.db.prepare(
19194
- `SELECT * FROM exceptions
19636
+ const rows = allRows(
19637
+ this.db.prepare(
19638
+ `SELECT * FROM exceptions
19195
19639
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19196
19640
  ORDER BY created_at DESC, rowid DESC`
19197
- ).all({ keyVersion, now });
19198
- const entries = [];
19199
- for (const row of rows) {
19200
- try {
19201
- const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19202
- entries.push(
19203
- ExceptionBundleEntry.parse({
19204
- id: row.id,
19205
- ruleId: row.rule_id,
19206
- valueFingerprint: row.value_fingerprint,
19207
- keyVersion: row.key_version,
19208
- expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19209
- maxUses: row.max_uses,
19210
- useCount: row.use_count,
19211
- conditions
19212
- })
19213
- );
19214
- } catch {
19215
- }
19216
- }
19641
+ ),
19642
+ { keyVersion, now }
19643
+ );
19644
+ const entries = mapRowsTolerant(rows, (row) => {
19645
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19646
+ return ExceptionBundleEntry.parse({
19647
+ id: row.id,
19648
+ ruleId: row.rule_id,
19649
+ valueFingerprint: row.value_fingerprint,
19650
+ keyVersion: row.key_version,
19651
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19652
+ maxUses: row.max_uses,
19653
+ useCount: row.use_count,
19654
+ conditions
19655
+ });
19656
+ });
19217
19657
  return Promise.resolve(entries);
19218
19658
  }
19219
19659
  /**
@@ -19240,11 +19680,14 @@ var SqliteExceptionsRepository = class {
19240
19680
  }
19241
19681
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19242
19682
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19243
- const rows = this.db.prepare(
19244
- `SELECT * FROM blocked_detections
19683
+ const rows = allRows(
19684
+ this.db.prepare(
19685
+ `SELECT * FROM blocked_detections
19245
19686
  WHERE blocked_at > :cutoff
19246
19687
  ORDER BY blocked_at DESC, rowid DESC`
19247
- ).all({ cutoff: Date.now() - windowMs });
19688
+ ),
19689
+ { cutoff: Date.now() - windowMs }
19690
+ );
19248
19691
  return Promise.resolve(
19249
19692
  rows.map((row) => ({
19250
19693
  reference: row.reference,
@@ -19324,7 +19767,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19324
19767
  )`;
19325
19768
 
19326
19769
  // ../../packages/persistence/src/repositories/findings.ts
19327
- var GROUP_ROW_CAP = 2e3;
19770
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19771
+ var CONCAT_SEP = ",";
19772
+ var TUPLE_SEP = "|";
19773
+ function splitConcat(value) {
19774
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19775
+ }
19328
19776
  function deriveInstanceStatus(row) {
19329
19777
  return deriveFindingStatus({
19330
19778
  kind: row.kind,
@@ -19392,13 +19840,16 @@ var SqliteFindingsRepository = class {
19392
19840
  }
19393
19841
  recentFindings(opts) {
19394
19842
  const limit = opts?.limit ?? 50;
19395
- const rows = this.db.prepare(
19396
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19843
+ const rows = allRows(
19844
+ this.db.prepare(
19845
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19397
19846
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19398
19847
  FROM findings f JOIN events e ON e.id = f.event_id
19399
19848
  ORDER BY e.occurred_at DESC, f.rowid DESC
19400
19849
  LIMIT :limit`
19401
- ).all({ limit });
19850
+ ),
19851
+ { limit }
19852
+ );
19402
19853
  return Promise.resolve(
19403
19854
  rows.map((r) => ({
19404
19855
  id: r.id,
@@ -19415,26 +19866,94 @@ var SqliteFindingsRepository = class {
19415
19866
  }))
19416
19867
  );
19417
19868
  }
19869
+ /** Live-enforced findings recorded for one session — a bare COUNT over the
19870
+ * session-stamped events (served by idx_events_session_id), so the Activity
19871
+ * page can label its findings link without the grouped pipeline. */
19872
+ sessionFindingsCount(sessionId) {
19873
+ if (!sessionId) return Promise.resolve(0);
19874
+ return Promise.resolve(
19875
+ countScalar(
19876
+ this.db,
19877
+ `SELECT count(*) AS n FROM findings f
19878
+ JOIN events e ON e.id = f.event_id
19879
+ WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
19880
+ { sessionId }
19881
+ )
19882
+ );
19883
+ }
19884
+ /** Per-rule transcript firing tally for one session — reads the OTHER finding
19885
+ * store (inspection_findings, keyed to audit_events): every detection the
19886
+ * transcript pass recorded, counted per firing rather than per unique value.
19887
+ * Rides on session-scoped grouped responses so the findings view can
19888
+ * reconcile the Activity page's tally with the deduped groups it lists. */
19889
+ sessionFirings(sessionId) {
19890
+ return Object.fromEntries(
19891
+ countBy(
19892
+ this.db,
19893
+ `SELECT d.rule_id AS k, count(*) AS n
19894
+ FROM inspection_findings f
19895
+ JOIN audit_events e ON e.id = f.audit_event_id
19896
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19897
+ WHERE e.root_session_id = :sessionId
19898
+ GROUP BY d.rule_id`,
19899
+ { sessionId }
19900
+ )
19901
+ );
19902
+ }
19418
19903
  /**
19419
- * Grouped findings for the dashboard — joins findings⋈events (repo/file
19420
- * from event metadata), groups by ruleId, computes per-filter-excluded facets,
19904
+ * Grouped findings for the dashboard — joins findings⋈events (repo/file/
19905
+ * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19421
19906
  * applies the requested filters, and sorts by severity then recency. Filtering
19422
19907
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19423
19908
  * reflect the full filtered set; `items` is the requested
19424
- * page (default 100); no cursor (nextCursor is always null).
19909
+ * page (default 50); no cursor (nextCursor is always null).
19910
+ *
19911
+ * Two reads, neither of which materializes a row per finding:
19912
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19913
+ * the group and the filters need (count, providers, actions, statuses,
19914
+ * latest, search text);
19915
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19916
+ * populate `instances` for the table's expanded rows.
19917
+ * The aggregates carry raw DB values and are translated by the same
19918
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19919
+ * rule is ever restated in SQL.
19425
19920
  */
19426
19921
  listGroupedFindings(query) {
19427
- const rows = this.db.prepare(
19428
- `SELECT f.id, f.rule_id, f.category, f.severity, f.masked_match,
19429
- f.action_taken, f.confidence, e.occurred_at, e.source_tool,
19430
- json_extract(e.metadata, '$.repo') AS repo,
19431
- json_extract(e.metadata, '$.filePath') AS file,
19432
- e.kind AS kind, f.finding_key AS finding_key,
19433
- ${latestResolutionStatusSql("f")} AS latest_status
19434
- FROM findings f JOIN events e ON e.id = f.event_id
19435
- ORDER BY e.occurred_at DESC, f.id DESC
19436
- LIMIT :cap`
19437
- ).all({ cap: GROUP_ROW_CAP });
19922
+ const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
19923
+ const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
19924
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
19925
+ predicate: sessionPredicate,
19926
+ params: sessionParams
19927
+ });
19928
+ const rows = allRows(
19929
+ this.db.prepare(
19930
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19931
+ occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
19932
+ FROM (
19933
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19934
+ f.severity AS severity, f.masked_match AS masked_match,
19935
+ f.action_taken AS action_taken, f.confidence AS confidence,
19936
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19937
+ json_extract(e.metadata, '$.repo') AS repo,
19938
+ json_extract(e.metadata, '$.filePath') AS file,
19939
+ json_extract(e.metadata, '$.toolName') AS tool_name,
19940
+ e.kind AS kind, f.finding_key AS finding_key,
19941
+ latest.status AS latest_status,
19942
+ ROW_NUMBER() OVER (
19943
+ PARTITION BY f.rule_id
19944
+ ORDER BY e.occurred_at DESC, f.id DESC
19945
+ ) AS rn
19946
+ FROM findings f
19947
+ JOIN events e ON e.id = f.event_id
19948
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19949
+ ON latest.finding_key = f.finding_key
19950
+ ${sessionPredicate}
19951
+ )
19952
+ WHERE rn <= :cap
19953
+ ORDER BY occurred_at DESC, id DESC`
19954
+ ),
19955
+ { cap: PREVIEW_INSTANCES_PER_GROUP, ...sessionParams }
19956
+ );
19438
19957
  const groupable = rows.map((r) => ({
19439
19958
  id: r.id,
19440
19959
  ruleId: r.rule_id,
@@ -19447,9 +19966,10 @@ var SqliteFindingsRepository = class {
19447
19966
  sourceTool: r.source_tool,
19448
19967
  repo: r.repo ?? "",
19449
19968
  file: r.file ?? "",
19969
+ ...r.tool_name === null ? {} : { toolName: r.tool_name },
19450
19970
  status: deriveInstanceStatus(r)
19451
19971
  }));
19452
- const allGroups = buildFindingGroups(groupable);
19972
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19453
19973
  const filterOpts = {
19454
19974
  severity: query.severity,
19455
19975
  providers: query.provider,
@@ -19465,44 +19985,134 @@ var SqliteFindingsRepository = class {
19465
19985
  };
19466
19986
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
19467
19987
  const items = sorted.slice(0, limit);
19468
- return Promise.resolve({ totals, facets, items, nextCursor: null });
19988
+ return Promise.resolve({
19989
+ totals,
19990
+ facets,
19991
+ items,
19992
+ nextCursor: null,
19993
+ ...query.sessionId ? { sessionFirings: this.sessionFirings(query.sessionId) } : {}
19994
+ });
19995
+ }
19996
+ /**
19997
+ * One row per rule_id, folding EVERY instance of the group into the values
19998
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19999
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
20000
+ *
20001
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
20002
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
20003
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
20004
+ * status keeps the classifier itself in @akasecurity/schema, where
20005
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
20006
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
20007
+ * a group's row stays small however many findings it holds.
20008
+ *
20009
+ * `withSearchText` is the exception, and the one column here that does NOT
20010
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
20011
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
20012
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
20013
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
20014
+ * match an instance outside the preview, which searching the preview alone
20015
+ * would silently lose, so it is fetched only when the request actually
20016
+ * carries a `q`.
20017
+ */
20018
+ groupAggregates(withSearchText, scope) {
20019
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
20020
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
20021
+ group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
20022
+ const rows = this.db.prepare(
20023
+ `SELECT f.rule_id AS rule_id,
20024
+ count(*) AS instance_count,
20025
+ max(e.occurred_at) AS latest_at,
20026
+ group_concat(DISTINCT e.source_tool) AS source_tools,
20027
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
20028
+ group_concat(DISTINCT (
20029
+ e.kind || '${TUPLE_SEP}' ||
20030
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
20031
+ coalesce(latest.status, '')
20032
+ )) AS status_inputs
20033
+ ${searchTextColumns}
20034
+ FROM findings f
20035
+ JOIN events e ON e.id = f.event_id
20036
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20037
+ ON latest.finding_key = f.finding_key
20038
+ ${scope.predicate}
20039
+ GROUP BY f.rule_id`
20040
+ ).all(scope.params);
20041
+ return new Map(
20042
+ rows.map((r) => [
20043
+ r.rule_id,
20044
+ {
20045
+ instanceCount: r.instance_count,
20046
+ sourceTools: splitConcat(r.source_tools),
20047
+ actionsTaken: splitConcat(r.actions_taken),
20048
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
20049
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
20050
+ return {
20051
+ // deriveFindingStatus only distinguishes null from non-null here,
20052
+ // so the marker stands in for the key itself (never rendered).
20053
+ kind,
20054
+ findingKey: keyMarker === "" ? null : keyMarker,
20055
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
20056
+ };
20057
+ }),
20058
+ latestDetectedAt: epochMillisToIso(r.latest_at),
20059
+ // Free text only — joined and substring-matched, so group_concat's
20060
+ // commas need no unpicking (a repo/path containing one still matches).
20061
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
20062
+ // tell "no q this request" from "a group with no repo/file at all"
20063
+ // and skip priming a haystack nothing will read.
20064
+ ...withSearchText ? {
20065
+ searchText: [r.repos ?? "", r.files ?? "", r.tool_names ?? ""].filter((s) => s !== "").join(" ")
20066
+ } : {}
20067
+ }
20068
+ ])
20069
+ );
19469
20070
  }
19470
20071
  healthSummary() {
19471
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
20072
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19472
20073
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
19473
- const grouped = this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken").all();
20074
+ const grouped = allRows(
20075
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
20076
+ );
19474
20077
  for (const row of grouped) {
19475
20078
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19476
20079
  }
19477
20080
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
19478
- const sevRows = this.db.prepare(
19479
- `SELECT f.severity AS severity, count(*) AS c
20081
+ const sevRows = allRows(
20082
+ this.db.prepare(
20083
+ `SELECT f.severity AS severity, count(*) AS c
19480
20084
  FROM findings f
19481
20085
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19482
20086
  ON latest.finding_key = f.finding_key
19483
20087
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19484
20088
  GROUP BY f.severity`
19485
- ).all();
20089
+ )
20090
+ );
19486
20091
  for (const row of sevRows) {
19487
20092
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19488
20093
  }
19489
20094
  const categories = ENFORCEABLE_CATEGORIES;
19490
- const enabledRows = this.db.prepare(
19491
- `SELECT DISTINCT json_extract(target, '$.category') AS category
20095
+ const enabledRows = allRows(
20096
+ this.db.prepare(
20097
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19492
20098
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19493
- ).all();
20099
+ )
20100
+ );
19494
20101
  const enabled = new Set(enabledRows.map((r) => r.category));
19495
20102
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19496
20103
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19497
20104
  }
19498
20105
  activityByDay(days = 7) {
19499
20106
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
19500
- const rows = this.db.prepare(
19501
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20107
+ const rows = allRows(
20108
+ this.db.prepare(
20109
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19502
20110
  FROM findings f JOIN events e ON e.id = f.event_id
19503
20111
  WHERE e.occurred_at >= :since
19504
20112
  GROUP BY day, f.action_taken`
19505
- ).all({ since });
20113
+ ),
20114
+ { since }
20115
+ );
19506
20116
  const buckets = /* @__PURE__ */ new Map();
19507
20117
  for (let i = 0; i < days; i++) {
19508
20118
  const day = isoDay(since + i * DAY_MS3);
@@ -19574,17 +20184,19 @@ var SqliteInspectionFindingsRepository = class {
19574
20184
  insertStmt;
19575
20185
  insertFinding(input) {
19576
20186
  const row = toInspectionFindingRow(input);
19577
- this.insertStmt.run({
19578
- id: row.id,
19579
- auditEventId: row.auditEventId,
19580
- inspectionDefinitionId: row.inspectionDefinitionId,
19581
- classifiedDataId: row.classifiedDataId ?? null,
19582
- spanStart: row.spanStart,
19583
- spanEnd: row.spanEnd,
19584
- maskedMatch: row.maskedMatch,
19585
- actionTaken: row.actionTaken,
19586
- confidence: row.confidence
19587
- });
20187
+ this.insertStmt.run(
20188
+ bindParams({
20189
+ id: row.id,
20190
+ auditEventId: row.auditEventId,
20191
+ inspectionDefinitionId: row.inspectionDefinitionId,
20192
+ classifiedDataId: row.classifiedDataId,
20193
+ spanStart: row.spanStart,
20194
+ spanEnd: row.spanEnd,
20195
+ maskedMatch: row.maskedMatch,
20196
+ actionTaken: row.actionTaken,
20197
+ confidence: row.confidence
20198
+ })
20199
+ );
19588
20200
  }
19589
20201
  };
19590
20202
 
@@ -19660,12 +20272,7 @@ function isMirrorDowngrade(incoming, stored) {
19660
20272
  }
19661
20273
  function ruleIdsOf(rulesJson) {
19662
20274
  const ids = /* @__PURE__ */ new Set();
19663
- let raw;
19664
- try {
19665
- raw = JSON.parse(rulesJson);
19666
- } catch {
19667
- return ids;
19668
- }
20275
+ const raw = safeJson(rulesJson, []);
19669
20276
  if (!Array.isArray(raw)) return ids;
19670
20277
  for (const entry of raw) {
19671
20278
  if (entry && typeof entry === "object") {
@@ -19738,47 +20345,48 @@ var SqliteInstalledPacksRepository = class {
19738
20345
  }));
19739
20346
  if (this.storedSignature() === inventorySignature(rows)) return;
19740
20347
  const now = Date.now();
19741
- this.db.exec("BEGIN IMMEDIATE");
19742
- try {
19743
- const mirror = this.mirrorState();
19744
- let behind = false;
19745
- for (const row of rows) {
19746
- const params = {
19747
- id: randomUUID2(),
19748
- namespace: row.namespace,
19749
- packId: row.packId,
19750
- version: row.version,
19751
- name: row.name,
19752
- rulesJson: row.rulesJson,
19753
- now
19754
- };
19755
- const stored = mirror.get(`${row.namespace}/${row.packId}`);
19756
- if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
19757
- this.upsertAvailableStmt.run({
19758
- ...params,
20348
+ withTransaction(
20349
+ this.db,
20350
+ () => {
20351
+ const mirror = this.mirrorState();
20352
+ let behind = false;
20353
+ for (const row of rows) {
20354
+ const params = {
19759
20355
  id: randomUUID2(),
19760
- recordedBy: meta3?.recordedBy ?? null
19761
- });
19762
- } else {
19763
- behind = true;
20356
+ namespace: row.namespace,
20357
+ packId: row.packId,
20358
+ version: row.version,
20359
+ name: row.name,
20360
+ rulesJson: row.rulesJson,
20361
+ now
20362
+ };
20363
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
20364
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20365
+ this.upsertAvailableStmt.run({
20366
+ ...params,
20367
+ id: randomUUID2(),
20368
+ recordedBy: meta3?.recordedBy ?? null
20369
+ });
20370
+ } else {
20371
+ behind = true;
20372
+ }
20373
+ this.insertMissingStmt.run(params);
19764
20374
  }
19765
- this.insertMissingStmt.run(params);
19766
- }
19767
- if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
19768
- this.db.exec("COMMIT");
19769
- } catch (err) {
19770
- this.db.exec("ROLLBACK");
19771
- throw err;
19772
- }
20375
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20376
+ },
20377
+ "IMMEDIATE"
20378
+ );
19773
20379
  } catch {
19774
20380
  }
19775
20381
  }
19776
20382
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19777
20383
  // input to the downgrade guard. Read INSIDE the write transaction.
19778
20384
  mirrorState() {
19779
- const rows = this.db.prepare(
19780
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19781
- ).all();
20385
+ const rows = allRows(
20386
+ this.db.prepare(
20387
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20388
+ )
20389
+ );
19782
20390
  return new Map(
19783
20391
  rows.map((r) => [
19784
20392
  `${r.namespace}/${r.packId}`,
@@ -19790,7 +20398,9 @@ var SqliteInstalledPacksRepository = class {
19790
20398
  // (keys joined with '/', matching the detection id slug encoding — packId may
19791
20399
  // itself contain '/', but namespace may not, so the join is unambiguous).
19792
20400
  pruneAvailable(keep) {
19793
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20401
+ const rows = allRows(
20402
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20403
+ );
19794
20404
  const keepSet = new Set(keep);
19795
20405
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19796
20406
  for (const r of rows) {
@@ -19817,11 +20427,13 @@ var SqliteInstalledPacksRepository = class {
19817
20427
  if (this.db.isTransaction) {
19818
20428
  throw new Error("applyUpdate must not be called inside an open transaction");
19819
20429
  }
19820
- this.db.exec("BEGIN IMMEDIATE");
19821
- try {
19822
- this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
19823
- const res = this.db.prepare(
19824
- `UPDATE installed_packs SET
20430
+ let changed = false;
20431
+ withTransaction(
20432
+ this.db,
20433
+ () => {
20434
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20435
+ const res = this.db.prepare(
20436
+ `UPDATE installed_packs SET
19825
20437
  version = (SELECT a.version FROM available_packs a
19826
20438
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19827
20439
  name = (SELECT a.name FROM available_packs a
@@ -19832,17 +20444,13 @@ var SqliteInstalledPacksRepository = class {
19832
20444
  WHERE namespace = :namespace AND pack_id = :packId
19833
20445
  AND EXISTS (SELECT 1 FROM available_packs a
19834
20446
  WHERE a.namespace = :namespace AND a.pack_id = :packId)`
19835
- ).run({ namespace, packId, now: Date.now() });
19836
- this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
19837
- this.db.exec("COMMIT");
19838
- return Number(res.changes) > 0;
19839
- } catch (err) {
19840
- try {
19841
- this.db.exec("ROLLBACK");
19842
- } catch {
19843
- }
19844
- throw err;
19845
- }
20447
+ ).run({ namespace, packId, now: Date.now() });
20448
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20449
+ changed = Number(res.changes) > 0;
20450
+ },
20451
+ "IMMEDIATE"
20452
+ );
20453
+ return changed;
19846
20454
  }
19847
20455
  /**
19848
20456
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19856,9 +20464,11 @@ var SqliteInstalledPacksRepository = class {
19856
20464
  * JSON-level failure therefore counts as invalid.
19857
20465
  */
19858
20466
  installedRuleset() {
19859
- const rows = this.db.prepare(
19860
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19861
- ).all();
20467
+ const rows = allRows(
20468
+ this.db.prepare(
20469
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20470
+ )
20471
+ );
19862
20472
  const out = {
19863
20473
  installedPacks: rows.length,
19864
20474
  enabledPacks: 0,
@@ -19867,7 +20477,7 @@ var SqliteInstalledPacksRepository = class {
19867
20477
  ruleActions: /* @__PURE__ */ new Map()
19868
20478
  };
19869
20479
  for (const row of rows) {
19870
- if (row.enabled !== 1) continue;
20480
+ if (!intToBool(row.enabled)) continue;
19871
20481
  out.enabledPacks += 1;
19872
20482
  const action = policyIdToAction(row.policyId);
19873
20483
  let raw;
@@ -19902,9 +20512,11 @@ var SqliteInstalledPacksRepository = class {
19902
20512
  * running max would mask a genuinely-newer parseable stamp.
19903
20513
  */
19904
20514
  newestRecordedBinary() {
19905
- const rows = this.db.prepare(
19906
- `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
19907
- ).all();
20515
+ const rows = allRows(
20516
+ this.db.prepare(
20517
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20518
+ )
20519
+ );
19908
20520
  let newest = null;
19909
20521
  for (const row of rows) {
19910
20522
  const at = row.recordedBy.lastIndexOf("@");
@@ -19919,13 +20531,15 @@ var SqliteInstalledPacksRepository = class {
19919
20531
  return newest;
19920
20532
  }
19921
20533
  counts() {
19922
- const row = this.db.prepare(
19923
- `SELECT count(*) AS packs,
20534
+ const row = getRow(
20535
+ this.db.prepare(
20536
+ `SELECT count(*) AS packs,
19924
20537
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19925
20538
  coalesce(sum(enabled), 0) AS enabled
19926
20539
  FROM installed_packs`
19927
- ).get();
19928
- return Promise.resolve(row);
20540
+ )
20541
+ );
20542
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19929
20543
  }
19930
20544
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19931
20545
  // Back the Policies page's built-in catalog: how many
@@ -19938,26 +20552,29 @@ var SqliteInstalledPacksRepository = class {
19938
20552
  * attributed to Monitor, matching the Detections views.
19939
20553
  */
19940
20554
  countsByPolicyId() {
19941
- const rows = this.db.prepare(
19942
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20555
+ return countBy(
20556
+ this.db,
20557
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19943
20558
  FROM installed_packs
19944
- GROUP BY pid`
19945
- ).all();
19946
- return new Map(rows.map((r) => [r.pid, r.n]));
20559
+ GROUP BY k`
20560
+ );
19947
20561
  }
19948
20562
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19949
20563
  listByPolicyId(policyId) {
19950
- const rows = this.db.prepare(
19951
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20564
+ const rows = allRows(
20565
+ this.db.prepare(
20566
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19952
20567
  FROM installed_packs
19953
20568
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19954
20569
  ORDER BY name ASC`
19955
- ).all(policyId);
20570
+ ),
20571
+ [policyId]
20572
+ );
19956
20573
  return rows.map((r) => ({
19957
20574
  id: `${r.namespace}/${r.packId}`,
19958
20575
  name: r.name,
19959
20576
  ruleCount: parseRules(r.rulesJson).length,
19960
- enabled: r.enabled === 1
20577
+ enabled: intToBool(r.enabled)
19961
20578
  }));
19962
20579
  }
19963
20580
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19986,14 +20603,14 @@ var SqliteInstalledPacksRepository = class {
19986
20603
  const res = this.db.prepare(
19987
20604
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19988
20605
  WHERE namespace = :namespace AND pack_id = :packId`
19989
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20606
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19990
20607
  return Number(res.changes) > 0;
19991
20608
  }
19992
20609
  // Fingerprint of the recorded available mirror — compared against the
19993
20610
  // incoming inventory's signature to skip the write entirely when the running
19994
20611
  // binary's inventory hasn't changed since the last record.
19995
20612
  storedSignature() {
19996
- const rows = this.signatureStmt.all();
20613
+ const rows = allRows(this.signatureStmt);
19997
20614
  return inventorySignature(rows);
19998
20615
  }
19999
20616
  };
@@ -20021,42 +20638,48 @@ var SqliteInventoryRepository = class {
20021
20638
  upsert(input, now = Date.now()) {
20022
20639
  const id = inventoryId(input.objectType, input.identityKey);
20023
20640
  const row = toInventoryRow(input, id, now);
20024
- this.upsertStmt.run({
20025
- id: row.id,
20026
- objectType: row.objectType,
20027
- location: row.location ?? null,
20028
- title: row.title ?? null,
20029
- hostId: row.hostId ?? null,
20030
- attributes: row.attributes,
20031
- firstSeen: row.firstSeen,
20032
- lastSeen: row.lastSeen
20033
- });
20641
+ this.upsertStmt.run(
20642
+ bindParams({
20643
+ id: row.id,
20644
+ objectType: row.objectType,
20645
+ location: row.location,
20646
+ title: row.title,
20647
+ hostId: row.hostId,
20648
+ attributes: row.attributes,
20649
+ firstSeen: row.firstSeen,
20650
+ lastSeen: row.lastSeen
20651
+ })
20652
+ );
20034
20653
  return id;
20035
20654
  }
20036
20655
  // The full row, for round-trip assertions.
20037
20656
  findById(id) {
20038
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
20039
- return row;
20657
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
20040
20658
  }
20041
20659
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
20042
20660
  // served from the object_type index, never from audit_events.
20043
20661
  distinctTitles(objectType) {
20044
- const rows = this.db.prepare(
20045
- `SELECT DISTINCT title FROM inventory
20662
+ const rows = allRows(
20663
+ this.db.prepare(
20664
+ `SELECT DISTINCT title FROM inventory
20046
20665
  WHERE object_type = :objectType AND title IS NOT NULL
20047
20666
  ORDER BY title`
20048
- ).all({ objectType });
20667
+ ),
20668
+ { objectType }
20669
+ );
20049
20670
  return rows.map((r) => r.title);
20050
20671
  }
20051
20672
  // Distinct host os_version values — a facet served from an inventory index
20052
20673
  // over the generated column, never from the audit fact (confirm via EXPLAIN
20053
20674
  // QUERY PLAN).
20054
20675
  osVersions() {
20055
- const rows = this.db.prepare(
20056
- `SELECT DISTINCT os_version AS value FROM inventory
20676
+ const rows = allRows(
20677
+ this.db.prepare(
20678
+ `SELECT DISTINCT os_version AS value FROM inventory
20057
20679
  WHERE object_type = 'host' AND os_version IS NOT NULL
20058
20680
  ORDER BY value`
20059
- ).all();
20681
+ )
20682
+ );
20060
20683
  return rows.map((r) => r.value);
20061
20684
  }
20062
20685
  };
@@ -20076,14 +20699,6 @@ var EMPTY_PROJECT_AGG = {
20076
20699
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
20077
20700
  findingsCount: 0
20078
20701
  };
20079
- function safeJson(s, fallback) {
20080
- if (s == null) return fallback;
20081
- try {
20082
- return JSON.parse(s);
20083
- } catch {
20084
- return fallback;
20085
- }
20086
- }
20087
20702
  function resolveHarnessId(attrs, row) {
20088
20703
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
20089
20704
  return attrs.provider;
@@ -20279,30 +20894,49 @@ var SqliteInventoryAssetsRepository = class {
20279
20894
  configRowsCache;
20280
20895
  // ─── stats ─────────────────────────────────────────────────────────────────
20281
20896
  getInventoryStats() {
20282
- const byType = { project: 0, skill: 0, mcp: 0, hook: 0, config: 0 };
20283
- for (const r of this.db.prepare("SELECT asset_type AS t, count(*) AS n FROM inventory_asset GROUP BY asset_type").all()) {
20284
- if (r.t in byType) byType[r.t] = r.n;
20285
- }
20286
- byType.project = this.db.prepare(`SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`).get().n;
20287
- const mcpTrust = { "known-good": 0, risky: 0, unapproved: 0 };
20288
- for (const r of this.db.prepare(
20289
- `SELECT coalesce(o.trust, a.trust) AS trust, count(*) AS n
20897
+ const typeCounts = countBy(
20898
+ this.db,
20899
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20900
+ );
20901
+ const byType = {
20902
+ project: 0,
20903
+ skill: typeCounts.get("skill") ?? 0,
20904
+ mcp: typeCounts.get("mcp") ?? 0,
20905
+ hook: typeCounts.get("hook") ?? 0,
20906
+ config: typeCounts.get("config") ?? 0
20907
+ };
20908
+ byType.project = countScalar(
20909
+ this.db,
20910
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20911
+ );
20912
+ const mcpTrustCounts = countBy(
20913
+ this.db,
20914
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20290
20915
  FROM inventory_asset a
20291
20916
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20292
20917
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20293
20918
  GROUP BY coalesce(o.trust, a.trust)`
20294
- ).all()) {
20295
- if (r.trust in mcpTrust) mcpTrust[r.trust] = r.n;
20296
- }
20297
- const harnesses = this.db.prepare(
20919
+ );
20920
+ const mcpTrust = {
20921
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20922
+ risky: mcpTrustCounts.get("risky") ?? 0,
20923
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20924
+ };
20925
+ const harnesses = countScalar(
20926
+ this.db,
20298
20927
  `SELECT count(*) AS n FROM inventory
20299
20928
  WHERE object_type = 'harness'
20300
- AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20301
- ).get({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }).n;
20302
- const flaggedAssets = this.db.prepare("SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'").get().n;
20303
- const flaggedProjects = this.db.prepare(
20929
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20930
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20931
+ );
20932
+ const flaggedAssets = countScalar(
20933
+ this.db,
20934
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20935
+ );
20936
+ const flaggedProjects = countScalar(
20937
+ this.db,
20304
20938
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20305
- ).get().n;
20939
+ );
20306
20940
  const configRows = this.configAssetRows();
20307
20941
  for (const r of configRows) {
20308
20942
  byType[r.assetType] += 1;
@@ -20559,12 +21193,15 @@ var SqliteInventoryAssetsRepository = class {
20559
21193
  }
20560
21194
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20561
21195
  fetchHarnessRows() {
20562
- return this.db.prepare(
20563
- `SELECT id, title, attributes, harness_version AS harnessVersion
21196
+ return allRows(
21197
+ this.db.prepare(
21198
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20564
21199
  FROM inventory
20565
21200
  WHERE object_type = 'harness'
20566
21201
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20567
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
21202
+ ),
21203
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
21204
+ );
20568
21205
  }
20569
21206
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20570
21207
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20574,12 +21211,13 @@ var SqliteInventoryAssetsRepository = class {
20574
21211
  const params = [...harnessInvIds];
20575
21212
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20576
21213
  if (q) {
20577
- const pat = `%${escapeLikePattern(q)}%`;
20578
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
21214
+ const pat = containsPattern(q);
21215
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20579
21216
  params.push(pat, pat);
20580
21217
  }
20581
- const rows = this.db.prepare(
20582
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
21218
+ const rows = allRows(
21219
+ this.db.prepare(
21220
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20583
21221
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20584
21222
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20585
21223
  FROM harness_asset ha
@@ -20587,7 +21225,9 @@ var SqliteInventoryAssetsRepository = class {
20587
21225
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20588
21226
  WHERE ${where}
20589
21227
  ORDER BY a.name ASC`
20590
- ).all(...params);
21228
+ ),
21229
+ params
21230
+ );
20591
21231
  for (const raw of rows) {
20592
21232
  const harnessInvId = raw.harnessInvId;
20593
21233
  const [asset] = this.mapAssetRows([raw]);
@@ -20606,21 +21246,24 @@ var SqliteInventoryAssetsRepository = class {
20606
21246
  params.push(...types);
20607
21247
  }
20608
21248
  if (q) {
20609
- const pat = `%${escapeLikePattern(q)}%`;
20610
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
21249
+ const pat = containsPattern(q);
21250
+ conditions.push(likeAny(["a.name", "a.sub"]));
20611
21251
  params.push(pat, pat);
20612
21252
  }
20613
21253
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20614
21254
  const sampleRows = this.mapAssetRows(
20615
- this.db.prepare(
20616
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21255
+ allRows(
21256
+ this.db.prepare(
21257
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20617
21258
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20618
21259
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20619
21260
  FROM inventory_asset a
20620
21261
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20621
21262
  ${where}
20622
21263
  ORDER BY a.name ASC`
20623
- ).all(...params)
21264
+ ),
21265
+ params
21266
+ )
20624
21267
  );
20625
21268
  const configRows = this.configAssetRows(q).filter(
20626
21269
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20629,14 +21272,17 @@ var SqliteInventoryAssetsRepository = class {
20629
21272
  }
20630
21273
  fetchAssetById(assetId) {
20631
21274
  const rows = this.mapAssetRows(
20632
- this.db.prepare(
20633
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21275
+ allRows(
21276
+ this.db.prepare(
21277
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20634
21278
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20635
21279
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20636
21280
  FROM inventory_asset a
20637
21281
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20638
21282
  WHERE a.id = ?`
20639
- ).all(assetId)
21283
+ ),
21284
+ [assetId]
21285
+ )
20640
21286
  );
20641
21287
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20642
21288
  }
@@ -20692,37 +21338,39 @@ var SqliteInventoryAssetsRepository = class {
20692
21338
  return rows;
20693
21339
  }
20694
21340
  latestConfigScanId() {
20695
- const row = this.db.prepare(
20696
- `SELECT id FROM audit_events WHERE event_type = 'config_scan'
20697
- ORDER BY started_at DESC, id DESC LIMIT 1`
20698
- ).get();
20699
- return row?.id ?? null;
21341
+ return latestConfigScan(this.db)?.id ?? null;
20700
21342
  }
20701
21343
  fetchProjects(q) {
20702
21344
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20703
21345
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20704
21346
  const params = [];
20705
21347
  if (q) {
20706
- const pat = `%${escapeLikePattern(q)}%`;
20707
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
21348
+ const pat = containsPattern(q);
21349
+ sql += ` AND ${likeAny(["name", "url"])}`;
20708
21350
  params.push(pat, pat);
20709
21351
  }
20710
21352
  sql += " ORDER BY name ASC";
20711
- return this.db.prepare(sql).all(...params);
21353
+ return allRows(this.db.prepare(sql), params);
20712
21354
  }
20713
21355
  fetchProjectById(projectId) {
20714
- return this.db.prepare(
20715
- "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
20716
- ).get(projectId) ?? null;
21356
+ return getRow(
21357
+ this.db.prepare(
21358
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21359
+ ),
21360
+ [projectId]
21361
+ ) ?? null;
20717
21362
  }
20718
21363
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20719
21364
  fetchProjectsByIds(projectIds) {
20720
21365
  const map2 = /* @__PURE__ */ new Map();
20721
21366
  if (projectIds.length === 0) return map2;
20722
- const rows = this.db.prepare(
20723
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21367
+ const rows = allRows(
21368
+ this.db.prepare(
21369
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20724
21370
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20725
- ).all(...projectIds);
21371
+ ),
21372
+ projectIds
21373
+ );
20726
21374
  for (const r of rows) map2.set(r.id, r);
20727
21375
  return map2;
20728
21376
  }
@@ -20733,8 +21381,9 @@ var SqliteInventoryAssetsRepository = class {
20733
21381
  projectAggregates(projectIds) {
20734
21382
  const map2 = /* @__PURE__ */ new Map();
20735
21383
  if (projectIds.length === 0) return map2;
20736
- const rows = this.db.prepare(
20737
- `SELECT f.project_id AS projectId,
21384
+ const rows = allRows(
21385
+ this.db.prepare(
21386
+ `SELECT f.project_id AS projectId,
20738
21387
  coalesce(o.access, f.default_access) AS eff,
20739
21388
  count(*) AS n,
20740
21389
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20742,7 +21391,9 @@ var SqliteInventoryAssetsRepository = class {
20742
21391
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20743
21392
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20744
21393
  GROUP BY f.project_id, eff`
20745
- ).all(...projectIds);
21394
+ ),
21395
+ projectIds
21396
+ );
20746
21397
  for (const r of rows) {
20747
21398
  let agg = map2.get(r.projectId);
20748
21399
  if (!agg) {
@@ -20780,37 +21431,52 @@ var SqliteInventoryAssetsRepository = class {
20780
21431
  fetchProjectFilesUnder(projectId, prefix) {
20781
21432
  if (prefix === "") {
20782
21433
  return this.mapFileRows(
20783
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21434
+ allRows(
21435
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21436
+ [projectId]
21437
+ )
20784
21438
  );
20785
21439
  }
20786
21440
  return this.mapFileRows(
20787
- this.db.prepare(
20788
- this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
20789
- ).all(projectId, `${escapeLikePattern(prefix)}/%`)
21441
+ allRows(
21442
+ this.db.prepare(
21443
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21444
+ ),
21445
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21446
+ )
20790
21447
  );
20791
21448
  }
20792
21449
  fetchProjectFilesSearch(projectId, q) {
20793
- const pat = `%${escapeLikePattern(q)}%`;
21450
+ const pat = containsPattern(q);
20794
21451
  return this.mapFileRows(
20795
- this.db.prepare(
20796
- this.fileSelect(
20797
- "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
20798
- )
20799
- ).all(projectId, pat, pat)
21452
+ allRows(
21453
+ this.db.prepare(
21454
+ this.fileSelect(
21455
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21456
+ )
21457
+ ),
21458
+ [projectId, pat, pat]
21459
+ )
20800
21460
  );
20801
21461
  }
20802
21462
  fetchProjectFilesBlocked(projectId) {
20803
21463
  return this.mapFileRows(
20804
- this.db.prepare(
20805
- this.fileSelect(
20806
- "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
20807
- )
20808
- ).all(projectId)
21464
+ allRows(
21465
+ this.db.prepare(
21466
+ this.fileSelect(
21467
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21468
+ )
21469
+ ),
21470
+ [projectId]
21471
+ )
20809
21472
  );
20810
21473
  }
20811
21474
  fetchProjectFile(projectId, path) {
20812
21475
  const rows = this.mapFileRows(
20813
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21476
+ allRows(
21477
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21478
+ [projectId, path]
21479
+ )
20814
21480
  );
20815
21481
  return rows[0] ?? null;
20816
21482
  }
@@ -20824,39 +21490,32 @@ var SqlitePoliciesRepository = class {
20824
21490
  }
20825
21491
  db;
20826
21492
  readPolicies() {
20827
- const rows = this.db.prepare("SELECT * FROM policies").all();
20828
- const policies = [];
20829
- for (const row of rows) {
20830
- try {
20831
- const target = JSON.parse(row.target);
20832
- const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
20833
- policies.push(
20834
- Policy.parse({
20835
- id: row.id,
20836
- scope: row.scope,
20837
- target,
20838
- action: row.action,
20839
- enabled: row.enabled === 1,
20840
- customKeywords
20841
- })
20842
- );
20843
- } catch {
20844
- }
20845
- }
21493
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21494
+ const policies = mapRowsTolerant(rows, (row) => {
21495
+ const target = JSON.parse(row.target);
21496
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21497
+ return Policy.parse({
21498
+ id: row.id,
21499
+ scope: row.scope,
21500
+ target,
21501
+ action: row.action,
21502
+ enabled: intToBool(row.enabled),
21503
+ customKeywords
21504
+ });
21505
+ });
20846
21506
  return Promise.resolve(policies);
20847
21507
  }
20848
21508
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20849
21509
  // detection-type config exists from first run. Only when the table is empty,
20850
21510
  // so a user's edits are never clobbered.
20851
21511
  seedDefaults() {
20852
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21512
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20853
21513
  if (count > 0) return;
20854
21514
  const stmt = this.db.prepare(
20855
21515
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20856
21516
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20857
21517
  );
20858
- this.db.exec("BEGIN");
20859
- try {
21518
+ failOpenTransaction(this.db, () => {
20860
21519
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20861
21520
  stmt.run({
20862
21521
  id: randomUUID4(),
@@ -20865,10 +21524,41 @@ var SqlitePoliciesRepository = class {
20865
21524
  now: Date.now()
20866
21525
  });
20867
21526
  }
20868
- this.db.exec("COMMIT");
20869
- } catch {
20870
- this.db.exec("ROLLBACK");
20871
- }
21527
+ });
21528
+ }
21529
+ // Insert-or-update the single global per-category policy row, keyed on the
21530
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21531
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21532
+ // values), so the runtime's resolveAction reads rows written by either path
21533
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21534
+ // `id` and `created_at` are left exactly as they were.
21535
+ upsertCategoryAction(category, action) {
21536
+ const now = Date.now();
21537
+ this.db.prepare(
21538
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21539
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21540
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21541
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21542
+ }
21543
+ // Caps every global per-category policy currently set to block/redact down
21544
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21545
+ // Returns the number of rows changed.
21546
+ capCategoryActions() {
21547
+ const info = this.db.prepare(
21548
+ `UPDATE policies SET action='warn', updated_at=:now
21549
+ WHERE scope='global' AND action IN ('block','redact')
21550
+ AND json_extract(target,'$.category') IS NOT NULL`
21551
+ ).run({ now: Date.now() });
21552
+ return Number(info.changes);
21553
+ }
21554
+ // Read the current action for a single global per-category policy row, mirroring
21555
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21556
+ // row exists yet, so callers can distinguish an unset category from a set one.
21557
+ getCategoryAction(category) {
21558
+ const row = this.db.prepare(
21559
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21560
+ ).get({ category });
21561
+ return row?.action;
20872
21562
  }
20873
21563
  };
20874
21564
 
@@ -20944,7 +21634,7 @@ var SqliteProjectFilesRepository = class {
20944
21634
  maxStampStmt;
20945
21635
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20946
21636
  replaceForProject(projectId, scan2, now) {
20947
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21637
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20948
21638
  const stamp = Math.max(now, maxStamp + 1);
20949
21639
  for (const file2 of scan2.files) {
20950
21640
  this.upsertStmt.run({
@@ -21027,7 +21717,7 @@ var SqliteResolutionsRepository = class {
21027
21717
  }
21028
21718
  /** The newest disposition recorded for a finding key, or undefined if none. */
21029
21719
  latestByKey(key) {
21030
- const row = this.latestStmt.get({ findingKey: key });
21720
+ const row = getRow(this.latestStmt, { findingKey: key });
21031
21721
  if (!row) return void 0;
21032
21722
  return {
21033
21723
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -21044,7 +21734,7 @@ var SqliteResolutionsRepository = class {
21044
21734
  * the CLI) surfaces for that file.
21045
21735
  */
21046
21736
  openAtRestKeysForPath(path) {
21047
- const rows = this.openAtRestStmt.all({ path });
21737
+ const rows = allRows(this.openAtRestStmt, { path });
21048
21738
  return rows.map((r) => r.finding_key);
21049
21739
  }
21050
21740
  /**
@@ -21055,7 +21745,7 @@ var SqliteResolutionsRepository = class {
21055
21745
  * resolution row (see scan.ts).
21056
21746
  */
21057
21747
  resolvedAtRestKeysForPath(path) {
21058
- const rows = this.resolvedAtRestStmt.all({ path });
21748
+ const rows = allRows(this.resolvedAtRestStmt, { path });
21059
21749
  return rows.map((r) => r.finding_key);
21060
21750
  }
21061
21751
  };
@@ -21084,38 +21774,31 @@ var SqliteScanLedgerRepository = class {
21084
21774
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
21085
21775
  // older ruleset are simply absent, which reads as "never scanned".
21086
21776
  entriesForRuleset(rulesetHash) {
21087
- const rows = this.readStmt.all({ rulesetHash });
21777
+ const rows = allRows(this.readStmt, {
21778
+ rulesetHash
21779
+ });
21088
21780
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
21089
21781
  }
21090
21782
  upsertEntries(entries) {
21091
21783
  if (entries.length === 0) return;
21092
21784
  const scannedAt = Date.now();
21093
- try {
21094
- this.db.exec("BEGIN");
21095
- try {
21096
- for (const entry of entries) {
21097
- this.upsertStmt.run({
21098
- path: entry.path,
21099
- mtime: entry.mtime,
21100
- contentHash: entry.contentHash,
21101
- rulesetHash: entry.rulesetHash,
21102
- scannedAt
21103
- });
21104
- }
21105
- this.db.exec("COMMIT");
21106
- } catch (err) {
21107
- this.db.exec("ROLLBACK");
21108
- throw err;
21785
+ failOpenTransaction(this.db, () => {
21786
+ for (const entry of entries) {
21787
+ this.upsertStmt.run({
21788
+ path: entry.path,
21789
+ mtime: entry.mtime,
21790
+ contentHash: entry.contentHash,
21791
+ rulesetHash: entry.rulesetHash,
21792
+ scannedAt
21793
+ });
21109
21794
  }
21110
- } catch {
21111
- }
21795
+ });
21112
21796
  }
21113
21797
  };
21114
21798
 
21115
21799
  // ../../packages/persistence/src/repositories/security.ts
21116
21800
  var DAY_MS4 = 864e5;
21117
21801
  var SEVERITIES = ["critical", "high", "medium", "low"];
21118
- var RANGE_DAYS = { "7d": 7, "30d": 30, "3m": 90, "6m": 180 };
21119
21802
  var ACTION_TO_KIND = {
21120
21803
  block: "blocked",
21121
21804
  redact: "redacted",
@@ -21130,8 +21813,14 @@ var SCAN_COVERAGE = [
21130
21813
  { provider: "copilot", coverage: 0, supported: false },
21131
21814
  { provider: "api", coverage: 0, supported: false }
21132
21815
  ];
21816
+ var GRANULARITY = {
21817
+ "7d": "day",
21818
+ "30d": "day",
21819
+ "3m": "week",
21820
+ "6m": "week"
21821
+ };
21133
21822
  function granularityFor(range) {
21134
- return range === "7d" || range === "30d" ? "day" : "week";
21823
+ return GRANULARITY[range];
21135
21824
  }
21136
21825
  function startOfUtcDay2(ms) {
21137
21826
  return Math.floor(ms / DAY_MS4) * DAY_MS4;
@@ -21185,8 +21874,9 @@ var SqliteSecurityRepository = class {
21185
21874
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21186
21875
  // double-counting a key that accumulated several append-only rows.
21187
21876
  severitySummary() {
21188
- const rows = this.db.prepare(
21189
- `SELECT f.severity AS severity,
21877
+ const rows = allRows(
21878
+ this.db.prepare(
21879
+ `SELECT f.severity AS severity,
21190
21880
  COUNT(*) AS count,
21191
21881
  SUM(CASE
21192
21882
  WHEN e.kind != 'code_change' THEN 1
@@ -21205,7 +21895,8 @@ var SqliteSecurityRepository = class {
21205
21895
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21206
21896
  ON latest.finding_key = f.finding_key
21207
21897
  GROUP BY f.severity`
21208
- ).all();
21898
+ )
21899
+ );
21209
21900
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21210
21901
  const bySeverity = SEVERITIES.map((severity) => ({
21211
21902
  severity,
@@ -21289,14 +21980,15 @@ var SqliteSecurityRepository = class {
21289
21980
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21290
21981
  const now = this.now();
21291
21982
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
21292
- const rows = this.db.prepare(
21293
- // first_detected_at is the PRESERVED first-detection time (set once on a
21294
- // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21295
- // measures from first sighting — not the latest re-scan's event, whose
21296
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21297
- // the parent event's occurred_at defends against any legacy/edge row the
21298
- // backfill left null.
21299
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21983
+ const rows = allRows(
21984
+ this.db.prepare(
21985
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21986
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21987
+ // measures from first sighting — not the latest re-scan's event, whose
21988
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21989
+ // the parent event's occurred_at defends against any legacy/edge row the
21990
+ // backfill left null.
21991
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21300
21992
  (
21301
21993
  SELECT fr.status FROM finding_resolution fr
21302
21994
  WHERE fr.finding_key = f.finding_key
@@ -21322,14 +22014,16 @@ var SqliteSecurityRepository = class {
21322
22014
  WHERE fr.finding_key = f.finding_key
21323
22015
  AND fr.resolved_at >= :windowStart
21324
22016
  )`
21325
- // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21326
- // any resolution activity at/after the window start — a row this method
21327
- // ultimately counts has its LATEST resolution inside the window, which
21328
- // implies such a row exists, so nothing wanted is dropped. The exact
21329
- // latest-wins + status/method + window gate stays in JS below,
21330
- // dialect-agnostic. Without this, a
21331
- // 7d request evaluated the store's entire trackable-findings history.
21332
- ).all({ windowStart });
22017
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
22018
+ // any resolution activity at/after the window start — a row this method
22019
+ // ultimately counts has its LATEST resolution inside the window, which
22020
+ // implies such a row exists, so nothing wanted is dropped. The exact
22021
+ // latest-wins + status/method + window gate stays in JS below,
22022
+ // dialect-agnostic. Without this, a
22023
+ // 7d request evaluated the store's entire trackable-findings history.
22024
+ ),
22025
+ { windowStart }
22026
+ );
21333
22027
  const sums = /* @__PURE__ */ new Map();
21334
22028
  const counts = /* @__PURE__ */ new Map();
21335
22029
  for (const r of rows) {
@@ -21359,8 +22053,9 @@ var SqliteSecurityRepository = class {
21359
22053
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21360
22054
  const now = this.now();
21361
22055
  const from = now - RANGE_DAYS[range] * DAY_MS4;
21362
- const rows = this.db.prepare(
21363
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
22056
+ const rows = allRows(
22057
+ this.db.prepare(
22058
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21364
22059
  FROM findings f JOIN events e ON e.id = f.event_id
21365
22060
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21366
22061
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21368,7 +22063,9 @@ var SqliteSecurityRepository = class {
21368
22063
  GROUP BY repo
21369
22064
  ORDER BY c DESC, repo
21370
22065
  LIMIT :limit`
21371
- ).all({ from, to: now, limit });
22066
+ ),
22067
+ { from, to: now, limit }
22068
+ );
21372
22069
  const items = rows.map((r) => ({
21373
22070
  id: `repo_${r.repo}`,
21374
22071
  name: r.repo,
@@ -21390,8 +22087,9 @@ var SqliteSecurityRepository = class {
21390
22087
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21391
22088
  // capped at `limit`.
21392
22089
  recentlyResolved(limit = 20) {
21393
- const rows = this.db.prepare(
21394
- `SELECT f.finding_key AS finding_key,
22090
+ const rows = allRows(
22091
+ this.db.prepare(
22092
+ `SELECT f.finding_key AS finding_key,
21395
22093
  f.rule_id AS rule_id,
21396
22094
  f.severity AS severity,
21397
22095
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21425,7 +22123,9 @@ var SqliteSecurityRepository = class {
21425
22123
  ) IS NOT NULL
21426
22124
  ORDER BY latest_resolved_at DESC
21427
22125
  LIMIT :limit`
21428
- ).all({ limit });
22126
+ ),
22127
+ { limit }
22128
+ );
21429
22129
  const items = rows.map((r) => ({
21430
22130
  findingKey: r.finding_key,
21431
22131
  ruleId: r.rule_id,
@@ -21442,12 +22142,15 @@ var SqliteSecurityRepository = class {
21442
22142
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21443
22143
  // numeric and the JS aggregations bucket/split on ms directly.
21444
22144
  findingsInRange(fromMs, toMs) {
21445
- const rows = this.db.prepare(
21446
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22145
+ const rows = allRows(
22146
+ this.db.prepare(
22147
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21447
22148
  FROM findings f JOIN events e ON e.id = f.event_id
21448
22149
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21449
22150
  ORDER BY e.occurred_at`
21450
- ).all({ from: fromMs, to: toMs });
22151
+ ),
22152
+ { from: fromMs, to: toMs }
22153
+ );
21451
22154
  return rows.map((r) => ({
21452
22155
  occurredAt: r.occurred_at,
21453
22156
  severity: r.severity,
@@ -21461,12 +22164,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21461
22164
  var KIND_ORDER = ["provider", "internal", "ip"];
21462
22165
  var CALL_SITE_EMBED_CAP = 200;
21463
22166
  function parseNetwork(networkJson) {
21464
- if (!networkJson) return null;
21465
- try {
21466
- return JSON.parse(networkJson);
21467
- } catch {
21468
- return null;
21469
- }
22167
+ return safeJson(networkJson, null);
21470
22168
  }
21471
22169
  function toEndpointSummary(row) {
21472
22170
  return {
@@ -21553,27 +22251,39 @@ var SqliteSharesRepository = class {
21553
22251
  }
21554
22252
  db;
21555
22253
  stats() {
21556
- const scalar = (sql) => this.db.prepare(sql).get()?.n ?? 0;
21557
- const destinations = scalar("SELECT count(*) AS n FROM share_destination");
21558
- const endpoints = scalar("SELECT count(*) AS n FROM share_endpoint");
21559
- const callSites = scalar("SELECT count(*) AS n FROM share_call_site");
21560
- const insecure = scalar(
22254
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
22255
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
22256
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
22257
+ const insecure = countScalar(
22258
+ this.db,
21561
22259
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21562
22260
  );
21563
- const needsReview = scalar(
22261
+ const needsReview = countScalar(
22262
+ this.db,
21564
22263
  `SELECT count(DISTINCT d.id) AS n
21565
22264
  FROM share_destination d
21566
22265
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21567
22266
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21568
22267
  );
21569
- const byKind = { provider: 0, internal: 0, ip: 0 };
21570
- for (const row of this.db.prepare("SELECT kind, count(*) AS n FROM share_destination GROUP BY kind").all()) {
21571
- byKind[row.kind] = row.n;
21572
- }
21573
- const byTrust = { recognized: 0, internal: 0, unverified: 0, ip: 0 };
21574
- for (const row of this.db.prepare("SELECT trust, count(*) AS n FROM share_destination GROUP BY trust").all()) {
21575
- byTrust[row.trust] = row.n;
21576
- }
22268
+ const kindCounts = countBy(
22269
+ this.db,
22270
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
22271
+ );
22272
+ const byKind = {
22273
+ provider: kindCounts.get("provider") ?? 0,
22274
+ internal: kindCounts.get("internal") ?? 0,
22275
+ ip: kindCounts.get("ip") ?? 0
22276
+ };
22277
+ const trustCounts = countBy(
22278
+ this.db,
22279
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
22280
+ );
22281
+ const byTrust = {
22282
+ recognized: trustCounts.get("recognized") ?? 0,
22283
+ internal: trustCounts.get("internal") ?? 0,
22284
+ unverified: trustCounts.get("unverified") ?? 0,
22285
+ ip: trustCounts.get("ip") ?? 0
22286
+ };
21577
22287
  return Promise.resolve({
21578
22288
  destinations,
21579
22289
  endpoints,
@@ -21689,7 +22399,7 @@ var SqliteSharesRepository = class {
21689
22399
  }
21690
22400
  let sql;
21691
22401
  if (q) {
21692
- const pattern = `%${escapeLikePattern(q)}%`;
22402
+ const pattern = containsPattern(q);
21693
22403
  conditions.push(
21694
22404
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21695
22405
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21709,24 +22419,31 @@ var SqliteSharesRepository = class {
21709
22419
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21710
22420
  ORDER BY d.created_at ASC, d.id ASC`;
21711
22421
  }
21712
- const rows = this.db.prepare(sql).all(...params);
22422
+ const rows = allRows(
22423
+ this.db.prepare(sql),
22424
+ params
22425
+ );
21713
22426
  return rows.map((r) => this.mapDestRow(r));
21714
22427
  }
21715
22428
  fetchDestinationById(destinationId) {
21716
- const row = this.db.prepare(
21717
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22429
+ const row = getRow(
22430
+ this.db.prepare(
22431
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21718
22432
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21719
22433
  o.decision AS overrideDecision
21720
22434
  FROM share_destination d
21721
22435
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21722
22436
  WHERE d.id = ?`
21723
- ).get(destinationId);
22437
+ ),
22438
+ [destinationId]
22439
+ );
21724
22440
  return row ? this.mapDestRow(row) : null;
21725
22441
  }
21726
22442
  fetchEndpoints(destinationIds) {
21727
22443
  if (destinationIds.length === 0) return [];
21728
- const rows = this.db.prepare(
21729
- `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
22444
+ const rows = allRows(
22445
+ this.db.prepare(
22446
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21730
22447
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21731
22448
  count(c.id) AS callSiteCount
21732
22449
  FROM share_endpoint e
@@ -21734,7 +22451,9 @@ var SqliteSharesRepository = class {
21734
22451
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21735
22452
  GROUP BY e.id
21736
22453
  ORDER BY e.created_at ASC, e.id ASC`
21737
- ).all(...destinationIds);
22454
+ ),
22455
+ destinationIds
22456
+ );
21738
22457
  return rows.map((r) => ({
21739
22458
  id: r.id,
21740
22459
  destinationId: r.destinationId,
@@ -21759,13 +22478,16 @@ var SqliteSharesRepository = class {
21759
22478
  }
21760
22479
  fetchCallSites(endpointIds) {
21761
22480
  if (endpointIds.length === 0) return [];
21762
- const rows = this.db.prepare(
21763
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22481
+ const rows = allRows(
22482
+ this.db.prepare(
22483
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21764
22484
  project_id AS projectId
21765
22485
  FROM share_call_site
21766
22486
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21767
22487
  ORDER BY created_at ASC, id ASC`
21768
- ).all(...endpointIds);
22488
+ ),
22489
+ endpointIds
22490
+ );
21769
22491
  return rows.map((r) => ({
21770
22492
  id: r.id,
21771
22493
  endpointId: r.endpointId,
@@ -21801,27 +22523,36 @@ var SqliteSourceProjectRepository = class {
21801
22523
  upsert(input, now = Date.now()) {
21802
22524
  const id = sourceProjectId(input.url);
21803
22525
  const row = toSourceProjectRow(input, id, now);
21804
- this.upsertStmt.run({
21805
- id: row.id,
21806
- url: row.url ?? null,
21807
- name: row.name ?? null,
21808
- attributes: row.attributes,
21809
- firstSeen: row.firstSeen,
21810
- lastSeen: row.lastSeen
21811
- });
22526
+ this.upsertStmt.run(
22527
+ bindParams({
22528
+ id: row.id,
22529
+ url: row.url,
22530
+ name: row.name,
22531
+ attributes: row.attributes,
22532
+ firstSeen: row.firstSeen,
22533
+ lastSeen: row.lastSeen
22534
+ })
22535
+ );
21812
22536
  return id;
21813
22537
  }
21814
22538
  findById(id) {
21815
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22539
+ return getRow(
22540
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22541
+ {
22542
+ id
22543
+ }
22544
+ );
21816
22545
  }
21817
22546
  // Distinct project names — a filter facet, served from the source_project
21818
22547
  // table, never from the audit fact table.
21819
22548
  distinctNames() {
21820
- const rows = this.db.prepare(
21821
- `SELECT DISTINCT name FROM source_project
22549
+ const rows = allRows(
22550
+ this.db.prepare(
22551
+ `SELECT DISTINCT name FROM source_project
21822
22552
  WHERE name IS NOT NULL
21823
22553
  ORDER BY name`
21824
- ).all();
22554
+ )
22555
+ );
21825
22556
  return rows.map((r) => r.name);
21826
22557
  }
21827
22558
  };
@@ -21840,8 +22571,7 @@ function hasLegacySampleRows(db) {
21840
22571
  function purgeSampleData(db) {
21841
22572
  try {
21842
22573
  if (!hasLegacySampleRows(db)) return;
21843
- db.exec("BEGIN");
21844
- try {
22574
+ withTransaction(db, () => {
21845
22575
  db.exec(
21846
22576
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21847
22577
  SELECT e.id FROM share_endpoint e
@@ -21886,11 +22616,7 @@ function purgeSampleData(db) {
21886
22616
  value TEXT NOT NULL
21887
22617
  )`);
21888
22618
  db.exec("DELETE FROM app_meta WHERE key LIKE 'sample_seeded:%'");
21889
- db.exec("COMMIT");
21890
- } catch (err) {
21891
- db.exec("ROLLBACK");
21892
- throw err;
21893
- }
22619
+ });
21894
22620
  } catch {
21895
22621
  }
21896
22622
  }
@@ -21909,7 +22635,7 @@ function openWithPragmas(file2) {
21909
22635
  function backupLegacyStore(file2) {
21910
22636
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21911
22637
  renameSync(file2, backup);
21912
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22638
+ for (const sidecar of walSidecars(file2)) {
21913
22639
  if (existsSync(sidecar)) rmSync(sidecar);
21914
22640
  }
21915
22641
  return backup;
@@ -21922,9 +22648,8 @@ function openLocalDatabase(dir) {
21922
22648
  db.close();
21923
22649
  const backup = backupLegacyStore(file2);
21924
22650
  db = openWithPragmas(file2);
21925
- process.stderr.write(
21926
- `[aka] Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.
21927
- `
22651
+ akaWarn(
22652
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21928
22653
  );
21929
22654
  }
21930
22655
  applyMigrations(db);
@@ -21952,96 +22677,77 @@ function openLocalDatabase(dir) {
21952
22677
  const configInventory = new SqliteConfigInventoryRepository(db);
21953
22678
  policies.seedDefaults();
21954
22679
  function recordCapture(event, detected) {
21955
- try {
21956
- db.exec("BEGIN");
21957
- try {
21958
- events.insertEvent(event);
21959
- const sessionId = event.metadata?.sessionId;
21960
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
21961
- db.exec("COMMIT");
21962
- } catch (err) {
21963
- db.exec("ROLLBACK");
21964
- throw err;
21965
- }
21966
- } catch {
21967
- }
22680
+ failOpenTransaction(db, () => {
22681
+ events.insertEvent(event);
22682
+ const sessionId = event.metadata?.sessionId;
22683
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22684
+ });
21968
22685
  }
21969
22686
  function ensureInventory(ctx) {
21970
22687
  const resolved = {};
21971
- try {
21972
- db.exec("BEGIN");
21973
- try {
21974
- const now = Date.now();
21975
- if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
21976
- if (ctx.harness) {
21977
- resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21978
- }
21979
- resolved.accountId = inventory.upsert(
21980
- linkHost(
21981
- {
21982
- objectType: "user",
21983
- identityKey: "local",
21984
- attributes: { source: "local" }
21985
- },
21986
- resolved.hostId
21987
- ),
21988
- now
21989
- );
21990
- if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
21991
- db.exec("COMMIT");
21992
- } catch (err) {
21993
- db.exec("ROLLBACK");
21994
- throw err;
22688
+ const committed = failOpenTransaction(db, () => {
22689
+ const now = Date.now();
22690
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22691
+ if (ctx.harness) {
22692
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21995
22693
  }
21996
- } catch {
21997
- return {};
21998
- }
21999
- return resolved;
22694
+ resolved.accountId = inventory.upsert(
22695
+ linkHost(
22696
+ {
22697
+ objectType: "user",
22698
+ identityKey: "local",
22699
+ attributes: { source: "local" }
22700
+ },
22701
+ resolved.hostId
22702
+ ),
22703
+ now
22704
+ );
22705
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22706
+ });
22707
+ return committed ? resolved : {};
22000
22708
  }
22001
22709
  function recordConfigScan(record2) {
22002
- try {
22003
- db.exec("BEGIN");
22004
- try {
22005
- const now = isoToEpochMillis(record2.scanEvent.startedAt);
22006
- for (const item of record2.items) inventory.upsert(item, now);
22007
- auditEvents.insertAuditEvent(record2.scanEvent);
22008
- const definitionIds = /* @__PURE__ */ new Map();
22009
- for (const def of record2.definitions ?? []) {
22010
- definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22011
- }
22012
- for (const finding2 of record2.findings ?? []) {
22013
- const definitionId = definitionIds.get(`${finding2.ruleId}@${finding2.version}`);
22014
- if (!definitionId) continue;
22015
- inspectionFindings.insertFinding({
22016
- id: randomUUID8(),
22017
- auditEventId: record2.scanEvent.id,
22018
- inspectionDefinitionId: definitionId,
22019
- span: finding2.span,
22020
- maskedMatch: finding2.maskedMatch,
22021
- actionTaken: finding2.actionTaken,
22022
- confidence: finding2.confidence
22023
- });
22024
- }
22025
- db.exec("COMMIT");
22026
- } catch (err) {
22027
- db.exec("ROLLBACK");
22028
- throw err;
22710
+ failOpenTransaction(db, () => {
22711
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22712
+ for (const item of record2.items) inventory.upsert(item, now);
22713
+ auditEvents.insertAuditEvent(record2.scanEvent);
22714
+ const definitionIds = /* @__PURE__ */ new Map();
22715
+ for (const def of record2.definitions ?? []) {
22716
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22717
+ }
22718
+ for (const finding2 of record2.findings ?? []) {
22719
+ const definitionId = definitionIds.get(`${finding2.ruleId}@${finding2.version}`);
22720
+ if (!definitionId) continue;
22721
+ inspectionFindings.insertFinding({
22722
+ id: randomUUID8(),
22723
+ auditEventId: record2.scanEvent.id,
22724
+ inspectionDefinitionId: definitionId,
22725
+ span: finding2.span,
22726
+ maskedMatch: finding2.maskedMatch,
22727
+ actionTaken: finding2.actionTaken,
22728
+ confidence: finding2.confidence
22729
+ });
22029
22730
  }
22030
- } catch {
22031
- }
22731
+ });
22032
22732
  }
22033
22733
  function recordProjectFiles(projectId, scan2) {
22034
22734
  if (scan2.files.length === 0) return;
22735
+ failOpenTransaction(db, () => {
22736
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22737
+ });
22738
+ }
22739
+ async function transaction(fn) {
22740
+ db.exec("BEGIN");
22035
22741
  try {
22036
- db.exec("BEGIN");
22742
+ const result = await fn();
22743
+ db.exec("COMMIT");
22744
+ return result;
22745
+ } catch (err) {
22037
22746
  try {
22038
- projectFiles.replaceForProject(projectId, scan2, Date.now());
22039
- db.exec("COMMIT");
22040
- } catch (err) {
22041
22747
  db.exec("ROLLBACK");
22042
- throw err;
22748
+ } catch {
22043
22749
  }
22044
- } catch {
22750
+ throw err;
22045
22751
  }
22046
22752
  }
22047
22753
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -22060,8 +22766,7 @@ function openLocalDatabase(dir) {
22060
22766
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
22061
22767
  });
22062
22768
  if (stale.length === 0) return;
22063
- db.exec("BEGIN");
22064
- try {
22769
+ withTransaction(db, () => {
22065
22770
  for (const { id } of stale) {
22066
22771
  db.prepare(
22067
22772
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -22073,11 +22778,7 @@ function openLocalDatabase(dir) {
22073
22778
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
22074
22779
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
22075
22780
  }
22076
- db.exec("COMMIT");
22077
- } catch (err) {
22078
- db.exec("ROLLBACK");
22079
- throw err;
22080
- }
22781
+ });
22081
22782
  } catch {
22082
22783
  }
22083
22784
  }
@@ -22119,6 +22820,7 @@ function openLocalDatabase(dir) {
22119
22820
  purgeSampleData: () => {
22120
22821
  purgeSampleData(db);
22121
22822
  },
22823
+ transaction,
22122
22824
  close: () => {
22123
22825
  db.close();
22124
22826
  }
@@ -22211,12 +22913,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22211
22913
  }
22212
22914
  }
22213
22915
  function readJson(file2) {
22916
+ let text;
22214
22917
  try {
22215
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22216
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22918
+ text = readFileSync2(file2, "utf8");
22217
22919
  } catch {
22218
22920
  return null;
22219
22921
  }
22922
+ return parseJsonObject(text) ?? null;
22923
+ }
22924
+
22925
+ // ../../packages/persistence/src/warn-era-cap.ts
22926
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22927
+ import { join as join5 } from "path";
22928
+ var MARKER = "warn-era-capped";
22929
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22930
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22931
+ const marker = join5(dataDir2, MARKER);
22932
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22933
+ const capped = db.policies.capCategoryActions();
22934
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22935
+ `, { mode: DATA_FILE_MODE });
22936
+ return { capped };
22220
22937
  }
22221
22938
 
22222
22939
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22291,23 +23008,30 @@ function resolveProviderSafe() {
22291
23008
  // ../../packages/plugin-sdk/src/config-inventory.ts
22292
23009
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22293
23010
  import { homedir as homedir2 } from "os";
22294
- import { basename as basename3, join as join6 } from "path";
23011
+ import { basename as basename3, join as join7 } from "path";
23012
+
23013
+ // ../../packages/detections/src/escape-regexp.ts
23014
+ function escapeRegExp(value) {
23015
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23016
+ }
23017
+
23018
+ // ../../packages/detections/src/matchers/limits.ts
23019
+ var MAX_MATCHES_PER_RULE = 1e4;
22295
23020
 
22296
23021
  // ../../packages/detections/src/matchers/keyword.ts
22297
23022
  var KeywordMatcher2 = class {
22298
23023
  match(text, rule) {
22299
23024
  if (rule.matcher.type !== "keyword") return [];
22300
23025
  const { keywords, caseSensitive } = rule.matcher;
22301
- const haystack = caseSensitive ? text : text.toLowerCase();
22302
23026
  const spans = [];
22303
23027
  for (const kw of keywords) {
22304
- const needle = caseSensitive ? kw : kw.toLowerCase();
22305
- let idx = 0;
22306
- while (idx < haystack.length) {
22307
- const pos = haystack.indexOf(needle, idx);
22308
- if (pos === -1) break;
22309
- spans.push({ start: pos, end: pos + kw.length });
22310
- idx = pos + 1;
23028
+ if (kw.length === 0) continue;
23029
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
23030
+ const re = new RegExp(escapeRegExp(kw), caseSensitive ? "gu" : "giu");
23031
+ let m;
23032
+ while ((m = re.exec(text)) !== null) {
23033
+ spans.push({ start: m.index, end: m.index + m[0].length });
23034
+ if (spans.length >= MAX_MATCHES_PER_RULE) break;
22311
23035
  }
22312
23036
  }
22313
23037
  return spans;
@@ -22315,7 +23039,6 @@ var KeywordMatcher2 = class {
22315
23039
  };
22316
23040
 
22317
23041
  // ../../packages/detections/src/matchers/regex.ts
22318
- var MAX_MATCHES_PER_RULE = 1e4;
22319
23042
  var RegexMatcher2 = class {
22320
23043
  match(text, rule) {
22321
23044
  if (rule.matcher.type !== "regex") return [];
@@ -22401,9 +23124,6 @@ function registerPack(pack) {
22401
23124
  function getLoadedRules() {
22402
23125
  return [...packs.values()].flatMap((p) => p.rules);
22403
23126
  }
22404
- function escapeRegExp(value) {
22405
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22406
- }
22407
23127
  function isCorroborated(candidate, candidates, text) {
22408
23128
  const req = candidate.rule.requiresNearby;
22409
23129
  if (!req) return true;
@@ -23267,7 +23987,6 @@ var db_table_name_default = {
23267
23987
  "SELECT * FROM ",
23268
23988
  "SELECT COUNT(*) FROM ",
23269
23989
  "INSERT INTO ",
23270
- "UPDATE ",
23271
23990
  "DELETE FROM ",
23272
23991
  "CREATE TABLE ",
23273
23992
  "ALTER TABLE ",
@@ -24755,8 +25474,8 @@ function maskText(text) {
24755
25474
  }
24756
25475
 
24757
25476
  // ../../packages/plugin-sdk/src/repo.ts
24758
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "fs";
24759
- import { basename as basename2, dirname, isAbsolute, join as join5, sep as sep2 } from "path";
25477
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25478
+ import { basename as basename2, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24760
25479
  function resolveRepoIdentity(cwd) {
24761
25480
  try {
24762
25481
  const root = findGitRoot(cwd);
@@ -24806,7 +25525,7 @@ function resolveGitBranch(cwd) {
24806
25525
  try {
24807
25526
  const root = findGitRoot(cwd);
24808
25527
  if (!root) return void 0;
24809
- const dotGit = join5(root, ".git");
25528
+ const dotGit = join6(root, ".git");
24810
25529
  let gitdir;
24811
25530
  try {
24812
25531
  gitdir = statSync(dotGit).isDirectory() ? dotGit : resolveWorktreeGitdir(root, dotGit);
@@ -24814,7 +25533,7 @@ function resolveGitBranch(cwd) {
24814
25533
  return void 0;
24815
25534
  }
24816
25535
  if (gitdir === void 0) return void 0;
24817
- const head = safeRead(join5(gitdir, "HEAD"));
25536
+ const head = safeRead(join6(gitdir, "HEAD"));
24818
25537
  if (!head) return void 0;
24819
25538
  return /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(head)?.[1];
24820
25539
  } catch {
@@ -24824,37 +25543,37 @@ function resolveGitBranch(cwd) {
24824
25543
  function resolveWorktreeGitdir(root, dotGitFile) {
24825
25544
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGitFile) ?? "")?.[1];
24826
25545
  if (!target) return void 0;
24827
- return isAbsolute(target) ? target : join5(root, target);
25546
+ return isAbsolute(target) ? target : join6(root, target);
24828
25547
  }
24829
25548
  function findGitRoot(start) {
24830
25549
  let dir = start;
24831
25550
  for (; ; ) {
24832
- if (existsSync2(join5(dir, ".git"))) return dir;
25551
+ if (existsSync3(join6(dir, ".git"))) return dir;
24833
25552
  const parent = dirname(dir);
24834
25553
  if (parent === dir) return void 0;
24835
25554
  dir = parent;
24836
25555
  }
24837
25556
  }
24838
25557
  function resolveGitContext(root) {
24839
- const dotGit = join5(root, ".git");
25558
+ const dotGit = join6(root, ".git");
24840
25559
  try {
24841
25560
  if (statSync(dotGit).isDirectory()) {
24842
- return { configPath: join5(dotGit, "config"), headRoot: root };
25561
+ return { configPath: join6(dotGit, "config"), headRoot: root };
24843
25562
  }
24844
25563
  } catch {
24845
25564
  return void 0;
24846
25565
  }
24847
25566
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
24848
25567
  if (!target) return void 0;
24849
- const gitdir = isAbsolute(target) ? target : join5(root, target);
24850
- if (existsSync2(join5(gitdir, "config"))) {
24851
- return { configPath: join5(gitdir, "config"), headRoot: root };
25568
+ const gitdir = isAbsolute(target) ? target : join6(root, target);
25569
+ if (existsSync3(join6(gitdir, "config"))) {
25570
+ return { configPath: join6(gitdir, "config"), headRoot: root };
24852
25571
  }
24853
- const commonRaw = safeRead(join5(gitdir, "commondir"))?.trim();
25572
+ const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
24854
25573
  if (!commonRaw) return void 0;
24855
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join5(gitdir, commonRaw);
25574
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
24856
25575
  const headRoot = basename2(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
24857
- return { configPath: join5(commonGitDir, "config"), headRoot };
25576
+ return { configPath: join6(commonGitDir, "config"), headRoot };
24858
25577
  }
24859
25578
  function safeRead(path) {
24860
25579
  try {
@@ -24920,31 +25639,31 @@ function resolveConfigInventory(input) {
24920
25639
  };
24921
25640
  try {
24922
25641
  const home = input.homeDir ?? homedir2();
24923
- const claudeDir = join6(home, ".claude");
25642
+ const claudeDir = join7(home, ".claude");
24924
25643
  const repo = resolveRepoIdentity(input.cwd);
24925
25644
  const repoIdentity = repo?.url ?? input.cwd;
24926
25645
  const projectSource = `project:${repoIdentity}`;
24927
- collectSettingsHooks(scan2, join6(claudeDir, "settings.json"), "user");
24928
- collectSettingsHooks(scan2, join6(input.cwd, ".claude", "settings.json"), "project");
24929
- collectSettingsHooks(scan2, join6(input.cwd, ".claude", "settings.local.json"), "local");
25646
+ collectSettingsHooks(scan2, join7(claudeDir, "settings.json"), "user");
25647
+ collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.json"), "project");
25648
+ collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.local.json"), "local");
24930
25649
  const projectOrigin = { scope: "project", project: repoIdentity };
24931
- collectMcpFile(scan2, join6(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
24932
- collectUserClaudeJson(scan2, join6(home, ".claude.json"), input.cwd, repoIdentity);
24933
- collectMcpFile(scan2, join6(claudeDir, "settings.json"), { scope: "user" });
24934
- collectMcpFile(scan2, join6(input.cwd, ".claude", "settings.json"), projectOrigin);
24935
- collectMcpFile(scan2, join6(input.cwd, ".claude", "settings.local.json"), {
25650
+ collectMcpFile(scan2, join7(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
25651
+ collectUserClaudeJson(scan2, join7(home, ".claude.json"), input.cwd, repoIdentity);
25652
+ collectMcpFile(scan2, join7(claudeDir, "settings.json"), { scope: "user" });
25653
+ collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.json"), projectOrigin);
25654
+ collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.local.json"), {
24936
25655
  scope: "local",
24937
25656
  project: repoIdentity
24938
25657
  });
24939
25658
  collectConfigFiles(scan2, claudeDir, input.cwd);
24940
- collectSkillsDir(scan2, join6(claudeDir, "skills"), { source: "local", scope: "user" });
24941
- collectSkillsDir(scan2, join6(input.cwd, ".claude", "skills"), {
25659
+ collectSkillsDir(scan2, join7(claudeDir, "skills"), { source: "local", scope: "user" });
25660
+ collectSkillsDir(scan2, join7(input.cwd, ".claude", "skills"), {
24942
25661
  source: projectSource,
24943
25662
  scope: "project"
24944
25663
  });
24945
25664
  collectInstalledPlugins(scan2, claudeDir);
24946
25665
  collectMarketplaceSkills(scan2, claudeDir);
24947
- collectSkillsDir(scan2, join6(input.cwd, "skills"), { source: projectSource, scope: "project" });
25666
+ collectSkillsDir(scan2, join7(input.cwd, "skills"), { source: projectSource, scope: "project" });
24948
25667
  scan2.skills = dedupeSkills(scan2.skills);
24949
25668
  scan2.mcpServers = dedupeMcpServers(scan2.mcpServers);
24950
25669
  } catch (err) {
@@ -25073,7 +25792,7 @@ function projectEntryFor(projects, cwd) {
25073
25792
  return void 0;
25074
25793
  }
25075
25794
  function collectPluginManifestMcp(scan2, installPath, origin) {
25076
- const manifestPath = join6(installPath, ".claude-plugin", "plugin.json");
25795
+ const manifestPath = join7(installPath, ".claude-plugin", "plugin.json");
25077
25796
  const raw = readOptional(manifestPath);
25078
25797
  if (raw === void 0) return;
25079
25798
  try {
@@ -25081,7 +25800,7 @@ function collectPluginManifestMcp(scan2, installPath, origin) {
25081
25800
  if (typeof parsed !== "object" || parsed === null) return;
25082
25801
  const declared = parsed.mcpServers;
25083
25802
  if (typeof declared === "string" && declared.length > 0) {
25084
- collectMcpFile(scan2, join6(installPath, declared), origin, { recordErrors: true });
25803
+ collectMcpFile(scan2, join7(installPath, declared), origin, { recordErrors: true });
25085
25804
  } else {
25086
25805
  collectMcpObject(scan2, declared, manifestPath, origin);
25087
25806
  }
@@ -25098,14 +25817,14 @@ var SETTINGS_KEY_LABELS = [
25098
25817
  ["statusLine", "status line"]
25099
25818
  ];
25100
25819
  function collectConfigFiles(scan2, claudeDir, cwd) {
25101
- settingsConfigFile(scan2, join6(claudeDir, "settings.json"), "user", "User settings");
25102
- settingsConfigFile(scan2, join6(cwd, ".claude", "settings.json"), "project", "Project settings");
25103
- settingsConfigFile(scan2, join6(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
25104
- memoryConfigFile(scan2, join6(claudeDir, "CLAUDE.md"), "user", "User memory");
25105
- memoryConfigFile(scan2, join6(cwd, "CLAUDE.md"), "project", "Project memory");
25106
- mcpJsonConfigFile(scan2, join6(cwd, ".mcp.json"));
25107
- dirConfigFile(scan2, join6(cwd, ".claude", "commands"), "Slash commands", "command");
25108
- dirConfigFile(scan2, join6(cwd, ".claude", "agents"), "Subagents", "subagent");
25820
+ settingsConfigFile(scan2, join7(claudeDir, "settings.json"), "user", "User settings");
25821
+ settingsConfigFile(scan2, join7(cwd, ".claude", "settings.json"), "project", "Project settings");
25822
+ settingsConfigFile(scan2, join7(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
25823
+ memoryConfigFile(scan2, join7(claudeDir, "CLAUDE.md"), "user", "User memory");
25824
+ memoryConfigFile(scan2, join7(cwd, "CLAUDE.md"), "project", "Project memory");
25825
+ mcpJsonConfigFile(scan2, join7(cwd, ".mcp.json"));
25826
+ dirConfigFile(scan2, join7(cwd, ".claude", "commands"), "Slash commands", "command");
25827
+ dirConfigFile(scan2, join7(cwd, ".claude", "agents"), "Subagents", "subagent");
25109
25828
  }
25110
25829
  function configFileEntry(path, scope, kind) {
25111
25830
  try {
@@ -25180,7 +25899,7 @@ function countMarkdownFiles(dir, depth) {
25180
25899
  let count = 0;
25181
25900
  for (const dirent of readdirSync(dir, { withFileTypes: true })) {
25182
25901
  if (dirent.name.startsWith(".")) continue;
25183
- if (dirent.isDirectory()) count += countMarkdownFiles(join6(dir, dirent.name), depth + 1);
25902
+ if (dirent.isDirectory()) count += countMarkdownFiles(join7(dir, dirent.name), depth + 1);
25184
25903
  else if (dirent.name.endsWith(".md")) count += 1;
25185
25904
  }
25186
25905
  return count;
@@ -25193,7 +25912,7 @@ function collectSkillsDir(scan2, dir, origin) {
25193
25912
  return;
25194
25913
  }
25195
25914
  for (const name of names) {
25196
- const skillFile = join6(dir, name, "SKILL.md");
25915
+ const skillFile = join7(dir, name, "SKILL.md");
25197
25916
  try {
25198
25917
  const raw = readOptional(skillFile);
25199
25918
  if (raw === void 0) continue;
@@ -25202,7 +25921,7 @@ function collectSkillsDir(scan2, dir, origin) {
25202
25921
  name: front.name ?? name,
25203
25922
  source: origin.source,
25204
25923
  scope: origin.scope,
25205
- location: join6(dir, name),
25924
+ location: join7(dir, name),
25206
25925
  updatedAt: statSync2(skillFile).mtime.toISOString()
25207
25926
  };
25208
25927
  const version2 = front.version ?? origin.defaultVersion;
@@ -25233,7 +25952,7 @@ function parseFrontmatter(raw) {
25233
25952
  return out;
25234
25953
  }
25235
25954
  function collectInstalledPlugins(scan2, claudeDir) {
25236
- const manifestPath = join6(claudeDir, "plugins", "installed_plugins.json");
25955
+ const manifestPath = join7(claudeDir, "plugins", "installed_plugins.json");
25237
25956
  const raw = readOptional(manifestPath);
25238
25957
  if (raw === void 0) return;
25239
25958
  let plugins;
@@ -25258,7 +25977,7 @@ function collectInstalledPlugins(scan2, claudeDir) {
25258
25977
  if (typeof installPath !== "string" || seen.has(installPath)) continue;
25259
25978
  seen.add(installPath);
25260
25979
  const version2 = install.version;
25261
- const hooksPath = join6(installPath, "hooks", "hooks.json");
25980
+ const hooksPath = join7(installPath, "hooks", "hooks.json");
25262
25981
  const hooksRaw = readOptional(hooksPath);
25263
25982
  if (hooksRaw !== void 0) {
25264
25983
  try {
@@ -25278,22 +25997,22 @@ function collectInstalledPlugins(scan2, claudeDir) {
25278
25997
  }
25279
25998
  const origin = { source: marketplace, scope: "plugin", pluginName };
25280
25999
  if (typeof version2 === "string") origin.defaultVersion = version2;
25281
- collectSkillsDir(scan2, join6(installPath, "skills"), origin);
26000
+ collectSkillsDir(scan2, join7(installPath, "skills"), origin);
25282
26001
  const mcpOrigin = { scope: "plugin", pluginName, marketplace };
25283
- collectMcpFile(scan2, join6(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
26002
+ collectMcpFile(scan2, join7(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
25284
26003
  collectPluginManifestMcp(scan2, installPath, mcpOrigin);
25285
26004
  }
25286
26005
  }
25287
26006
  }
25288
26007
  function collectMarketplaceSkills(scan2, claudeDir) {
25289
- for (const mp of readMarketplaces(join6(claudeDir, "plugins", "known_marketplaces.json"))) {
26008
+ for (const mp of readMarketplaces(join7(claudeDir, "plugins", "known_marketplaces.json"))) {
25290
26009
  if (isClaudeOfficialMarketplace(mp.name, mp.repo)) continue;
25291
- collectSkillsDir(scan2, join6(mp.installLocation, "skills"), {
26010
+ collectSkillsDir(scan2, join7(mp.installLocation, "skills"), {
25292
26011
  source: mp.name,
25293
26012
  scope: "plugin"
25294
26013
  });
25295
- collectPluginSkillDirs(scan2, join6(mp.installLocation, "plugins"), mp.name);
25296
- collectPluginSkillDirs(scan2, join6(mp.installLocation, "external_plugins"), mp.name);
26014
+ collectPluginSkillDirs(scan2, join7(mp.installLocation, "plugins"), mp.name);
26015
+ collectPluginSkillDirs(scan2, join7(mp.installLocation, "external_plugins"), mp.name);
25297
26016
  }
25298
26017
  }
25299
26018
  function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
@@ -25304,7 +26023,7 @@ function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
25304
26023
  return;
25305
26024
  }
25306
26025
  for (const plugin of plugins) {
25307
- collectSkillsDir(scan2, join6(pluginsDir, plugin, "skills"), {
26026
+ collectSkillsDir(scan2, join7(pluginsDir, plugin, "skills"), {
25308
26027
  source: marketplace,
25309
26028
  scope: "plugin",
25310
26029
  pluginName: plugin
@@ -25413,22 +26132,22 @@ function resolveInventoryContext(input) {
25413
26132
  }
25414
26133
 
25415
26134
  // ../../packages/plugin-sdk/src/nudge.ts
25416
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
25417
- import { join as join7 } from "path";
26135
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
26136
+ import { join as join8 } from "path";
25418
26137
  var SESSION_START_MARKER = "session-start-last";
25419
26138
  function claimSessionStart(dataDir2, sessionId) {
25420
26139
  return claimOncePerSession(dataDir2, SESSION_START_MARKER, sessionId);
25421
26140
  }
25422
26141
  function claimOncePerSession(dataDir2, marker, sessionId) {
25423
26142
  if (!sessionId) return true;
25424
- const path = join7(dataDir2, marker);
26143
+ const path = join8(dataDir2, marker);
25425
26144
  try {
25426
26145
  if (readFileSync5(path, "utf8") === sessionId) return false;
25427
26146
  } catch {
25428
26147
  }
25429
26148
  try {
25430
26149
  mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
25431
- writeFileSync3(path, sessionId, { mode: DATA_FILE_MODE });
26150
+ writeFileSync4(path, sessionId, { mode: DATA_FILE_MODE });
25432
26151
  } catch {
25433
26152
  }
25434
26153
  return true;
@@ -25436,8 +26155,8 @@ function claimOncePerSession(dataDir2, marker, sessionId) {
25436
26155
 
25437
26156
  // ../../packages/plugin-sdk/src/project-files.ts
25438
26157
  var import_ignore = __toESM(require_ignore(), 1);
25439
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25440
- import { basename as basename4, join as join8, relative, sep as sep3 } from "path";
26158
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
26159
+ import { basename as basename4, join as join9, relative, sep as sep3 } from "path";
25441
26160
  var SKIP_DIRS = /* @__PURE__ */ new Set([
25442
26161
  ".git",
25443
26162
  "node_modules",
@@ -25457,7 +26176,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
25457
26176
  var MAX_FILES = 2e4;
25458
26177
  function readIgnoreLayer(dir) {
25459
26178
  try {
25460
- const content = readFileSync6(join8(dir, ".gitignore"), "utf8");
26179
+ const content = readFileSync6(join9(dir, ".gitignore"), "utf8");
25461
26180
  return { base: dir, matcher: (0, import_ignore.default)().add(content) };
25462
26181
  } catch {
25463
26182
  return void 0;
@@ -25535,10 +26254,10 @@ function resolveProjectFiles(cwd) {
25535
26254
  const layer = readIgnoreLayer(dir);
25536
26255
  const dirLayers = layer ? [...layers, layer] : layers;
25537
26256
  for (const entry of dirents) {
25538
- const fullPath = join8(dir, entry.name);
26257
+ const fullPath = join9(dir, entry.name);
25539
26258
  if (entry.isDirectory()) {
25540
26259
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
25541
- if (existsSync3(join8(fullPath, ".git"))) continue;
26260
+ if (existsSync4(join9(fullPath, ".git"))) continue;
25542
26261
  if (visit2(fullPath, dirLayers)) return true;
25543
26262
  continue;
25544
26263
  }
@@ -25575,18 +26294,21 @@ function resolveProjectFiles(cwd) {
25575
26294
  // ../../packages/plugin-sdk/src/runtime.ts
25576
26295
  import { randomUUID as randomUUID10 } from "crypto";
25577
26296
 
26297
+ // ../../packages/plugin-sdk/src/suppressions.ts
26298
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
26299
+
25578
26300
  // ../../packages/plugin-sdk/src/throttle.ts
25579
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
25580
- import { join as join9 } from "path";
26301
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
26302
+ import { join as join10 } from "path";
25581
26303
  function throttled(dataDir2, markerName, windowMs) {
25582
- const marker = join9(dataDir2, markerName);
26304
+ const marker = join10(dataDir2, markerName);
25583
26305
  try {
25584
26306
  if (Date.now() - statSync3(marker).mtimeMs < windowMs) return true;
25585
26307
  } catch {
25586
26308
  }
25587
26309
  try {
25588
26310
  mkdirSync4(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
25589
- writeFileSync4(marker, String(Date.now()), { mode: DATA_FILE_MODE });
26311
+ writeFileSync5(marker, String(Date.now()), { mode: DATA_FILE_MODE });
25590
26312
  } catch {
25591
26313
  }
25592
26314
  return false;
@@ -25786,6 +26508,14 @@ var StandaloneDataGateway = class {
25786
26508
  sweepTerminalExceptions(retentionMs) {
25787
26509
  return this.db.exceptions.sweepTerminal(retentionMs);
25788
26510
  }
26511
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
26512
+ // from SessionStart, not part of the DataGateway port. Returns the number
26513
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
26514
+ // already-capped one).
26515
+ capWarnEraEnforcement(policyMode) {
26516
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
26517
+ return { capped };
26518
+ }
25789
26519
  // One project-file scan → the local project_file tree (one transaction inside
25790
26520
  // the LocalDatabase, fail-open there). Like the sweep above, this is
25791
26521
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -25908,6 +26638,15 @@ async function handleSessionStart(input, config2 = loadConfig()) {
25908
26638
  await gateway.sweepTerminalExceptions(EXCEPTION_RETENTION_MS);
25909
26639
  } catch {
25910
26640
  }
26641
+ try {
26642
+ const { capped } = gateway.capWarnEraEnforcement(config2.settings.policy);
26643
+ if (capped > 0) {
26644
+ process.stderr.write(
26645
+ 'AKA: the global "warn only" handling was retired; your existing block/redact categories were kept at warn. Re-run /aka:setup to adopt per-category enforcement.\n'
26646
+ );
26647
+ }
26648
+ } catch {
26649
+ }
25911
26650
  try {
25912
26651
  if (resolved.sourceProjectId) {
25913
26652
  const filesScan = resolveProjectFiles(input.cwd);
@@ -26001,7 +26740,7 @@ function buildSessionRoot(sessionId, input, ctx, resolved, provider, branch) {
26001
26740
 
26002
26741
  // src/history/reconcile-trigger.ts
26003
26742
  import { spawn } from "child_process";
26004
- import { dirname as dirname2, join as join11 } from "path";
26743
+ import { dirname as dirname2, join as join12 } from "path";
26005
26744
  import { fileURLToPath } from "url";
26006
26745
 
26007
26746
  // src/history/tail.ts
@@ -26013,9 +26752,9 @@ import {
26013
26752
  openSync,
26014
26753
  readFileSync as readFileSync7,
26015
26754
  readSync,
26016
- writeFileSync as writeFileSync5
26755
+ writeFileSync as writeFileSync6
26017
26756
  } from "fs";
26018
- import { join as join10 } from "path";
26757
+ import { join as join11 } from "path";
26019
26758
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
26020
26759
  function safeSessionId(sessionId) {
26021
26760
  if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
@@ -26032,7 +26771,7 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
26032
26771
  const marker = `${RECONCILE_MARKER_PREFIX}-${safeSessionId(sessionId)}`;
26033
26772
  if (throttled(dataDir2, marker, RECONCILE_THROTTLE_MS)) return;
26034
26773
  const here = dirname2(fileURLToPath(import.meta.url));
26035
- const child = spawn(process.execPath, [join11(here, "reconcile.js"), sessionId, transcriptPath], {
26774
+ const child = spawn(process.execPath, [join12(here, "reconcile.js"), sessionId, transcriptPath], {
26036
26775
  detached: true,
26037
26776
  stdio: "ignore"
26038
26777
  });