@akasecurity/ai-tc-claude-code 0.8.1 → 0.8.2

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.
@@ -15433,6 +15433,11 @@ var AuditEventType = external_exports.enum([
15433
15433
  "prompt",
15434
15434
  "response",
15435
15435
  "code_change",
15436
+ // The events.kind of a scanned tool call, widened in to keep this a
15437
+ // superset. Narrower than 'tool_call' above and not a duplicate of it:
15438
+ // 'tool_call' is the reconciler's structural row for every call, while
15439
+ // 'tool_use' exists only where a hook enforced against the arguments.
15440
+ "tool_use",
15436
15441
  // One row per config-inventory scan, hung off the session root. It is the
15437
15442
  // fact the posture inspection findings reference (findings require an
15438
15443
  // audit_event_id), and its started_at is the "scanned Nm ago" the read
@@ -15824,7 +15829,7 @@ var ActivityOverviewResponse = external_exports.object({
15824
15829
  }).meta({ id: "ActivityOverviewResponse" });
15825
15830
 
15826
15831
  // ../../packages/schema/src/zod/event.ts
15827
- var EventKind = external_exports.enum(["prompt", "response", "code_change"]).meta({ id: "EventKind" });
15832
+ var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15828
15833
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15829
15834
  var EventMetadata = external_exports.object({
15830
15835
  sessionId: external_exports.string().optional(),
@@ -16137,7 +16142,7 @@ var DetectionException = external_exports.object({
16137
16142
  justification: external_exports.string().min(1),
16138
16143
  conditions: ExceptionConditions.nullable(),
16139
16144
  createdBy: external_exports.string(),
16140
- createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api"]),
16145
+ createdVia: external_exports.enum(["cli-approve", "cli-add", "web-approve", "web-add", "api", "setup-triage"]),
16141
16146
  createdAt: external_exports.iso.datetime(),
16142
16147
  updatedAt: external_exports.iso.datetime(),
16143
16148
  // Revocation is terminal and retained — consumed/expired/revoked rows are
@@ -16306,20 +16311,26 @@ var PolicyBundle = external_exports.object({
16306
16311
  customKeywords: external_exports.array(external_exports.string()),
16307
16312
  fetchedAt: external_exports.iso.datetime()
16308
16313
  }).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
16314
  var OBSERVE_ONLY_CATEGORIES = ["config"];
16322
16315
  var ENFORCEABLE_CATEGORIES = DetectionCategory.options.filter((c) => !OBSERVE_ONLY_CATEGORIES.includes(c));
16316
+ var CATEGORY_PEAK_SEVERITY = {
16317
+ secret: "critical",
16318
+ financial: "critical",
16319
+ // core-financial/credit-card
16320
+ code_flaw: "critical",
16321
+ pii: "high",
16322
+ phi: "high",
16323
+ custom: "high",
16324
+ // user-defined; conservative
16325
+ code_context: "low",
16326
+ config: "low"
16327
+ // observe-only; floors to monitor regardless
16328
+ };
16329
+ function severityFloorPolicy(category) {
16330
+ if (OBSERVE_ONLY_CATEGORIES.includes(category)) return "monitor";
16331
+ const peak = CATEGORY_PEAK_SEVERITY[category];
16332
+ return peak === "critical" || peak === "high" ? "warn" : "monitor";
16333
+ }
16323
16334
  var PolicyKind = external_exports.enum(["builtin", "custom"]).meta({ id: "PolicyKind" });
16324
16335
  var KNOWN_BUILTIN_IDS = ["monitor", "warn", "redact", "block"];
16325
16336
  var BuiltinPolicyId = external_exports.enum(KNOWN_BUILTIN_IDS).meta({ id: "BuiltinPolicyId" });
@@ -16346,6 +16357,12 @@ var BUILTIN_POLICY_SPECS = {
16346
16357
  description: "Refuse the request entirely whenever any rule in this detection matches."
16347
16358
  }
16348
16359
  };
16360
+ function builtinPolicyToAction(id) {
16361
+ return BUILTIN_POLICY_SPECS[id].action;
16362
+ }
16363
+ var DEFAULT_ACTIONS = Object.fromEntries(
16364
+ DetectionCategory.options.map((c) => [c, builtinPolicyToAction(severityFloorPolicy(c))])
16365
+ );
16349
16366
  var BUILTIN_POLICIES = Object.fromEntries(
16350
16367
  KNOWN_BUILTIN_IDS.map((id) => [id, { id, ...BUILTIN_POLICY_SPECS[id] }])
16351
16368
  );
@@ -16866,10 +16883,8 @@ function toApiProvider(sourceTool) {
16866
16883
  return TOOL_TO_HARNESS[sourceTool] ?? "api";
16867
16884
  }
16868
16885
  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
- );
16886
+ function foldGroupStatus(instanceStatuses) {
16887
+ const statuses = new Set(instanceStatuses.filter((s) => s !== void 0));
16873
16888
  if (statuses.size === 0) return void 0;
16874
16889
  for (const candidate of STATUS_PRECEDENCE) {
16875
16890
  if (statuses.has(candidate)) return candidate;
@@ -16887,6 +16902,7 @@ function deriveFindingStatus(row) {
16887
16902
  function buildFindingGroups(rows, opts = {}) {
16888
16903
  const overrides = opts.overrides;
16889
16904
  const packNames = opts.packNames;
16905
+ const aggregates = opts.aggregates;
16890
16906
  const byRuleId = /* @__PURE__ */ new Map();
16891
16907
  for (const row of rows) {
16892
16908
  const existing = byRuleId.get(row.ruleId);
@@ -16908,17 +16924,20 @@ function buildFindingGroups(rows, opts = {}) {
16908
16924
  status: r.status
16909
16925
  };
16910
16926
  });
16911
- const latestDetectedAt = ruleRows.reduce(
16927
+ const agg = aggregates?.get(ruleId);
16928
+ const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
16912
16929
  (max, r) => r.occurredAt > max ? r.occurredAt : max,
16913
16930
  ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
16914
16931
  );
16915
16932
  const seenProviders = /* @__PURE__ */ new Set();
16916
- const providers = instances.map((i) => i.provider).filter((p) => {
16933
+ const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
16917
16934
  if (seenProviders.has(p)) return false;
16918
16935
  seenProviders.add(p);
16919
16936
  return true;
16920
16937
  });
16921
- const actionSet = new Set(instances.map((i) => i.action));
16938
+ const actionSet = new Set(
16939
+ agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
16940
+ );
16922
16941
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
16923
16942
  const severity = ruleRows[0]?.severity ?? "low";
16924
16943
  const detection = {
@@ -16932,8 +16951,10 @@ function buildFindingGroups(rows, opts = {}) {
16932
16951
  contextPrefix: ""
16933
16952
  // empty (pending privacy review)
16934
16953
  };
16935
- const status = deriveGroupStatus(instances);
16936
- groups.push({
16954
+ const status = foldGroupStatus(
16955
+ agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
16956
+ );
16957
+ const group = {
16937
16958
  id: ruleId,
16938
16959
  category: apiCategory,
16939
16960
  subtype: ruleId,
@@ -16942,21 +16963,26 @@ function buildFindingGroups(rows, opts = {}) {
16942
16963
  match,
16943
16964
  detection,
16944
16965
  policy,
16945
- instanceCount: instances.length,
16966
+ instanceCount: agg?.instanceCount ?? instances.length,
16946
16967
  providers,
16947
16968
  aggregateAction,
16948
16969
  latestDetectedAt,
16949
16970
  instances,
16950
16971
  status
16951
- });
16972
+ };
16973
+ if (agg) {
16974
+ actionsCache.set(group, [...actionSet]);
16975
+ if (agg.searchText !== void 0) {
16976
+ haystackCache.set(group, buildHaystack(group, agg.searchText));
16977
+ }
16978
+ }
16979
+ groups.push(group);
16952
16980
  }
16953
16981
  return groups;
16954
16982
  }
16955
16983
  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 = [
16984
+ function buildHaystack(g, extra) {
16985
+ return [
16960
16986
  g.subtype,
16961
16987
  g.category,
16962
16988
  g.match.maskedValue,
@@ -16964,11 +16990,25 @@ function groupHaystack(g) {
16964
16990
  g.id,
16965
16991
  ...g.instances.map((i) => i.repo),
16966
16992
  ...g.instances.map((i) => i.file),
16967
- ...g.instances.map((i) => i.id)
16993
+ ...g.instances.map((i) => i.id),
16994
+ ...extra === void 0 ? [] : [extra]
16968
16995
  ].join(" ").toLowerCase();
16996
+ }
16997
+ function groupHaystack(g) {
16998
+ const cached2 = haystackCache.get(g);
16999
+ if (cached2 !== void 0) return cached2;
17000
+ const haystack = buildHaystack(g);
16969
17001
  haystackCache.set(g, haystack);
16970
17002
  return haystack;
16971
17003
  }
17004
+ var actionsCache = /* @__PURE__ */ new WeakMap();
17005
+ function groupActions(g) {
17006
+ const cached2 = actionsCache.get(g);
17007
+ if (cached2 !== void 0) return cached2;
17008
+ const actions = [...new Set(g.instances.map((i) => i.action))];
17009
+ actionsCache.set(g, actions);
17010
+ return actions;
17011
+ }
16972
17012
  function applyFindingFilters(groups, opts) {
16973
17013
  let filtered = groups;
16974
17014
  if (opts.severity && opts.severity.length > 0) {
@@ -16981,7 +17021,7 @@ function applyFindingFilters(groups, opts) {
16981
17021
  }
16982
17022
  if (opts.actions && opts.actions.length > 0) {
16983
17023
  const actionSet = new Set(opts.actions);
16984
- filtered = filtered.filter((g) => g.instances.some((i) => actionSet.has(i.action)));
17024
+ filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
16985
17025
  }
16986
17026
  if (opts.subtype && opts.subtype.length > 0) {
16987
17027
  const subtypeSet = new Set(opts.subtype);
@@ -17033,8 +17073,7 @@ function computeFindingFacets(allGroups, opts) {
17033
17073
  });
17034
17074
  const actionMap = /* @__PURE__ */ new Map();
17035
17075
  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);
17076
+ for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
17038
17077
  }
17039
17078
  const forSubtype = applyFindingFilters(allGroups, {
17040
17079
  providers: opts.providers,
@@ -17596,6 +17635,132 @@ function reviewSeverityRank(reasons) {
17596
17635
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17597
17636
  }
17598
17637
 
17638
+ // ../../packages/schema/src/zod/triage.ts
17639
+ var TriageHit = external_exports.object({
17640
+ ruleId: external_exports.string(),
17641
+ category: DetectionCategory,
17642
+ severity: Severity,
17643
+ maskedMatch: external_exports.string(),
17644
+ rawMatch: external_exports.string(),
17645
+ context: external_exports.string(),
17646
+ filePath: external_exports.string().optional(),
17647
+ confidence: external_exports.number().min(0).max(1),
17648
+ id: external_exports.string().optional(),
17649
+ valueFingerprint: external_exports.string().optional(),
17650
+ keyVersion: external_exports.number().int().nonnegative().optional()
17651
+ });
17652
+ var TriagePolicy = BuiltinPolicyId;
17653
+ var TriageCategoryRec = external_exports.object({
17654
+ category: DetectionCategory,
17655
+ action: TriagePolicy,
17656
+ reasoning: external_exports.string(),
17657
+ genuineCount: external_exports.number().int().nonnegative(),
17658
+ fpCount: external_exports.number().int().nonnegative(),
17659
+ // TriageHit ids judged false-positive in this category. fpCount must equal
17660
+ // this array's length — enforced by the consumer, not this schema.
17661
+ fpIds: external_exports.array(external_exports.string())
17662
+ });
17663
+ var TriageRecommendation = external_exports.object({
17664
+ perCategory: external_exports.array(TriageCategoryRec),
17665
+ notes: external_exports.string()
17666
+ });
17667
+
17668
+ // ../../packages/persistence/src/internal/sql-text.ts
17669
+ function escapeLikePattern(s) {
17670
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
17671
+ }
17672
+ function placeholders(n) {
17673
+ return Array.from({ length: n }, () => "?").join(", ");
17674
+ }
17675
+ function containsPattern(q) {
17676
+ return `%${escapeLikePattern(q)}%`;
17677
+ }
17678
+ function likeAny(exprs) {
17679
+ return `(${exprs.map((e) => `${e} LIKE ? ESCAPE '\\'`).join(" OR ")})`;
17680
+ }
17681
+
17682
+ // ../../packages/persistence/src/internal/transactions.ts
17683
+ var savepointSeq = 0;
17684
+ function withTransaction(db, fn, mode = "DEFERRED") {
17685
+ if (db.isTransaction) {
17686
+ const savepoint = `aka_sp_${String(savepointSeq += 1)}`;
17687
+ db.exec(`SAVEPOINT ${savepoint}`);
17688
+ try {
17689
+ fn();
17690
+ db.exec(`RELEASE ${savepoint}`);
17691
+ } catch (error51) {
17692
+ try {
17693
+ db.exec(`ROLLBACK TO ${savepoint}`);
17694
+ db.exec(`RELEASE ${savepoint}`);
17695
+ } catch {
17696
+ }
17697
+ throw error51;
17698
+ }
17699
+ return;
17700
+ }
17701
+ db.exec(mode === "IMMEDIATE" ? "BEGIN IMMEDIATE" : "BEGIN");
17702
+ try {
17703
+ fn();
17704
+ db.exec("COMMIT");
17705
+ } catch (error51) {
17706
+ try {
17707
+ db.exec("ROLLBACK");
17708
+ } catch {
17709
+ }
17710
+ throw error51;
17711
+ }
17712
+ }
17713
+ function failOpenTransaction(db, fn, mode = "DEFERRED") {
17714
+ const nested = db.isTransaction;
17715
+ try {
17716
+ withTransaction(db, fn, mode);
17717
+ return true;
17718
+ } catch (error51) {
17719
+ if (!db.isTransaction && nested) throw error51;
17720
+ return false;
17721
+ }
17722
+ }
17723
+
17724
+ // ../../packages/persistence/src/internal/warn.ts
17725
+ function akaWarn(message2) {
17726
+ process.stderr.write(`[aka] ${message2}
17727
+ `);
17728
+ }
17729
+
17730
+ // ../../packages/persistence/src/db/migrations/introspection.ts
17731
+ function evidenceObjects(sql) {
17732
+ const objects = [];
17733
+ for (const m of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?`([^`]+)`/g)) {
17734
+ if (m[1] !== void 0 && !m[1].startsWith("__new_")) {
17735
+ objects.push({ kind: "table", name: m[1] });
17736
+ }
17737
+ }
17738
+ for (const m of sql.matchAll(/ALTER TABLE `([^`]+)` ADD (?:COLUMN )?`([^`]+)`/g)) {
17739
+ if (m[1] !== void 0 && m[2] !== void 0) {
17740
+ objects.push({ kind: "column", table: m[1], name: m[2] });
17741
+ }
17742
+ }
17743
+ return objects;
17744
+ }
17745
+ function schemaObjectExists(db, kind, name) {
17746
+ const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1").get(kind, name);
17747
+ return row !== void 0;
17748
+ }
17749
+ function indexExists(db, name) {
17750
+ return schemaObjectExists(db, "index", name);
17751
+ }
17752
+ function columnNames(db, table, opts) {
17753
+ const pragma = opts?.includeGenerated ? "table_xinfo" : "table_info";
17754
+ const columns = db.prepare(`PRAGMA ${pragma}(${table})`).all();
17755
+ return columns.map((c) => c.name);
17756
+ }
17757
+ function evidenceExists(db, object2) {
17758
+ if (object2.kind === "column") {
17759
+ return columnNames(db, object2.table, { includeGenerated: true }).includes(object2.name);
17760
+ }
17761
+ return schemaObjectExists(db, "table", object2.name);
17762
+ }
17763
+
17599
17764
  // ../../packages/persistence/src/ids.ts
17600
17765
  import { createHash } from "crypto";
17601
17766
  function sha256Hex(input) {
@@ -17632,28 +17797,6 @@ function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
17632
17797
  }
17633
17798
 
17634
17799
  // ../../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
17800
  function describeObject(object2) {
17658
17801
  return object2.kind === "column" ? `column ${object2.table}.${object2.name}` : `table ${object2.name}`;
17659
17802
  }
@@ -17664,10 +17807,6 @@ function createdIndexName(statement) {
17664
17807
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
17665
17808
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
17666
17809
  }
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
17810
  function applyMigrations(db) {
17672
17811
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
17673
17812
  db.exec(
@@ -17686,44 +17825,39 @@ function applyMigrations(db) {
17686
17825
  const present = evidence.filter((o) => evidenceExists(db, o));
17687
17826
  if (present.length > 0 && present.length < evidence.length) {
17688
17827
  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);
17828
+ 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.`;
17829
+ akaWarn(message2);
17830
+ throw new Error(`[aka] ${message2}`);
17693
17831
  }
17694
17832
  const alreadyApplied = evidence.length > 0 ? present.length === evidence.length : preLedgerStore && index < legacyCount;
17695
17833
  const wantsFkOff = /PRAGMA foreign_keys\s*=\s*OFF/i.test(migration.sql);
17696
17834
  const statements = splitStatements(migration.sql);
17697
17835
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = OFF");
17698
17836
  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;
17837
+ withTransaction(
17838
+ db,
17839
+ () => {
17840
+ for (const statement of statements) {
17841
+ const indexName = createdIndexName(statement);
17842
+ if (indexName === void 0) {
17843
+ if (alreadyApplied) continue;
17844
+ } else if (indexExists(db, indexName)) {
17845
+ continue;
17846
+ }
17847
+ db.exec(statement);
17707
17848
  }
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
- );
17849
+ if (wantsFkOff && !alreadyApplied) {
17850
+ const violations = db.prepare("PRAGMA foreign_key_check").all();
17851
+ if (violations.length > 0) {
17852
+ throw new Error(
17853
+ `[aka] sqlite migration ${migration.tag} left ${String(violations.length)} foreign-key violation(s); rolling back.`
17854
+ );
17855
+ }
17716
17856
  }
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
- }
17857
+ record2.run(migration.tag, Date.now());
17858
+ },
17859
+ "IMMEDIATE"
17860
+ );
17727
17861
  } finally {
17728
17862
  if (wantsFkOff) db.exec("PRAGMA foreign_keys = ON");
17729
17863
  }
@@ -17766,8 +17900,7 @@ var TOKEN_USAGE_COLUMNS = [
17766
17900
  }
17767
17901
  ];
17768
17902
  function ensureTokenUsageColumns(db) {
17769
- const columns = db.prepare("PRAGMA table_xinfo(audit_events)").all();
17770
- const existing = new Set(columns.map((c) => c.name));
17903
+ const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
17771
17904
  for (const column of TOKEN_USAGE_COLUMNS) {
17772
17905
  if (!existing.has(column.name)) {
17773
17906
  db.exec(column.ddl);
@@ -17805,47 +17938,39 @@ function reconcileSourceProjectIds(db) {
17805
17938
  repoint: db.prepare(`UPDATE ${table} SET project_id = ? WHERE project_id = ?`)
17806
17939
  }));
17807
17940
  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);
17941
+ withTransaction(
17942
+ db,
17943
+ () => {
17944
+ for (const { row, canonicalId } of legacy) {
17945
+ foldProject.run(
17946
+ canonicalId,
17947
+ row.url,
17948
+ row.name,
17949
+ row.attributes,
17950
+ row.firstSeen,
17951
+ row.lastSeen
17952
+ );
17953
+ repointAudit.run(canonicalId, row.id);
17954
+ for (const { dropCollisions, repoint } of pathTables) {
17955
+ dropCollisions.run(row.id, canonicalId);
17956
+ repoint.run(canonicalId, row.id);
17957
+ }
17958
+ repointCallSite.run(canonicalId, row.id);
17959
+ deleteLegacy.run(row.id);
17823
17960
  }
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
- }
17961
+ },
17962
+ "IMMEDIATE"
17963
+ );
17835
17964
  } catch (error51) {
17836
- process.stderr.write(`[aka] source_project id reconcile failed: ${String(error51)}
17837
- `);
17965
+ akaWarn(`source_project id reconcile failed: ${String(error51)}`);
17838
17966
  }
17839
17967
  }
17840
17968
  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");
17969
+ if (schemaObjectExists(db, "table", "tenants")) return true;
17970
+ return columnNames(db, "events").includes("tenant_id");
17845
17971
  }
17846
17972
  function ensureSyncedAtColumn(db, table) {
17847
- const columns = db.prepare(`PRAGMA table_info(${table})`).all();
17848
- if (!columns.some((c) => c.name === "synced_at")) {
17973
+ if (!columnNames(db, table).includes("synced_at")) {
17849
17974
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
17850
17975
  }
17851
17976
  }
@@ -17896,8 +18021,11 @@ function ensureDataDirSync(dir) {
17896
18021
  } catch {
17897
18022
  }
17898
18023
  }
18024
+ function walSidecars(file2) {
18025
+ return [`${file2}-wal`, `${file2}-shm`];
18026
+ }
17899
18027
  function tightenPerms(file2) {
17900
- for (const path of [file2, `${file2}-wal`, `${file2}-shm`]) {
18028
+ for (const path of [file2, ...walSidecars(file2)]) {
17901
18029
  try {
17902
18030
  chmodSync(path, DATA_FILE_MODE);
17903
18031
  } catch {
@@ -17905,12 +18033,68 @@ function tightenPerms(file2) {
17905
18033
  }
17906
18034
  }
17907
18035
 
17908
- // ../../packages/persistence/src/repositories/sql-utils.ts
17909
- function escapeLikePattern(s) {
17910
- return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
18036
+ // ../../packages/persistence/src/internal/json.ts
18037
+ function safeJson(s, fallback) {
18038
+ if (s == null) return fallback;
18039
+ try {
18040
+ return JSON.parse(s);
18041
+ } catch {
18042
+ return fallback;
18043
+ }
17911
18044
  }
17912
- function placeholders(n) {
17913
- return Array.from({ length: n }, () => "?").join(", ");
18045
+ function parseJsonObject(s) {
18046
+ if (s == null) return void 0;
18047
+ try {
18048
+ const parsed = JSON.parse(s);
18049
+ if (typeof parsed === "object" && parsed !== null) return parsed;
18050
+ } catch {
18051
+ }
18052
+ return void 0;
18053
+ }
18054
+
18055
+ // ../../packages/persistence/src/internal/rows.ts
18056
+ function allRows(stmt, params) {
18057
+ if (params === void 0) return stmt.all();
18058
+ if (Array.isArray(params)) return stmt.all(...params);
18059
+ return stmt.all(params);
18060
+ }
18061
+ function getRow(stmt, params) {
18062
+ if (params === void 0) return stmt.get();
18063
+ if (Array.isArray(params)) return stmt.get(...params);
18064
+ return stmt.get(params);
18065
+ }
18066
+ function intToBool(raw) {
18067
+ return raw === 1 || raw === true;
18068
+ }
18069
+ function boolToInt(b) {
18070
+ return b ? 1 : 0;
18071
+ }
18072
+ function bindParams(row) {
18073
+ const out = {};
18074
+ for (const [key, value] of Object.entries(row)) {
18075
+ out[key] = value === void 0 ? null : value;
18076
+ }
18077
+ return out;
18078
+ }
18079
+ function countScalar(db, sql, params) {
18080
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18081
+ }
18082
+ function countBy(db, sql, params) {
18083
+ const map2 = /* @__PURE__ */ new Map();
18084
+ for (const row of allRows(db.prepare(sql), params)) {
18085
+ map2.set(row.k, row.n);
18086
+ }
18087
+ return map2;
18088
+ }
18089
+ function mapRowsTolerant(rows, map2) {
18090
+ const out = [];
18091
+ for (const row of rows) {
18092
+ try {
18093
+ out.push(map2(row));
18094
+ } catch {
18095
+ }
18096
+ }
18097
+ return out;
17914
18098
  }
17915
18099
 
17916
18100
  // ../../packages/persistence/src/repositories/activity.ts
@@ -17962,15 +18146,11 @@ function encodeCursor(payload) {
17962
18146
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
17963
18147
  }
17964
18148
  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;
18149
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
18150
+ if (parsed !== void 0 && "startedAtMs" in parsed && "id" in parsed && typeof parsed.startedAtMs === "number" && typeof parsed.id === "string") {
18151
+ return parsed;
17973
18152
  }
18153
+ return null;
17974
18154
  }
17975
18155
  var DB_EVENT_TYPE_TO_KIND = {
17976
18156
  session: "session",
@@ -17987,15 +18167,8 @@ var DB_EVENT_TYPE_TO_KIND = {
17987
18167
  };
17988
18168
  function safeParseStringArray(raw) {
17989
18169
  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;
18170
+ const parsed = safeJson(raw, null);
18171
+ return Array.isArray(parsed) ? parsed : [];
17999
18172
  }
18000
18173
  function toHarness(raw) {
18001
18174
  const parsed = Harness.safeParse(raw);
@@ -18048,8 +18221,8 @@ function buildAuditEvent(row) {
18048
18221
  severity: severityParsed?.success ? severityParsed.data : null,
18049
18222
  link: linkParsed?.success ? linkParsed.data : null,
18050
18223
  targetId: row.target_id,
18051
- internal: toBool(row.internal),
18052
- flagged: toBool(row.flagged)
18224
+ internal: intToBool(row.internal),
18225
+ flagged: intToBool(row.flagged)
18053
18226
  };
18054
18227
  }
18055
18228
  var TIMELINE_COLUMNS = `
@@ -18075,12 +18248,15 @@ var SqliteActivityRepository = class {
18075
18248
  stats(tz) {
18076
18249
  const window = todayWindow(tz ?? defaultTimeZone(), this.now());
18077
18250
  const { startMs, endMs } = window;
18078
- const sessionsToday = this.db.prepare(
18251
+ const sessionsToday = countScalar(
18252
+ this.db,
18079
18253
  `SELECT count(*) AS n FROM audit_events
18080
- WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`
18081
- ).get(startMs, endMs).n;
18254
+ WHERE ${SESSION_ROOT} AND started_at >= ? AND started_at < ?`,
18255
+ [startMs, endMs]
18256
+ );
18082
18257
  const liveThreshold = this.now() - LIVE_ACTIVITY_WINDOW_MS;
18083
- const liveNow = this.db.prepare(
18258
+ const liveNow = countScalar(
18259
+ this.db,
18084
18260
  `SELECT count(*) AS n FROM audit_events s
18085
18261
  WHERE s.event_type = 'session' AND s.ended_at IS NULL
18086
18262
  AND max(
@@ -18089,22 +18265,29 @@ var SqliteActivityRepository = class {
18089
18265
  (SELECT max(${LAST_ACTIVITY_EXPR}) FROM audit_events e WHERE e.root_session_id = s.id),
18090
18266
  s.started_at
18091
18267
  )
18092
- ) >= ?`
18093
- ).get(liveThreshold).n;
18094
- const toolCallsToday = this.db.prepare(
18268
+ ) >= ?`,
18269
+ [liveThreshold]
18270
+ );
18271
+ const toolCallsToday = countScalar(
18272
+ this.db,
18095
18273
  `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(
18274
+ WHERE event_type = 'tool_call' AND started_at >= ? AND started_at < ?`,
18275
+ [startMs, endMs]
18276
+ );
18277
+ const findingsToday = countScalar(
18278
+ this.db,
18099
18279
  `SELECT count(*) AS n FROM inspection_findings f
18100
18280
  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(
18281
+ WHERE e.started_at >= ? AND e.started_at < ?`,
18282
+ [startMs, endMs]
18283
+ );
18284
+ const egressToday = countScalar(
18285
+ this.db,
18104
18286
  `SELECT count(DISTINCT json_extract(attributes, '$.destination')) AS n
18105
18287
  FROM audit_events
18106
- WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`
18107
- ).get(startMs, endMs).n;
18288
+ WHERE event_type = 'share' AND started_at >= ? AND started_at < ?`,
18289
+ [startMs, endMs]
18290
+ );
18108
18291
  return Promise.resolve({ sessionsToday, liveNow, toolCallsToday, findingsToday, egressToday });
18109
18292
  }
18110
18293
  listSessions(query) {
@@ -18126,7 +18309,7 @@ var SqliteActivityRepository = class {
18126
18309
  conditions.push("started_at <= ?");
18127
18310
  params.push(toMs);
18128
18311
  if (query.q) {
18129
- const pattern = `%${escapeLikePattern(query.q)}%`;
18312
+ const pattern = containsPattern(query.q);
18130
18313
  conditions.push(
18131
18314
  `(content LIKE ? ESCAPE '\\'
18132
18315
  OR json_extract(attributes, '$.project') LIKE ? ESCAPE '\\'
@@ -18145,8 +18328,9 @@ var SqliteActivityRepository = class {
18145
18328
  params.push(cursor.startedAtMs, cursor.startedAtMs, cursor.id);
18146
18329
  }
18147
18330
  const limit = query.limit;
18148
- const rows = this.db.prepare(
18149
- `SELECT id,
18331
+ const rows = allRows(
18332
+ this.db.prepare(
18333
+ `SELECT id,
18150
18334
  json_extract(attributes, '$.harness') AS harness,
18151
18335
  content AS title,
18152
18336
  json_extract(attributes, '$.project') AS project,
@@ -18159,7 +18343,9 @@ var SqliteActivityRepository = class {
18159
18343
  WHERE ${conditions.join(" AND ")}
18160
18344
  ORDER BY started_at DESC, id DESC
18161
18345
  LIMIT ?`
18162
- ).all(...params, limit + 1);
18346
+ ),
18347
+ [...params, limit + 1]
18348
+ );
18163
18349
  const hasMore = rows.length > limit;
18164
18350
  const page = hasMore ? rows.slice(0, limit) : rows;
18165
18351
  const rollups = this.rollupsFor(page.map((r) => r.id));
@@ -18176,8 +18362,9 @@ var SqliteActivityRepository = class {
18176
18362
  return Promise.resolve({ items, nextCursor });
18177
18363
  }
18178
18364
  getSession(sessionId) {
18179
- const rootRow = this.db.prepare(
18180
- `SELECT id,
18365
+ const rootRow = getRow(
18366
+ this.db.prepare(
18367
+ `SELECT id,
18181
18368
  json_extract(attributes, '$.harness') AS harness,
18182
18369
  content AS title,
18183
18370
  json_extract(attributes, '$.project') AS project,
@@ -18194,47 +18381,66 @@ var SqliteActivityRepository = class {
18194
18381
  FROM audit_events
18195
18382
  WHERE id = ? AND event_type = 'session'
18196
18383
  LIMIT 1`
18197
- ).get(sessionId);
18384
+ ),
18385
+ [sessionId]
18386
+ );
18198
18387
  if (!rootRow) return Promise.resolve(null);
18199
- const timelineRows = this.db.prepare(
18200
- `SELECT ${TIMELINE_COLUMNS}
18388
+ const timelineRows = allRows(
18389
+ this.db.prepare(
18390
+ `SELECT ${TIMELINE_COLUMNS}
18201
18391
  FROM audit_events
18202
18392
  WHERE id = ? OR root_session_id = ?
18203
18393
  ORDER BY started_at ASC, id ASC`
18204
- ).all(sessionId, sessionId);
18394
+ ),
18395
+ [sessionId, sessionId]
18396
+ );
18205
18397
  const events = timelineRows.map(buildAuditEvent).filter((e) => e !== null);
18206
- const tokenRow = this.db.prepare(
18207
- `SELECT
18398
+ const tokenRow = getRow(
18399
+ this.db.prepare(
18400
+ `SELECT
18208
18401
  coalesce(sum(input_tokens), 0) AS input,
18209
18402
  coalesce(sum(output_tokens), 0) AS output,
18210
18403
  coalesce(sum(cache_creation_input_tokens), 0) AS cache_creation,
18211
18404
  coalesce(sum(cache_read_input_tokens), 0) AS cache_read
18212
18405
  FROM audit_events
18213
18406
  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
18407
+ ),
18408
+ [sessionId]
18409
+ ) ?? { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
18410
+ const primaryModel = getRow(
18411
+ this.db.prepare(
18412
+ `SELECT model, provider FROM audit_events
18217
18413
  WHERE root_session_id = ? AND event_type = 'llm_call'
18218
18414
  ORDER BY started_at ASC, id ASC
18219
18415
  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,
18416
+ ),
18417
+ [sessionId]
18418
+ );
18419
+ const toolRows = allRows(
18420
+ this.db.prepare(
18421
+ `SELECT coalesce(json_extract(attributes, '$.tool_name'), json_extract(attributes, '$.tool')) AS tool,
18223
18422
  count(*) AS n
18224
18423
  FROM audit_events
18225
18424
  WHERE root_session_id = ? AND event_type = 'tool_call'
18226
18425
  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
18426
+ ),
18427
+ [sessionId]
18428
+ );
18429
+ const modelRows = allRows(
18430
+ this.db.prepare(
18431
+ `SELECT DISTINCT model FROM audit_events
18230
18432
  WHERE root_session_id = ? AND event_type = 'llm_call' AND model IS NOT NULL AND model <> ''
18231
18433
  ORDER BY model`
18232
- ).all(sessionId);
18434
+ ),
18435
+ [sessionId]
18436
+ );
18233
18437
  const derivedModels = modelRows.map((r) => r.model);
18234
- const commits = this.db.prepare(
18438
+ const commits = countScalar(
18439
+ this.db,
18235
18440
  `SELECT count(*) AS n FROM audit_events
18236
- WHERE root_session_id = ? AND event_type = 'commit'`
18237
- ).get(sessionId).n;
18441
+ WHERE root_session_id = ? AND event_type = 'commit'`,
18442
+ [sessionId]
18443
+ );
18238
18444
  const rollup = this.rollupsFor([sessionId]).get(sessionId) ?? {
18239
18445
  turns: 0,
18240
18446
  findings: 0,
@@ -18301,7 +18507,10 @@ var SqliteActivityRepository = class {
18301
18507
  `SELECT DISTINCT coalesce(json_extract(attributes, '$.harness'), 'claudecode') AS harness
18302
18508
  FROM audit_events WHERE ${SESSION_ROOT}${where}`
18303
18509
  );
18304
- const rows = fromMs === void 0 ? stmt.all() : stmt.all(fromMs);
18510
+ const rows = allRows(
18511
+ stmt,
18512
+ fromMs === void 0 ? void 0 : [fromMs]
18513
+ );
18305
18514
  const seen = /* @__PURE__ */ new Set();
18306
18515
  for (const row of rows) seen.add(toHarness(row.harness));
18307
18516
  return Promise.resolve([...seen]);
@@ -18324,23 +18533,23 @@ var SqliteActivityRepository = class {
18324
18533
  conditions.push("started_at >= ?");
18325
18534
  params.push(opts.fromMs);
18326
18535
  }
18327
- const rows = this.db.prepare(
18328
- `SELECT root_session_id AS sessionId, attributes
18536
+ const rows = allRows(
18537
+ this.db.prepare(
18538
+ `SELECT root_session_id AS sessionId, attributes
18329
18539
  FROM audit_events
18330
18540
  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;
18541
+ ),
18542
+ params
18543
+ );
18544
+ return mapRowsTolerant(
18545
+ rows.filter(
18546
+ (row) => row.sessionId !== null
18547
+ ),
18548
+ (row) => ({
18549
+ sessionId: row.sessionId,
18550
+ attributes: JSON.parse(row.attributes)
18551
+ })
18552
+ );
18344
18553
  }
18345
18554
  /**
18346
18555
  * Per-session turns/findings/shares + last-activity for a page of session ids,
@@ -18354,57 +18563,72 @@ var SqliteActivityRepository = class {
18354
18563
  );
18355
18564
  if (sessionIds.length === 0) return result;
18356
18565
  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
18566
+ const lastActivityRows = allRows(
18567
+ this.db.prepare(
18568
+ `SELECT root_session_id AS id, max(${LAST_ACTIVITY_EXPR}) AS m FROM audit_events
18359
18569
  WHERE root_session_id IN (${inClause})
18360
18570
  GROUP BY root_session_id`
18361
- ).all(...sessionIds);
18571
+ ),
18572
+ sessionIds
18573
+ );
18362
18574
  for (const row of lastActivityRows) {
18363
18575
  if (row.id === null) continue;
18364
18576
  const entry = result.get(row.id);
18365
18577
  if (entry && row.m !== null) entry.lastActivityMs = row.m;
18366
18578
  }
18367
- const turnsRows = this.db.prepare(
18368
- `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18579
+ const turnsRows = allRows(
18580
+ this.db.prepare(
18581
+ `SELECT root_session_id AS id, count(*) AS n FROM audit_events
18369
18582
  WHERE root_session_id IN (${inClause}) AND event_type = 'prompt'
18370
18583
  GROUP BY root_session_id`
18371
- ).all(...sessionIds);
18584
+ ),
18585
+ sessionIds
18586
+ );
18372
18587
  for (const row of turnsRows) {
18373
18588
  if (row.id === null) continue;
18374
18589
  const entry = result.get(row.id);
18375
18590
  if (entry) entry.turns = row.n;
18376
18591
  }
18377
- const runKeyRows = this.db.prepare(
18378
- `SELECT root_session_id AS id,
18592
+ const runKeyRows = allRows(
18593
+ this.db.prepare(
18594
+ `SELECT root_session_id AS id,
18379
18595
  count(DISTINCT json_extract(attributes, '$.run_key')) AS n
18380
18596
  FROM audit_events
18381
18597
  WHERE root_session_id IN (${inClause}) AND event_type = 'llm_call'
18382
18598
  AND json_extract(attributes, '$.run_key') IS NOT NULL
18383
18599
  GROUP BY root_session_id`
18384
- ).all(...sessionIds);
18600
+ ),
18601
+ sessionIds
18602
+ );
18385
18603
  for (const row of runKeyRows) {
18386
18604
  if (row.id === null) continue;
18387
18605
  const entry = result.get(row.id);
18388
18606
  if (entry) entry.turns = Math.max(entry.turns, row.n);
18389
18607
  }
18390
- const findingsRows = this.db.prepare(
18391
- `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18608
+ const findingsRows = allRows(
18609
+ this.db.prepare(
18610
+ `SELECT e.root_session_id AS id, count(*) AS n FROM inspection_findings f
18392
18611
  JOIN audit_events e ON e.id = f.audit_event_id
18393
18612
  WHERE e.root_session_id IN (${inClause})
18394
18613
  GROUP BY e.root_session_id`
18395
- ).all(...sessionIds);
18614
+ ),
18615
+ sessionIds
18616
+ );
18396
18617
  for (const row of findingsRows) {
18397
18618
  if (row.id === null) continue;
18398
18619
  const entry = result.get(row.id);
18399
18620
  if (entry) entry.findings = row.n;
18400
18621
  }
18401
- const sharesRows = this.db.prepare(
18402
- `SELECT root_session_id AS id,
18622
+ const sharesRows = allRows(
18623
+ this.db.prepare(
18624
+ `SELECT root_session_id AS id,
18403
18625
  count(DISTINCT json_extract(attributes, '$.destination')) AS n
18404
18626
  FROM audit_events
18405
18627
  WHERE root_session_id IN (${inClause}) AND event_type = 'share'
18406
18628
  GROUP BY root_session_id`
18407
- ).all(...sessionIds);
18629
+ ),
18630
+ sessionIds
18631
+ );
18408
18632
  for (const row of sharesRows) {
18409
18633
  if (row.id === null) continue;
18410
18634
  const entry = result.get(row.id);
@@ -18454,33 +18678,28 @@ var SqliteAuditEventsRepository = class {
18454
18678
  // the caller fails open and drops the whole pass — recovered idempotently on the
18455
18679
  // next pass. Nesting-safe is NOT needed: the reconciler is the sole caller.
18456
18680
  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
- }
18681
+ withTransaction(this.db, fn);
18465
18682
  }
18466
18683
  insertAuditEvent(input) {
18467
18684
  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
- });
18685
+ this.insertStmt.run(
18686
+ bindParams({
18687
+ id: row.id,
18688
+ parentId: row.parentId,
18689
+ rootSessionId: row.rootSessionId,
18690
+ eventType: row.eventType,
18691
+ hostId: row.hostId,
18692
+ harnessId: row.harnessId,
18693
+ sourceProjectId: row.sourceProjectId,
18694
+ startedAt: row.startedAt,
18695
+ endedAt: row.endedAt,
18696
+ severity: row.severity,
18697
+ priority: row.priority,
18698
+ content: row.content,
18699
+ contentHash: row.contentHash,
18700
+ attributes: row.attributes
18701
+ })
18702
+ );
18484
18703
  }
18485
18704
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18486
18705
  // (which takes a caller-supplied random id), the id here is MINTED internally
@@ -18494,22 +18713,24 @@ var SqliteAuditEventsRepository = class {
18494
18713
  const startedAt = isoToEpochMillis(input.startedAt);
18495
18714
  if (!Number.isFinite(startedAt)) return;
18496
18715
  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
- });
18716
+ this.upsertLlmCallStmt.run(
18717
+ bindParams({
18718
+ id,
18719
+ parentId: input.parentId,
18720
+ rootSessionId: input.rootSessionId,
18721
+ eventType: "llm_call",
18722
+ hostId: null,
18723
+ harnessId: null,
18724
+ sourceProjectId: null,
18725
+ startedAt,
18726
+ endedAt: null,
18727
+ severity: null,
18728
+ priority: null,
18729
+ content: null,
18730
+ contentHash: null,
18731
+ attributes: JSON.stringify(input.attributes)
18732
+ })
18733
+ );
18513
18734
  }
18514
18735
  // Insert one transcript-derived `tool_call` leaf. Like `insertLlmCall` the id is
18515
18736
  // MINTED internally from the natural key — `toolCallId(sessionId, toolUseId)` —
@@ -18533,25 +18754,29 @@ var SqliteAuditEventsRepository = class {
18533
18754
  const startedAt = isoToEpochMillis(input.startedAt);
18534
18755
  if (!Number.isFinite(startedAt)) return;
18535
18756
  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
- });
18757
+ this.insertStmt.run(
18758
+ bindParams({
18759
+ id,
18760
+ parentId: input.parentId,
18761
+ rootSessionId: input.rootSessionId,
18762
+ eventType: "tool_call",
18763
+ hostId: null,
18764
+ harnessId: null,
18765
+ sourceProjectId: null,
18766
+ startedAt,
18767
+ endedAt: null,
18768
+ severity: null,
18769
+ priority: null,
18770
+ content: null,
18771
+ contentHash: null,
18772
+ attributes: JSON.stringify(input.attributes)
18773
+ })
18774
+ );
18552
18775
  }
18553
18776
  findById(id) {
18554
- return this.db.prepare("SELECT * FROM audit_events WHERE id = :id").get({ id });
18777
+ return getRow(this.db.prepare("SELECT * FROM audit_events WHERE id = :id"), {
18778
+ id
18779
+ });
18555
18780
  }
18556
18781
  // Read the `provider` snapshotted onto a session root's attributes.
18557
18782
  // The reconciler ensures the root, then reads provider back from it — SessionStart's
@@ -18561,14 +18786,8 @@ var SqliteAuditEventsRepository = class {
18561
18786
  sessionProvider(sessionId) {
18562
18787
  const row = this.findById(sessionId);
18563
18788
  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
- }
18789
+ const provider = parseJsonObject(row.attributes)?.provider;
18790
+ if (typeof provider === "string") return provider;
18572
18791
  return void 0;
18573
18792
  }
18574
18793
  // Every `llm_call` leaf's session id + raw attribute bag, for the read-time token
@@ -18577,11 +18796,13 @@ var SqliteAuditEventsRepository = class {
18577
18796
  // is the leaf's session (the reconciler sets parent_id = root_session_id = sessionId);
18578
18797
  // rows whose attributes blob is NULL are skipped (nothing to roll up).
18579
18798
  llmCallLeaves() {
18580
- return this.db.prepare(
18581
- `SELECT root_session_id AS sessionId, attributes
18799
+ return allRows(
18800
+ this.db.prepare(
18801
+ `SELECT root_session_id AS sessionId, attributes
18582
18802
  FROM audit_events
18583
18803
  WHERE event_type = 'llm_call' AND attributes IS NOT NULL`
18584
- ).all();
18804
+ )
18805
+ );
18585
18806
  }
18586
18807
  };
18587
18808
 
@@ -18600,16 +18821,29 @@ var SqliteClassifiedDataRepository = class {
18600
18821
  upsert(input) {
18601
18822
  const id = classifiedDataId(input.class);
18602
18823
  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
- });
18824
+ this.insertStmt.run(
18825
+ bindParams({
18826
+ id: row.id,
18827
+ class: row.class,
18828
+ label: row.label,
18829
+ attributes: row.attributes
18830
+ })
18831
+ );
18609
18832
  return id;
18610
18833
  }
18611
18834
  };
18612
18835
 
18836
+ // ../../packages/persistence/src/repositories/config-scan.ts
18837
+ function latestConfigScan(db) {
18838
+ return getRow(
18839
+ db.prepare(
18840
+ `SELECT id, started_at, attributes FROM audit_events
18841
+ WHERE event_type = 'config_scan'
18842
+ ORDER BY started_at DESC, id DESC LIMIT 1`
18843
+ )
18844
+ );
18845
+ }
18846
+
18613
18847
  // ../../packages/persistence/src/repositories/config-inventory.ts
18614
18848
  var SqliteConfigInventoryRepository = class {
18615
18849
  constructor(db) {
@@ -18617,7 +18851,7 @@ var SqliteConfigInventoryRepository = class {
18617
18851
  }
18618
18852
  db;
18619
18853
  report() {
18620
- const scan2 = this.latestScan();
18854
+ const scan2 = latestConfigScan(this.db);
18621
18855
  if (!scan2) {
18622
18856
  return {
18623
18857
  scannedAt: null,
@@ -18628,17 +18862,23 @@ var SqliteConfigInventoryRepository = class {
18628
18862
  topics: []
18629
18863
  };
18630
18864
  }
18631
- const rows = this.db.prepare(
18632
- `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18865
+ const rows = allRows(
18866
+ this.db.prepare(
18867
+ `SELECT id, object_type AS objectType, title, location, attributes FROM inventory
18633
18868
  WHERE object_type IN ('skill', 'hook', 'mcp_server', 'config_file') AND last_seen >= :startedAt
18634
18869
  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
18870
+ ),
18871
+ { startedAt: scan2.started_at }
18872
+ );
18873
+ const findings = allRows(
18874
+ this.db.prepare(
18875
+ `SELECT f.masked_match AS maskedMatch, d.rule_id AS ruleId, d.name AS name
18638
18876
  FROM inspection_findings f
18639
18877
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
18640
18878
  WHERE f.audit_event_id = :scanId`
18641
- ).all({ scanId: scan2.id });
18879
+ ),
18880
+ { scanId: scan2.id }
18881
+ );
18642
18882
  const skills = [];
18643
18883
  const hooks = [];
18644
18884
  const mcpServers = [];
@@ -18667,7 +18907,9 @@ var SqliteConfigInventoryRepository = class {
18667
18907
  // schema note); an override whose asset is gone simply never matches. A row
18668
18908
  // with an out-of-vocabulary trust value is ignored rather than guessed at.
18669
18909
  trustOverrides() {
18670
- const rows = this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override").all();
18910
+ const rows = allRows(
18911
+ this.db.prepare("SELECT asset_id AS assetId, trust FROM mcp_trust_override")
18912
+ );
18671
18913
  const map2 = /* @__PURE__ */ new Map();
18672
18914
  for (const row of rows) {
18673
18915
  if (row.trust === "known-good" || row.trust === "risky" || row.trust === "unapproved") {
@@ -18676,13 +18918,6 @@ var SqliteConfigInventoryRepository = class {
18676
18918
  }
18677
18919
  return map2;
18678
18920
  }
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
18921
  };
18687
18922
  function toSkillItem(row, bag) {
18688
18923
  const item = {
@@ -18793,22 +19028,11 @@ function buildTopics(skills, hooks, mcpServers, configFiles, scanAttributes) {
18793
19028
  return topics;
18794
19029
  }
18795
19030
  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
- }
19031
+ const errors = parseJsonObject(attributes)?.errors;
19032
+ return typeof errors === "number" ? errors : 0;
18804
19033
  }
18805
19034
  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;
19035
+ return parseJsonObject(raw);
18812
19036
  }
18813
19037
  function str(value) {
18814
19038
  return typeof value === "string" ? value : void 0;
@@ -18817,12 +19041,7 @@ function str(value) {
18817
19041
  // ../../packages/persistence/src/repositories/detections.ts
18818
19042
  var DAY_MS2 = 864e5;
18819
19043
  function parseRules(rulesJson) {
18820
- let raw;
18821
- try {
18822
- raw = JSON.parse(rulesJson);
18823
- } catch {
18824
- return [];
18825
- }
19044
+ const raw = safeJson(rulesJson, []);
18826
19045
  if (!Array.isArray(raw)) return [];
18827
19046
  const rules = [];
18828
19047
  for (const entry of raw) {
@@ -18841,11 +19060,13 @@ var SqliteDetectionsRepository = class {
18841
19060
  db;
18842
19061
  now;
18843
19062
  listDetections(query) {
18844
- const rows = this.db.prepare(
18845
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19063
+ const rows = allRows(
19064
+ this.db.prepare(
19065
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18846
19066
  rules_json AS rulesJson
18847
19067
  FROM installed_packs`
18848
- ).all();
19068
+ )
19069
+ );
18849
19070
  const available = this.availableByPack();
18850
19071
  const summaries = rows.map((r) => {
18851
19072
  const latest = available.get(`${r.namespace}/${r.packId}`);
@@ -18854,7 +19075,7 @@ var SqliteDetectionsRepository = class {
18854
19075
  packId: r.packId,
18855
19076
  version: r.version,
18856
19077
  name: r.name,
18857
- enabled: r.enabled === 1,
19078
+ enabled: intToBool(r.enabled),
18858
19079
  // Count rules in JS via the tolerant parse rather than SQL json_array_length,
18859
19080
  // which THROWS "malformed JSON" on a corrupt/foreign rules_json and would
18860
19081
  // crash the whole list. This also keeps ruleCount identical to the detail
@@ -18871,19 +19092,23 @@ var SqliteDetectionsRepository = class {
18871
19092
  // available_packs keyed by the "namespace/packId" slug (one read per list /
18872
19093
  // detail call; the table is a handful of rows).
18873
19094
  availableByPack() {
18874
- const rows = this.db.prepare(
18875
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
19095
+ const rows = allRows(
19096
+ this.db.prepare(
19097
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson
18876
19098
  FROM available_packs`
18877
- ).all();
19099
+ )
19100
+ );
18878
19101
  return new Map(rows.map((r) => [`${r.namespace}/${r.packId}`, r]));
18879
19102
  }
18880
19103
  getDetectionStats() {
18881
- const rows = this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs").all();
19104
+ const rows = allRows(
19105
+ this.db.prepare("SELECT enabled, rules_json AS rulesJson FROM installed_packs")
19106
+ );
18882
19107
  let rules = 0;
18883
19108
  let active = 0;
18884
19109
  const ruleIds = /* @__PURE__ */ new Set();
18885
19110
  for (const r of rows) {
18886
- if (r.enabled === 1) active += 1;
19111
+ if (intToBool(r.enabled)) active += 1;
18887
19112
  const parsed = parseRules(r.rulesJson);
18888
19113
  rules += parsed.length;
18889
19114
  for (const rule of parsed) {
@@ -18901,12 +19126,15 @@ var SqliteDetectionsRepository = class {
18901
19126
  const parts = splitDetectionId(id);
18902
19127
  if (!parts) return Promise.resolve(null);
18903
19128
  const { namespace, packId } = parts;
18904
- const row = this.db.prepare(
18905
- `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
19129
+ const row = getRow(
19130
+ this.db.prepare(
19131
+ `SELECT namespace, pack_id AS packId, version, name, enabled, policy_id AS policyId,
18906
19132
  rules_json AS rulesJson, updated_at AS updatedAt
18907
19133
  FROM installed_packs
18908
19134
  WHERE namespace = ? AND pack_id = ?`
18909
- ).get(namespace, packId);
19135
+ ),
19136
+ [namespace, packId]
19137
+ );
18910
19138
  if (!row) return Promise.resolve(null);
18911
19139
  const rules = parseRules(row.rulesJson);
18912
19140
  const ruleIds = rules.map((r) => r.id).filter((id2) => typeof id2 === "string");
@@ -18924,7 +19152,7 @@ var SqliteDetectionsRepository = class {
18924
19152
  packId: row.packId,
18925
19153
  version: row.version,
18926
19154
  name: row.name,
18927
- enabled: row.enabled === 1,
19155
+ enabled: intToBool(row.enabled),
18928
19156
  rules,
18929
19157
  updatedAt: new Date(row.updatedAt),
18930
19158
  policyId: row.policyId
@@ -18939,13 +19167,14 @@ var SqliteDetectionsRepository = class {
18939
19167
  countFindingsLast30d(ruleIds) {
18940
19168
  if (ruleIds.length === 0) return 0;
18941
19169
  const since = this.now() - 30 * DAY_MS2;
18942
- const placeholders2 = ruleIds.map(() => "?").join(", ");
18943
- const row = this.db.prepare(
18944
- `SELECT count(*) AS c
19170
+ const inClause = placeholders(ruleIds.length);
19171
+ return countScalar(
19172
+ this.db,
19173
+ `SELECT count(*) AS n
18945
19174
  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;
19175
+ WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19176
+ [since, ...ruleIds]
19177
+ );
18949
19178
  }
18950
19179
  };
18951
19180
 
@@ -18962,16 +19191,17 @@ var SqliteEventsRepository = class {
18962
19191
  insertStmt;
18963
19192
  insertEvent(event) {
18964
19193
  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
- });
19194
+ this.insertStmt.run(
19195
+ bindParams({
19196
+ id: row.id,
19197
+ sourceTool: row.sourceTool,
19198
+ kind: row.kind,
19199
+ occurredAt: row.occurredAt,
19200
+ contentHash: row.contentHash,
19201
+ content: row.content,
19202
+ metadata: row.metadata
19203
+ })
19204
+ );
18975
19205
  }
18976
19206
  // Every recorded event's content hash — the historical backfill loads this once
18977
19207
  // to skip transcript messages it has already stored, so re-running the scan
@@ -18979,13 +19209,23 @@ var SqliteEventsRepository = class {
18979
19209
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
18980
19210
  // async EventsReadPort contract.
18981
19211
  contentHashes() {
18982
- const rows = this.db.prepare("SELECT content_hash FROM events").all();
19212
+ const rows = allRows(
19213
+ this.db.prepare("SELECT content_hash FROM events")
19214
+ );
18983
19215
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
18984
19216
  }
18985
19217
  };
18986
19218
 
18987
19219
  // ../../packages/persistence/src/repositories/exceptions.ts
18988
19220
  import { randomUUID } from "crypto";
19221
+
19222
+ // ../../packages/persistence/src/internal/sqlite-errors.ts
19223
+ var SQLITE_CONSTRAINT_UNIQUE = 2067;
19224
+ function isUniqueConstraintError(err) {
19225
+ return err instanceof Error && (err.errcode === SQLITE_CONSTRAINT_UNIQUE || err.message.includes("UNIQUE constraint failed"));
19226
+ }
19227
+
19228
+ // ../../packages/persistence/src/repositories/exceptions.ts
18989
19229
  var BLOCKED_DETECTIONS_TTL_MS = 30 * 60 * 1e3;
18990
19230
  var BLOCKED_DETECTIONS_RETENTION_MS = 24 * 60 * 60 * 1e3;
18991
19231
  var DuplicateActiveExceptionError = class extends Error {
@@ -19006,10 +19246,6 @@ var AmbiguousExceptionIdError = class extends Error {
19006
19246
  this.name = "AmbiguousExceptionIdError";
19007
19247
  }
19008
19248
  };
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
19249
  var ACTIVE_PREDICATE = `revoked_at IS NULL
19014
19250
  AND (expires_at IS NULL OR expires_at > :now)
19015
19251
  AND (max_uses IS NULL OR use_count < max_uses)`;
@@ -19065,10 +19301,11 @@ var SqliteExceptionsRepository = class {
19065
19301
  this.insertExceptionRow(id, input, now);
19066
19302
  } catch (err) {
19067
19303
  if (!isUniqueConstraintError(err)) throw err;
19068
- this.db.exec("BEGIN IMMEDIATE");
19069
- try {
19070
- const superseded = this.db.prepare(
19071
- `UPDATE exceptions
19304
+ withTransaction(
19305
+ this.db,
19306
+ () => {
19307
+ const superseded = this.db.prepare(
19308
+ `UPDATE exceptions
19072
19309
  SET revoked_at = :now, revoked_by = :revokedBy,
19073
19310
  revoke_reason = 'superseded by a new grant for the same value',
19074
19311
  updated_at = :now
@@ -19076,24 +19313,27 @@ var SqliteExceptionsRepository = class {
19076
19313
  AND key_version = :keyVersion AND revoked_at IS NULL
19077
19314
  AND ((expires_at IS NOT NULL AND expires_at <= :now)
19078
19315
  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
- }
19316
+ ).run({
19317
+ now,
19318
+ revokedBy: input.createdBy,
19319
+ ruleId: input.ruleId,
19320
+ valueFingerprint: input.valueFingerprint,
19321
+ keyVersion: input.keyVersion
19322
+ });
19323
+ if (Number(superseded.changes) !== 1) {
19324
+ throw new DuplicateActiveExceptionError(input.ruleId);
19325
+ }
19326
+ this.insertExceptionRow(id, input, now);
19327
+ },
19328
+ "IMMEDIATE"
19329
+ );
19330
+ }
19331
+ const row = getRow(this.db.prepare("SELECT * FROM exceptions WHERE id = :id"), {
19332
+ id
19333
+ });
19334
+ if (row === void 0) {
19335
+ throw new Error("exception row not found immediately after insert");
19095
19336
  }
19096
- const row = this.db.prepare("SELECT * FROM exceptions WHERE id = :id").get({ id });
19097
19337
  return parseExceptionRow(row);
19098
19338
  }
19099
19339
  insertExceptionRow(id, input, now) {
@@ -19131,14 +19371,11 @@ var SqliteExceptionsRepository = class {
19131
19371
  */
19132
19372
  list(opts) {
19133
19373
  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
- }
19374
+ const rows = allRows(
19375
+ this.db.prepare(`SELECT * FROM exceptions ${where} ORDER BY created_at DESC, rowid DESC`),
19376
+ opts?.includeTerminal ? {} : { now: Date.now() }
19377
+ );
19378
+ const exceptions = mapRowsTolerant(rows, parseExceptionRow);
19142
19379
  return Promise.resolve(exceptions);
19143
19380
  }
19144
19381
  /**
@@ -19148,7 +19385,12 @@ var SqliteExceptionsRepository = class {
19148
19385
  */
19149
19386
  getByIdPrefix(prefix) {
19150
19387
  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)}%` });
19388
+ const rows = allRows(
19389
+ this.db.prepare(
19390
+ String.raw`SELECT * FROM exceptions WHERE id LIKE :pattern ESCAPE '\' LIMIT 2`
19391
+ ),
19392
+ { pattern: `${escapeLikePattern(prefix)}%` }
19393
+ );
19152
19394
  if (rows.length > 1) {
19153
19395
  return Promise.reject(new AmbiguousExceptionIdError(prefix));
19154
19396
  }
@@ -19190,30 +19432,27 @@ var SqliteExceptionsRepository = class {
19190
19432
  * a different (rotated-away) key never match, so they are excluded at read.
19191
19433
  */
19192
19434
  activeBundleEntries(keyVersion, now = Date.now()) {
19193
- const rows = this.db.prepare(
19194
- `SELECT * FROM exceptions
19435
+ const rows = allRows(
19436
+ this.db.prepare(
19437
+ `SELECT * FROM exceptions
19195
19438
  WHERE key_version = :keyVersion AND ${ACTIVE_PREDICATE}
19196
19439
  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
- }
19440
+ ),
19441
+ { keyVersion, now }
19442
+ );
19443
+ const entries = mapRowsTolerant(rows, (row) => {
19444
+ const conditions = row.conditions === null ? null : JSON.parse(row.conditions);
19445
+ return ExceptionBundleEntry.parse({
19446
+ id: row.id,
19447
+ ruleId: row.rule_id,
19448
+ valueFingerprint: row.value_fingerprint,
19449
+ keyVersion: row.key_version,
19450
+ expiresAt: row.expires_at === null ? null : epochMillisToIso(row.expires_at),
19451
+ maxUses: row.max_uses,
19452
+ useCount: row.use_count,
19453
+ conditions
19454
+ });
19455
+ });
19217
19456
  return Promise.resolve(entries);
19218
19457
  }
19219
19458
  /**
@@ -19240,11 +19479,14 @@ var SqliteExceptionsRepository = class {
19240
19479
  }
19241
19480
  /** Blocked detections within the window (default: the 30-minute TTL), newest-first. */
19242
19481
  recentBlocked(windowMs = BLOCKED_DETECTIONS_TTL_MS) {
19243
- const rows = this.db.prepare(
19244
- `SELECT * FROM blocked_detections
19482
+ const rows = allRows(
19483
+ this.db.prepare(
19484
+ `SELECT * FROM blocked_detections
19245
19485
  WHERE blocked_at > :cutoff
19246
19486
  ORDER BY blocked_at DESC, rowid DESC`
19247
- ).all({ cutoff: Date.now() - windowMs });
19487
+ ),
19488
+ { cutoff: Date.now() - windowMs }
19489
+ );
19248
19490
  return Promise.resolve(
19249
19491
  rows.map((row) => ({
19250
19492
  reference: row.reference,
@@ -19324,7 +19566,12 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
19324
19566
  )`;
19325
19567
 
19326
19568
  // ../../packages/persistence/src/repositories/findings.ts
19327
- var GROUP_ROW_CAP = 2e3;
19569
+ var PREVIEW_INSTANCES_PER_GROUP = 200;
19570
+ var CONCAT_SEP = ",";
19571
+ var TUPLE_SEP = "|";
19572
+ function splitConcat(value) {
19573
+ return value === null || value === "" ? [] : value.split(CONCAT_SEP);
19574
+ }
19328
19575
  function deriveInstanceStatus(row) {
19329
19576
  return deriveFindingStatus({
19330
19577
  kind: row.kind,
@@ -19392,13 +19639,16 @@ var SqliteFindingsRepository = class {
19392
19639
  }
19393
19640
  recentFindings(opts) {
19394
19641
  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,
19642
+ const rows = allRows(
19643
+ this.db.prepare(
19644
+ `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19397
19645
  f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19398
19646
  FROM findings f JOIN events e ON e.id = f.event_id
19399
19647
  ORDER BY e.occurred_at DESC, f.rowid DESC
19400
19648
  LIMIT :limit`
19401
- ).all({ limit });
19649
+ ),
19650
+ { limit }
19651
+ );
19402
19652
  return Promise.resolve(
19403
19653
  rows.map((r) => ({
19404
19654
  id: r.id,
@@ -19421,20 +19671,47 @@ var SqliteFindingsRepository = class {
19421
19671
  * applies the requested filters, and sorts by severity then recency. Filtering
19422
19672
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19423
19673
  * reflect the full filtered set; `items` is the requested
19424
- * page (default 100); no cursor (nextCursor is always null).
19674
+ * page (default 50); no cursor (nextCursor is always null).
19675
+ *
19676
+ * Two reads, neither of which materializes a row per finding:
19677
+ * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
19678
+ * the group and the filters need (count, providers, actions, statuses,
19679
+ * latest, search text);
19680
+ * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
19681
+ * populate `instances` for the table's expanded rows.
19682
+ * The aggregates carry raw DB values and are translated by the same
19683
+ * @akasecurity/schema mappers the row path uses, so no enum mapping or status
19684
+ * rule is ever restated in SQL.
19425
19685
  */
19426
19686
  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 });
19687
+ const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "");
19688
+ const rows = allRows(
19689
+ this.db.prepare(
19690
+ `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
19691
+ occurred_at, source_tool, repo, file, kind, finding_key, latest_status
19692
+ FROM (
19693
+ SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
19694
+ f.severity AS severity, f.masked_match AS masked_match,
19695
+ f.action_taken AS action_taken, f.confidence AS confidence,
19696
+ e.occurred_at AS occurred_at, e.source_tool AS source_tool,
19697
+ json_extract(e.metadata, '$.repo') AS repo,
19698
+ json_extract(e.metadata, '$.filePath') AS file,
19699
+ e.kind AS kind, f.finding_key AS finding_key,
19700
+ latest.status AS latest_status,
19701
+ ROW_NUMBER() OVER (
19702
+ PARTITION BY f.rule_id
19703
+ ORDER BY e.occurred_at DESC, f.id DESC
19704
+ ) AS rn
19705
+ FROM findings f
19706
+ JOIN events e ON e.id = f.event_id
19707
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19708
+ ON latest.finding_key = f.finding_key
19709
+ )
19710
+ WHERE rn <= :cap
19711
+ ORDER BY occurred_at DESC, id DESC`
19712
+ ),
19713
+ { cap: PREVIEW_INSTANCES_PER_GROUP }
19714
+ );
19438
19715
  const groupable = rows.map((r) => ({
19439
19716
  id: r.id,
19440
19717
  ruleId: r.rule_id,
@@ -19449,7 +19726,7 @@ var SqliteFindingsRepository = class {
19449
19726
  file: r.file ?? "",
19450
19727
  status: deriveInstanceStatus(r)
19451
19728
  }));
19452
- const allGroups = buildFindingGroups(groupable);
19729
+ const allGroups = buildFindingGroups(groupable, { aggregates });
19453
19730
  const filterOpts = {
19454
19731
  severity: query.severity,
19455
19732
  providers: query.provider,
@@ -19467,42 +19744,122 @@ var SqliteFindingsRepository = class {
19467
19744
  const items = sorted.slice(0, limit);
19468
19745
  return Promise.resolve({ totals, facets, items, nextCursor: null });
19469
19746
  }
19747
+ /**
19748
+ * One row per rule_id, folding EVERY instance of the group into the values
19749
+ * buildFindingGroups cannot recover from a preview. Bounded by the number of
19750
+ * distinct rule_ids (the installed packs' rules), not by the store's size.
19751
+ *
19752
+ * The per-instance sets ride back as group_concat lists of RAW DB values —
19753
+ * source_tool, action_taken, and the (kind, has-key, latest-status) triples
19754
+ * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
19755
+ * status keeps the classifier itself in @akasecurity/schema, where
19756
+ * severitySummary's SQL and this query can't drift apart on what 'resolved'
19757
+ * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
19758
+ * a group's row stays small however many findings it holds.
19759
+ *
19760
+ * `withSearchText` is the exception, and the one column here that does NOT
19761
+ * stay small: the group's distinct repos/filePaths, whose size tracks how many
19762
+ * distinct paths a rule fired across — for a rule hitting mostly-unique paths
19763
+ * that is a string proportional to the store (~8MB over 200k distinct paths,
19764
+ * and buildHaystack lowercases a second copy). It buys `q` the ability to
19765
+ * match an instance outside the preview, which searching the preview alone
19766
+ * would silently lose, so it is fetched only when the request actually
19767
+ * carries a `q`.
19768
+ */
19769
+ groupAggregates(withSearchText) {
19770
+ const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
19771
+ group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files` : `, NULL AS repos, NULL AS files`;
19772
+ const rows = this.db.prepare(
19773
+ `SELECT f.rule_id AS rule_id,
19774
+ count(*) AS instance_count,
19775
+ max(e.occurred_at) AS latest_at,
19776
+ group_concat(DISTINCT e.source_tool) AS source_tools,
19777
+ group_concat(DISTINCT f.action_taken) AS actions_taken,
19778
+ group_concat(DISTINCT (
19779
+ e.kind || '${TUPLE_SEP}' ||
19780
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
19781
+ coalesce(latest.status, '')
19782
+ )) AS status_inputs
19783
+ ${searchTextColumns}
19784
+ FROM findings f
19785
+ JOIN events e ON e.id = f.event_id
19786
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19787
+ ON latest.finding_key = f.finding_key
19788
+ GROUP BY f.rule_id`
19789
+ ).all();
19790
+ return new Map(
19791
+ rows.map((r) => [
19792
+ r.rule_id,
19793
+ {
19794
+ instanceCount: r.instance_count,
19795
+ sourceTools: splitConcat(r.source_tools),
19796
+ actionsTaken: splitConcat(r.actions_taken),
19797
+ statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
19798
+ const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
19799
+ return {
19800
+ // deriveFindingStatus only distinguishes null from non-null here,
19801
+ // so the marker stands in for the key itself (never rendered).
19802
+ kind,
19803
+ findingKey: keyMarker === "" ? null : keyMarker,
19804
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus
19805
+ };
19806
+ }),
19807
+ latestDetectedAt: epochMillisToIso(r.latest_at),
19808
+ // Free text only — joined and substring-matched, so group_concat's
19809
+ // commas need no unpicking (a repo/path containing one still matches).
19810
+ // Left undefined (not '') when unfetched, so buildFindingGroups can
19811
+ // tell "no q this request" from "a group with no repo/file at all"
19812
+ // and skip priming a haystack nothing will read.
19813
+ ...withSearchText ? { searchText: [r.repos ?? "", r.files ?? ""].filter((s) => s !== "").join(" ") } : {}
19814
+ }
19815
+ ])
19816
+ );
19817
+ }
19470
19818
  healthSummary() {
19471
- const total = this.db.prepare("SELECT count(*) AS c FROM findings").get().c;
19819
+ const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
19472
19820
  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();
19821
+ const grouped = allRows(
19822
+ this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
19823
+ );
19474
19824
  for (const row of grouped) {
19475
19825
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
19476
19826
  }
19477
19827
  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
19828
+ const sevRows = allRows(
19829
+ this.db.prepare(
19830
+ `SELECT f.severity AS severity, count(*) AS c
19480
19831
  FROM findings f
19481
19832
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
19482
19833
  ON latest.finding_key = f.finding_key
19483
19834
  WHERE latest.status IS NULL OR latest.status != 'resolved'
19484
19835
  GROUP BY f.severity`
19485
- ).all();
19836
+ )
19837
+ );
19486
19838
  for (const row of sevRows) {
19487
19839
  if (row.severity in bySeverity) bySeverity[row.severity] = row.c;
19488
19840
  }
19489
19841
  const categories = ENFORCEABLE_CATEGORIES;
19490
- const enabledRows = this.db.prepare(
19491
- `SELECT DISTINCT json_extract(target, '$.category') AS category
19842
+ const enabledRows = allRows(
19843
+ this.db.prepare(
19844
+ `SELECT DISTINCT json_extract(target, '$.category') AS category
19492
19845
  FROM policies WHERE enabled = 1 AND json_extract(target, '$.category') IS NOT NULL`
19493
- ).all();
19846
+ )
19847
+ );
19494
19848
  const enabled = new Set(enabledRows.map((r) => r.category));
19495
19849
  const coverage = categories.length === 0 ? 0 : categories.filter((c) => enabled.has(c)).length / categories.length;
19496
19850
  return Promise.resolve({ findings: total, byAction, bySeverity, coverage });
19497
19851
  }
19498
19852
  activityByDay(days = 7) {
19499
19853
  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
19854
+ const rows = allRows(
19855
+ this.db.prepare(
19856
+ `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
19502
19857
  FROM findings f JOIN events e ON e.id = f.event_id
19503
19858
  WHERE e.occurred_at >= :since
19504
19859
  GROUP BY day, f.action_taken`
19505
- ).all({ since });
19860
+ ),
19861
+ { since }
19862
+ );
19506
19863
  const buckets = /* @__PURE__ */ new Map();
19507
19864
  for (let i = 0; i < days; i++) {
19508
19865
  const day = isoDay(since + i * DAY_MS3);
@@ -19574,17 +19931,19 @@ var SqliteInspectionFindingsRepository = class {
19574
19931
  insertStmt;
19575
19932
  insertFinding(input) {
19576
19933
  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
- });
19934
+ this.insertStmt.run(
19935
+ bindParams({
19936
+ id: row.id,
19937
+ auditEventId: row.auditEventId,
19938
+ inspectionDefinitionId: row.inspectionDefinitionId,
19939
+ classifiedDataId: row.classifiedDataId,
19940
+ spanStart: row.spanStart,
19941
+ spanEnd: row.spanEnd,
19942
+ maskedMatch: row.maskedMatch,
19943
+ actionTaken: row.actionTaken,
19944
+ confidence: row.confidence
19945
+ })
19946
+ );
19588
19947
  }
19589
19948
  };
19590
19949
 
@@ -19660,12 +20019,7 @@ function isMirrorDowngrade(incoming, stored) {
19660
20019
  }
19661
20020
  function ruleIdsOf(rulesJson) {
19662
20021
  const ids = /* @__PURE__ */ new Set();
19663
- let raw;
19664
- try {
19665
- raw = JSON.parse(rulesJson);
19666
- } catch {
19667
- return ids;
19668
- }
20022
+ const raw = safeJson(rulesJson, []);
19669
20023
  if (!Array.isArray(raw)) return ids;
19670
20024
  for (const entry of raw) {
19671
20025
  if (entry && typeof entry === "object") {
@@ -19738,47 +20092,48 @@ var SqliteInstalledPacksRepository = class {
19738
20092
  }));
19739
20093
  if (this.storedSignature() === inventorySignature(rows)) return;
19740
20094
  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,
20095
+ withTransaction(
20096
+ this.db,
20097
+ () => {
20098
+ const mirror = this.mirrorState();
20099
+ let behind = false;
20100
+ for (const row of rows) {
20101
+ const params = {
19759
20102
  id: randomUUID2(),
19760
- recordedBy: meta3?.recordedBy ?? null
19761
- });
19762
- } else {
19763
- behind = true;
19764
- }
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
- }
20103
+ namespace: row.namespace,
20104
+ packId: row.packId,
20105
+ version: row.version,
20106
+ name: row.name,
20107
+ rulesJson: row.rulesJson,
20108
+ now
20109
+ };
20110
+ const stored = mirror.get(`${row.namespace}/${row.packId}`);
20111
+ if (stored === void 0 || !isMirrorDowngrade(row, stored)) {
20112
+ this.upsertAvailableStmt.run({
20113
+ ...params,
20114
+ id: randomUUID2(),
20115
+ recordedBy: meta3?.recordedBy ?? null
20116
+ });
20117
+ } else {
20118
+ behind = true;
20119
+ }
20120
+ this.insertMissingStmt.run(params);
20121
+ }
20122
+ if (!behind) this.pruneAvailable(rows.map((r) => `${r.namespace}/${r.packId}`));
20123
+ },
20124
+ "IMMEDIATE"
20125
+ );
19773
20126
  } catch {
19774
20127
  }
19775
20128
  }
19776
20129
  // The mirror's current (namespace/packId → {version, ruleIds}) map — the
19777
20130
  // input to the downgrade guard. Read INSIDE the write transaction.
19778
20131
  mirrorState() {
19779
- const rows = this.db.prepare(
19780
- `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
19781
- ).all();
20132
+ const rows = allRows(
20133
+ this.db.prepare(
20134
+ `SELECT namespace, pack_id AS packId, version, rules_json AS rulesJson FROM available_packs`
20135
+ )
20136
+ );
19782
20137
  return new Map(
19783
20138
  rows.map((r) => [
19784
20139
  `${r.namespace}/${r.packId}`,
@@ -19790,7 +20145,9 @@ var SqliteInstalledPacksRepository = class {
19790
20145
  // (keys joined with '/', matching the detection id slug encoding — packId may
19791
20146
  // itself contain '/', but namespace may not, so the join is unambiguous).
19792
20147
  pruneAvailable(keep) {
19793
- const rows = this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`).all();
20148
+ const rows = allRows(
20149
+ this.db.prepare(`SELECT namespace, pack_id AS packId FROM available_packs`)
20150
+ );
19794
20151
  const keepSet = new Set(keep);
19795
20152
  const del = this.db.prepare(`DELETE FROM available_packs WHERE namespace = ? AND pack_id = ?`);
19796
20153
  for (const r of rows) {
@@ -19817,11 +20174,13 @@ var SqliteInstalledPacksRepository = class {
19817
20174
  if (this.db.isTransaction) {
19818
20175
  throw new Error("applyUpdate must not be called inside an open transaction");
19819
20176
  }
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
20177
+ let changed = false;
20178
+ withTransaction(
20179
+ this.db,
20180
+ () => {
20181
+ this.db.exec("UPDATE _pack_write_gate SET open = 1 WHERE id = 1");
20182
+ const res = this.db.prepare(
20183
+ `UPDATE installed_packs SET
19825
20184
  version = (SELECT a.version FROM available_packs a
19826
20185
  WHERE a.namespace = :namespace AND a.pack_id = :packId),
19827
20186
  name = (SELECT a.name FROM available_packs a
@@ -19832,17 +20191,13 @@ var SqliteInstalledPacksRepository = class {
19832
20191
  WHERE namespace = :namespace AND pack_id = :packId
19833
20192
  AND EXISTS (SELECT 1 FROM available_packs a
19834
20193
  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
- }
20194
+ ).run({ namespace, packId, now: Date.now() });
20195
+ this.db.exec("UPDATE _pack_write_gate SET open = 0 WHERE id = 1");
20196
+ changed = Number(res.changes) > 0;
20197
+ },
20198
+ "IMMEDIATE"
20199
+ );
20200
+ return changed;
19846
20201
  }
19847
20202
  /**
19848
20203
  * The scan-time ruleset: every rule under an ENABLED installed pack that
@@ -19856,9 +20211,11 @@ var SqliteInstalledPacksRepository = class {
19856
20211
  * JSON-level failure therefore counts as invalid.
19857
20212
  */
19858
20213
  installedRuleset() {
19859
- const rows = this.db.prepare(
19860
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
19861
- ).all();
20214
+ const rows = allRows(
20215
+ this.db.prepare(
20216
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20217
+ )
20218
+ );
19862
20219
  const out = {
19863
20220
  installedPacks: rows.length,
19864
20221
  enabledPacks: 0,
@@ -19867,7 +20224,7 @@ var SqliteInstalledPacksRepository = class {
19867
20224
  ruleActions: /* @__PURE__ */ new Map()
19868
20225
  };
19869
20226
  for (const row of rows) {
19870
- if (row.enabled !== 1) continue;
20227
+ if (!intToBool(row.enabled)) continue;
19871
20228
  out.enabledPacks += 1;
19872
20229
  const action = policyIdToAction(row.policyId);
19873
20230
  let raw;
@@ -19902,9 +20259,11 @@ var SqliteInstalledPacksRepository = class {
19902
20259
  * running max would mask a genuinely-newer parseable stamp.
19903
20260
  */
19904
20261
  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();
20262
+ const rows = allRows(
20263
+ this.db.prepare(
20264
+ `SELECT DISTINCT recorded_by AS recordedBy FROM available_packs WHERE recorded_by IS NOT NULL`
20265
+ )
20266
+ );
19908
20267
  let newest = null;
19909
20268
  for (const row of rows) {
19910
20269
  const at = row.recordedBy.lastIndexOf("@");
@@ -19919,13 +20278,15 @@ var SqliteInstalledPacksRepository = class {
19919
20278
  return newest;
19920
20279
  }
19921
20280
  counts() {
19922
- const row = this.db.prepare(
19923
- `SELECT count(*) AS packs,
20281
+ const row = getRow(
20282
+ this.db.prepare(
20283
+ `SELECT count(*) AS packs,
19924
20284
  coalesce(sum(json_array_length(rules_json)), 0) AS rules,
19925
20285
  coalesce(sum(enabled), 0) AS enabled
19926
20286
  FROM installed_packs`
19927
- ).get();
19928
- return Promise.resolve(row);
20287
+ )
20288
+ );
20289
+ return Promise.resolve(row ?? { packs: 0, rules: 0, enabled: 0 });
19929
20290
  }
19930
20291
  // ─── Policy-catalog reads ────────────────────────────────────────────────────
19931
20292
  // Back the Policies page's built-in catalog: how many
@@ -19938,26 +20299,29 @@ var SqliteInstalledPacksRepository = class {
19938
20299
  * attributed to Monitor, matching the Detections views.
19939
20300
  */
19940
20301
  countsByPolicyId() {
19941
- const rows = this.db.prepare(
19942
- `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS pid, count(*) AS n
20302
+ return countBy(
20303
+ this.db,
20304
+ `SELECT coalesce(policy_id, '${DEFAULT_POLICY_ID}') AS k, count(*) AS n
19943
20305
  FROM installed_packs
19944
- GROUP BY pid`
19945
- ).all();
19946
- return new Map(rows.map((r) => [r.pid, r.n]));
20306
+ GROUP BY k`
20307
+ );
19947
20308
  }
19948
20309
  /** The detections governed by a built-in policy — one UsedByItem per pack. */
19949
20310
  listByPolicyId(policyId) {
19950
- const rows = this.db.prepare(
19951
- `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
20311
+ const rows = allRows(
20312
+ this.db.prepare(
20313
+ `SELECT namespace, pack_id AS packId, name, enabled, rules_json AS rulesJson
19952
20314
  FROM installed_packs
19953
20315
  WHERE coalesce(policy_id, '${DEFAULT_POLICY_ID}') = ?
19954
20316
  ORDER BY name ASC`
19955
- ).all(policyId);
20317
+ ),
20318
+ [policyId]
20319
+ );
19956
20320
  return rows.map((r) => ({
19957
20321
  id: `${r.namespace}/${r.packId}`,
19958
20322
  name: r.name,
19959
20323
  ruleCount: parseRules(r.rulesJson).length,
19960
- enabled: r.enabled === 1
20324
+ enabled: intToBool(r.enabled)
19961
20325
  }));
19962
20326
  }
19963
20327
  // ─── Writes ────────────────────────────────────────────────────────────────
@@ -19986,14 +20350,14 @@ var SqliteInstalledPacksRepository = class {
19986
20350
  const res = this.db.prepare(
19987
20351
  `UPDATE installed_packs SET enabled = :enabled, updated_at = :now
19988
20352
  WHERE namespace = :namespace AND pack_id = :packId`
19989
- ).run({ enabled: enabled ? 1 : 0, now: Date.now(), namespace, packId });
20353
+ ).run({ enabled: boolToInt(enabled), now: Date.now(), namespace, packId });
19990
20354
  return Number(res.changes) > 0;
19991
20355
  }
19992
20356
  // Fingerprint of the recorded available mirror — compared against the
19993
20357
  // incoming inventory's signature to skip the write entirely when the running
19994
20358
  // binary's inventory hasn't changed since the last record.
19995
20359
  storedSignature() {
19996
- const rows = this.signatureStmt.all();
20360
+ const rows = allRows(this.signatureStmt);
19997
20361
  return inventorySignature(rows);
19998
20362
  }
19999
20363
  };
@@ -20021,42 +20385,48 @@ var SqliteInventoryRepository = class {
20021
20385
  upsert(input, now = Date.now()) {
20022
20386
  const id = inventoryId(input.objectType, input.identityKey);
20023
20387
  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
- });
20388
+ this.upsertStmt.run(
20389
+ bindParams({
20390
+ id: row.id,
20391
+ objectType: row.objectType,
20392
+ location: row.location,
20393
+ title: row.title,
20394
+ hostId: row.hostId,
20395
+ attributes: row.attributes,
20396
+ firstSeen: row.firstSeen,
20397
+ lastSeen: row.lastSeen
20398
+ })
20399
+ );
20034
20400
  return id;
20035
20401
  }
20036
20402
  // The full row, for round-trip assertions.
20037
20403
  findById(id) {
20038
- const row = this.db.prepare("SELECT * FROM inventory WHERE id = :id").get({ id });
20039
- return row;
20404
+ return getRow(this.db.prepare("SELECT * FROM inventory WHERE id = :id"), { id });
20040
20405
  }
20041
20406
  // Distinct titles for an object_type — a filter facet (e.g. hostnames),
20042
20407
  // served from the object_type index, never from audit_events.
20043
20408
  distinctTitles(objectType) {
20044
- const rows = this.db.prepare(
20045
- `SELECT DISTINCT title FROM inventory
20409
+ const rows = allRows(
20410
+ this.db.prepare(
20411
+ `SELECT DISTINCT title FROM inventory
20046
20412
  WHERE object_type = :objectType AND title IS NOT NULL
20047
20413
  ORDER BY title`
20048
- ).all({ objectType });
20414
+ ),
20415
+ { objectType }
20416
+ );
20049
20417
  return rows.map((r) => r.title);
20050
20418
  }
20051
20419
  // Distinct host os_version values — a facet served from an inventory index
20052
20420
  // over the generated column, never from the audit fact (confirm via EXPLAIN
20053
20421
  // QUERY PLAN).
20054
20422
  osVersions() {
20055
- const rows = this.db.prepare(
20056
- `SELECT DISTINCT os_version AS value FROM inventory
20423
+ const rows = allRows(
20424
+ this.db.prepare(
20425
+ `SELECT DISTINCT os_version AS value FROM inventory
20057
20426
  WHERE object_type = 'host' AND os_version IS NOT NULL
20058
20427
  ORDER BY value`
20059
- ).all();
20428
+ )
20429
+ );
20060
20430
  return rows.map((r) => r.value);
20061
20431
  }
20062
20432
  };
@@ -20076,14 +20446,6 @@ var EMPTY_PROJECT_AGG = {
20076
20446
  accessCounts: { open: 0, approved: 0, blocked: 0, total: 0 },
20077
20447
  findingsCount: 0
20078
20448
  };
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
20449
  function resolveHarnessId(attrs, row) {
20088
20450
  if (attrs.provider && VALID_HARNESS_IDS.has(attrs.provider)) {
20089
20451
  return attrs.provider;
@@ -20279,30 +20641,49 @@ var SqliteInventoryAssetsRepository = class {
20279
20641
  configRowsCache;
20280
20642
  // ─── stats ─────────────────────────────────────────────────────────────────
20281
20643
  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
20644
+ const typeCounts = countBy(
20645
+ this.db,
20646
+ "SELECT asset_type AS k, count(*) AS n FROM inventory_asset GROUP BY asset_type"
20647
+ );
20648
+ const byType = {
20649
+ project: 0,
20650
+ skill: typeCounts.get("skill") ?? 0,
20651
+ mcp: typeCounts.get("mcp") ?? 0,
20652
+ hook: typeCounts.get("hook") ?? 0,
20653
+ config: typeCounts.get("config") ?? 0
20654
+ };
20655
+ byType.project = countScalar(
20656
+ this.db,
20657
+ `SELECT count(*) AS n FROM source_project WHERE ${WORKTREE_CHECKOUT_FILTER}`
20658
+ );
20659
+ const mcpTrustCounts = countBy(
20660
+ this.db,
20661
+ `SELECT coalesce(o.trust, a.trust) AS k, count(*) AS n
20290
20662
  FROM inventory_asset a
20291
20663
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20292
20664
  WHERE a.asset_type = 'mcp' AND coalesce(o.trust, a.trust) IS NOT NULL
20293
20665
  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(
20666
+ );
20667
+ const mcpTrust = {
20668
+ "known-good": mcpTrustCounts.get("known-good") ?? 0,
20669
+ risky: mcpTrustCounts.get("risky") ?? 0,
20670
+ unapproved: mcpTrustCounts.get("unapproved") ?? 0
20671
+ };
20672
+ const harnesses = countScalar(
20673
+ this.db,
20298
20674
  `SELECT count(*) AS n FROM inventory
20299
20675
  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(
20676
+ AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`,
20677
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20678
+ );
20679
+ const flaggedAssets = countScalar(
20680
+ this.db,
20681
+ "SELECT count(*) AS n FROM inventory_asset WHERE flags_json <> '[]'"
20682
+ );
20683
+ const flaggedProjects = countScalar(
20684
+ this.db,
20304
20685
  `SELECT count(DISTINCT project_id) AS n FROM project_file WHERE findings_count > 0`
20305
- ).get().n;
20686
+ );
20306
20687
  const configRows = this.configAssetRows();
20307
20688
  for (const r of configRows) {
20308
20689
  byType[r.assetType] += 1;
@@ -20559,12 +20940,15 @@ var SqliteInventoryAssetsRepository = class {
20559
20940
  }
20560
20941
  // ─── raw fetchers ────────────────────────────────────────────────────────────
20561
20942
  fetchHarnessRows() {
20562
- return this.db.prepare(
20563
- `SELECT id, title, attributes, harness_version AS harnessVersion
20943
+ return allRows(
20944
+ this.db.prepare(
20945
+ `SELECT id, title, attributes, harness_version AS harnessVersion
20564
20946
  FROM inventory
20565
20947
  WHERE object_type = 'harness'
20566
20948
  AND (last_seen >= :liveSince OR json_extract(attributes, '$.provenance') = 'sample')`
20567
- ).all({ liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS });
20949
+ ),
20950
+ { liveSince: Date.now() - HARNESS_LIVENESS_WINDOW_MS }
20951
+ );
20568
20952
  }
20569
20953
  // Every harness's assets in ONE grouped query, keyed by harness inventory id —
20570
20954
  // replaces the per-harness-row query the listHarnesses loop used to make.
@@ -20574,12 +20958,13 @@ var SqliteInventoryAssetsRepository = class {
20574
20958
  const params = [...harnessInvIds];
20575
20959
  let where = `ha.harness_id IN (${placeholders(harnessInvIds.length)})`;
20576
20960
  if (q) {
20577
- const pat = `%${escapeLikePattern(q)}%`;
20578
- where += " AND (a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')";
20961
+ const pat = containsPattern(q);
20962
+ where += ` AND ${likeAny(["a.name", "a.sub"])}`;
20579
20963
  params.push(pat, pat);
20580
20964
  }
20581
- const rows = this.db.prepare(
20582
- `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20965
+ const rows = allRows(
20966
+ this.db.prepare(
20967
+ `SELECT ha.harness_id AS harnessInvId, a.id, a.asset_type AS assetType, a.name, a.sub,
20583
20968
  a.description, a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20584
20969
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20585
20970
  FROM harness_asset ha
@@ -20587,7 +20972,9 @@ var SqliteInventoryAssetsRepository = class {
20587
20972
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20588
20973
  WHERE ${where}
20589
20974
  ORDER BY a.name ASC`
20590
- ).all(...params);
20975
+ ),
20976
+ params
20977
+ );
20591
20978
  for (const raw of rows) {
20592
20979
  const harnessInvId = raw.harnessInvId;
20593
20980
  const [asset] = this.mapAssetRows([raw]);
@@ -20606,21 +20993,24 @@ var SqliteInventoryAssetsRepository = class {
20606
20993
  params.push(...types);
20607
20994
  }
20608
20995
  if (q) {
20609
- const pat = `%${escapeLikePattern(q)}%`;
20610
- conditions.push("(a.name LIKE ? ESCAPE '\\' OR a.sub LIKE ? ESCAPE '\\')");
20996
+ const pat = containsPattern(q);
20997
+ conditions.push(likeAny(["a.name", "a.sub"]));
20611
20998
  params.push(pat, pat);
20612
20999
  }
20613
21000
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
20614
21001
  const sampleRows = this.mapAssetRows(
20615
- this.db.prepare(
20616
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21002
+ allRows(
21003
+ this.db.prepare(
21004
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20617
21005
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20618
21006
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20619
21007
  FROM inventory_asset a
20620
21008
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20621
21009
  ${where}
20622
21010
  ORDER BY a.name ASC`
20623
- ).all(...params)
21011
+ ),
21012
+ params
21013
+ )
20624
21014
  );
20625
21015
  const configRows = this.configAssetRows(q).filter(
20626
21016
  (r) => !types || types.length === 0 || types.includes(r.assetType)
@@ -20629,14 +21019,17 @@ var SqliteInventoryAssetsRepository = class {
20629
21019
  }
20630
21020
  fetchAssetById(assetId) {
20631
21021
  const rows = this.mapAssetRows(
20632
- this.db.prepare(
20633
- `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
21022
+ allRows(
21023
+ this.db.prepare(
21024
+ `SELECT a.id, a.asset_type AS assetType, a.name, a.sub, a.description,
20634
21025
  a.flags_json AS flagsJson, a.meta_json AS metaJson, a.trust,
20635
21026
  a.tools_json AS toolsJson, coalesce(o.trust, a.trust) AS effectiveTrust
20636
21027
  FROM inventory_asset a
20637
21028
  LEFT JOIN mcp_trust_override o ON o.asset_id = a.id
20638
21029
  WHERE a.id = ?`
20639
- ).all(assetId)
21030
+ ),
21031
+ [assetId]
21032
+ )
20640
21033
  );
20641
21034
  return rows[0] ?? this.configAssetRows().find((r) => r.id === assetId) ?? null;
20642
21035
  }
@@ -20692,37 +21085,39 @@ var SqliteInventoryAssetsRepository = class {
20692
21085
  return rows;
20693
21086
  }
20694
21087
  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;
21088
+ return latestConfigScan(this.db)?.id ?? null;
20700
21089
  }
20701
21090
  fetchProjects(q) {
20702
21091
  let sql = `SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project
20703
21092
  WHERE ${WORKTREE_CHECKOUT_FILTER}`;
20704
21093
  const params = [];
20705
21094
  if (q) {
20706
- const pat = `%${escapeLikePattern(q)}%`;
20707
- sql += " AND (name LIKE ? ESCAPE '\\' OR url LIKE ? ESCAPE '\\')";
21095
+ const pat = containsPattern(q);
21096
+ sql += ` AND ${likeAny(["name", "url"])}`;
20708
21097
  params.push(pat, pat);
20709
21098
  }
20710
21099
  sql += " ORDER BY name ASC";
20711
- return this.db.prepare(sql).all(...params);
21100
+ return allRows(this.db.prepare(sql), params);
20712
21101
  }
20713
21102
  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;
21103
+ return getRow(
21104
+ this.db.prepare(
21105
+ "SELECT id, url, name, attributes, last_seen AS lastSeen FROM source_project WHERE id = ?"
21106
+ ),
21107
+ [projectId]
21108
+ ) ?? null;
20717
21109
  }
20718
21110
  // The referenced projects in ONE `id IN (…)` fetch, keyed by id.
20719
21111
  fetchProjectsByIds(projectIds) {
20720
21112
  const map2 = /* @__PURE__ */ new Map();
20721
21113
  if (projectIds.length === 0) return map2;
20722
- const rows = this.db.prepare(
20723
- `SELECT id, url, name, attributes, last_seen AS lastSeen
21114
+ const rows = allRows(
21115
+ this.db.prepare(
21116
+ `SELECT id, url, name, attributes, last_seen AS lastSeen
20724
21117
  FROM source_project WHERE id IN (${placeholders(projectIds.length)})`
20725
- ).all(...projectIds);
21118
+ ),
21119
+ projectIds
21120
+ );
20726
21121
  for (const r of rows) map2.set(r.id, r);
20727
21122
  return map2;
20728
21123
  }
@@ -20733,8 +21128,9 @@ var SqliteInventoryAssetsRepository = class {
20733
21128
  projectAggregates(projectIds) {
20734
21129
  const map2 = /* @__PURE__ */ new Map();
20735
21130
  if (projectIds.length === 0) return map2;
20736
- const rows = this.db.prepare(
20737
- `SELECT f.project_id AS projectId,
21131
+ const rows = allRows(
21132
+ this.db.prepare(
21133
+ `SELECT f.project_id AS projectId,
20738
21134
  coalesce(o.access, f.default_access) AS eff,
20739
21135
  count(*) AS n,
20740
21136
  coalesce(sum(f.findings_count), 0) AS findings
@@ -20742,7 +21138,9 @@ var SqliteInventoryAssetsRepository = class {
20742
21138
  LEFT JOIN file_access_override o ON o.project_id = f.project_id AND o.path = f.path
20743
21139
  WHERE f.project_id IN (${placeholders(projectIds.length)})
20744
21140
  GROUP BY f.project_id, eff`
20745
- ).all(...projectIds);
21141
+ ),
21142
+ projectIds
21143
+ );
20746
21144
  for (const r of rows) {
20747
21145
  let agg = map2.get(r.projectId);
20748
21146
  if (!agg) {
@@ -20780,37 +21178,52 @@ var SqliteInventoryAssetsRepository = class {
20780
21178
  fetchProjectFilesUnder(projectId, prefix) {
20781
21179
  if (prefix === "") {
20782
21180
  return this.mapFileRows(
20783
- this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")).all(projectId)
21181
+ allRows(
21182
+ this.db.prepare(this.fileSelect("f.project_id = ? ORDER BY f.path ASC")),
21183
+ [projectId]
21184
+ )
20784
21185
  );
20785
21186
  }
20786
21187
  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)}/%`)
21188
+ allRows(
21189
+ this.db.prepare(
21190
+ this.fileSelect("f.project_id = ? AND f.path LIKE ? ESCAPE '\\' ORDER BY f.path ASC")
21191
+ ),
21192
+ [projectId, `${escapeLikePattern(prefix)}/%`]
21193
+ )
20790
21194
  );
20791
21195
  }
20792
21196
  fetchProjectFilesSearch(projectId, q) {
20793
- const pat = `%${escapeLikePattern(q)}%`;
21197
+ const pat = containsPattern(q);
20794
21198
  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)
21199
+ allRows(
21200
+ this.db.prepare(
21201
+ this.fileSelect(
21202
+ "f.project_id = ? AND (f.path LIKE ? ESCAPE '\\' OR f.name LIKE ? ESCAPE '\\') ORDER BY f.path ASC"
21203
+ )
21204
+ ),
21205
+ [projectId, pat, pat]
21206
+ )
20800
21207
  );
20801
21208
  }
20802
21209
  fetchProjectFilesBlocked(projectId) {
20803
21210
  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)
21211
+ allRows(
21212
+ this.db.prepare(
21213
+ this.fileSelect(
21214
+ "f.project_id = ? AND coalesce(o.access, f.default_access) = 'blocked' AND f.blocked_at IS NOT NULL"
21215
+ )
21216
+ ),
21217
+ [projectId]
21218
+ )
20809
21219
  );
20810
21220
  }
20811
21221
  fetchProjectFile(projectId, path) {
20812
21222
  const rows = this.mapFileRows(
20813
- this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")).all(projectId, path)
21223
+ allRows(
21224
+ this.db.prepare(this.fileSelect("f.project_id = ? AND f.path = ?")),
21225
+ [projectId, path]
21226
+ )
20814
21227
  );
20815
21228
  return rows[0] ?? null;
20816
21229
  }
@@ -20824,39 +21237,32 @@ var SqlitePoliciesRepository = class {
20824
21237
  }
20825
21238
  db;
20826
21239
  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
- }
21240
+ const rows = allRows(this.db.prepare("SELECT * FROM policies"));
21241
+ const policies = mapRowsTolerant(rows, (row) => {
21242
+ const target = JSON.parse(row.target);
21243
+ const customKeywords = row.custom_keywords ? JSON.parse(row.custom_keywords) : void 0;
21244
+ return Policy.parse({
21245
+ id: row.id,
21246
+ scope: row.scope,
21247
+ target,
21248
+ action: row.action,
21249
+ enabled: intToBool(row.enabled),
21250
+ customKeywords
21251
+ });
21252
+ });
20846
21253
  return Promise.resolve(policies);
20847
21254
  }
20848
21255
  // Seed one policy per bundled category from DEFAULT_ACTIONS so the
20849
21256
  // detection-type config exists from first run. Only when the table is empty,
20850
21257
  // so a user's edits are never clobbered.
20851
21258
  seedDefaults() {
20852
- const count = this.db.prepare("SELECT count(*) AS c FROM policies").get().c;
21259
+ const count = countScalar(this.db, "SELECT count(*) AS n FROM policies");
20853
21260
  if (count > 0) return;
20854
21261
  const stmt = this.db.prepare(
20855
21262
  `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
20856
21263
  VALUES (:id, 'global', :target, :action, 1, :now, :now)`
20857
21264
  );
20858
- this.db.exec("BEGIN");
20859
- try {
21265
+ failOpenTransaction(this.db, () => {
20860
21266
  for (const [category, action] of Object.entries(DEFAULT_ACTIONS)) {
20861
21267
  stmt.run({
20862
21268
  id: randomUUID4(),
@@ -20865,10 +21271,41 @@ var SqlitePoliciesRepository = class {
20865
21271
  now: Date.now()
20866
21272
  });
20867
21273
  }
20868
- this.db.exec("COMMIT");
20869
- } catch {
20870
- this.db.exec("ROLLBACK");
20871
- }
21274
+ });
21275
+ }
21276
+ // Insert-or-update the single global per-category policy row, keyed on the
21277
+ // existing uq_policies_scope_target unique index (scope, target). `action`
21278
+ // uses the SAME vocabulary seedDefaults writes (DEFAULT_ACTIONS' ActionTaken
21279
+ // values), so the runtime's resolveAction reads rows written by either path
21280
+ // identically. On conflict, `action`, `enabled`, and `updated_at` are updated;
21281
+ // `id` and `created_at` are left exactly as they were.
21282
+ upsertCategoryAction(category, action) {
21283
+ const now = Date.now();
21284
+ this.db.prepare(
21285
+ `INSERT INTO policies (id, scope, target, action, enabled, created_at, updated_at)
21286
+ VALUES (:id, 'global', :target, :action, 1, :now, :now)
21287
+ ON CONFLICT(scope, target) DO UPDATE SET action = excluded.action, enabled = 1, updated_at = excluded.updated_at`
21288
+ ).run({ id: randomUUID4(), target: JSON.stringify({ category }), action, now });
21289
+ }
21290
+ // Caps every global per-category policy currently set to block/redact down
21291
+ // to warn (see warn-era-cap.ts). Rule-targeted policies are untouched.
21292
+ // Returns the number of rows changed.
21293
+ capCategoryActions() {
21294
+ const info = this.db.prepare(
21295
+ `UPDATE policies SET action='warn', updated_at=:now
21296
+ WHERE scope='global' AND action IN ('block','redact')
21297
+ AND json_extract(target,'$.category') IS NOT NULL`
21298
+ ).run({ now: Date.now() });
21299
+ return Number(info.changes);
21300
+ }
21301
+ // Read the current action for a single global per-category policy row, mirroring
21302
+ // upsertCategoryAction's category-lookup predicate. Returns undefined when no
21303
+ // row exists yet, so callers can distinguish an unset category from a set one.
21304
+ getCategoryAction(category) {
21305
+ const row = this.db.prepare(
21306
+ `SELECT action FROM policies WHERE scope='global' AND json_extract(target,'$.category') = :category`
21307
+ ).get({ category });
21308
+ return row?.action;
20872
21309
  }
20873
21310
  };
20874
21311
 
@@ -20944,7 +21381,7 @@ var SqliteProjectFilesRepository = class {
20944
21381
  maxStampStmt;
20945
21382
  /** Replace `projectId`'s tree with the scan's files. Caller wraps in a transaction. */
20946
21383
  replaceForProject(projectId, scan2, now) {
20947
- const { maxStamp } = this.maxStampStmt.get({ projectId });
21384
+ const maxStamp = getRow(this.maxStampStmt, { projectId })?.maxStamp ?? 0;
20948
21385
  const stamp = Math.max(now, maxStamp + 1);
20949
21386
  for (const file2 of scan2.files) {
20950
21387
  this.upsertStmt.run({
@@ -21027,7 +21464,7 @@ var SqliteResolutionsRepository = class {
21027
21464
  }
21028
21465
  /** The newest disposition recorded for a finding key, or undefined if none. */
21029
21466
  latestByKey(key) {
21030
- const row = this.latestStmt.get({ findingKey: key });
21467
+ const row = getRow(this.latestStmt, { findingKey: key });
21031
21468
  if (!row) return void 0;
21032
21469
  return {
21033
21470
  // Safe narrows: insertResolution enum-parses both columns on every write,
@@ -21044,7 +21481,7 @@ var SqliteResolutionsRepository = class {
21044
21481
  * the CLI) surfaces for that file.
21045
21482
  */
21046
21483
  openAtRestKeysForPath(path) {
21047
- const rows = this.openAtRestStmt.all({ path });
21484
+ const rows = allRows(this.openAtRestStmt, { path });
21048
21485
  return rows.map((r) => r.finding_key);
21049
21486
  }
21050
21487
  /**
@@ -21055,7 +21492,7 @@ var SqliteResolutionsRepository = class {
21055
21492
  * resolution row (see scan.ts).
21056
21493
  */
21057
21494
  resolvedAtRestKeysForPath(path) {
21058
- const rows = this.resolvedAtRestStmt.all({ path });
21495
+ const rows = allRows(this.resolvedAtRestStmt, { path });
21059
21496
  return rows.map((r) => r.finding_key);
21060
21497
  }
21061
21498
  };
@@ -21084,31 +21521,25 @@ var SqliteScanLedgerRepository = class {
21084
21521
  // Previously scanned files under THIS ruleset, keyed by path. Rows from an
21085
21522
  // older ruleset are simply absent, which reads as "never scanned".
21086
21523
  entriesForRuleset(rulesetHash) {
21087
- const rows = this.readStmt.all({ rulesetHash });
21524
+ const rows = allRows(this.readStmt, {
21525
+ rulesetHash
21526
+ });
21088
21527
  return new Map(rows.map((r) => [r.path, { mtime: r.mtime, contentHash: r.contentHash }]));
21089
21528
  }
21090
21529
  upsertEntries(entries) {
21091
21530
  if (entries.length === 0) return;
21092
21531
  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;
21532
+ failOpenTransaction(this.db, () => {
21533
+ for (const entry of entries) {
21534
+ this.upsertStmt.run({
21535
+ path: entry.path,
21536
+ mtime: entry.mtime,
21537
+ contentHash: entry.contentHash,
21538
+ rulesetHash: entry.rulesetHash,
21539
+ scannedAt
21540
+ });
21109
21541
  }
21110
- } catch {
21111
- }
21542
+ });
21112
21543
  }
21113
21544
  };
21114
21545
 
@@ -21185,8 +21616,9 @@ var SqliteSecurityRepository = class {
21185
21616
  // finding — its rn = 1 filter is also what makes the LEFT JOIN safe against
21186
21617
  // double-counting a key that accumulated several append-only rows.
21187
21618
  severitySummary() {
21188
- const rows = this.db.prepare(
21189
- `SELECT f.severity AS severity,
21619
+ const rows = allRows(
21620
+ this.db.prepare(
21621
+ `SELECT f.severity AS severity,
21190
21622
  COUNT(*) AS count,
21191
21623
  SUM(CASE
21192
21624
  WHEN e.kind != 'code_change' THEN 1
@@ -21205,7 +21637,8 @@ var SqliteSecurityRepository = class {
21205
21637
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
21206
21638
  ON latest.finding_key = f.finding_key
21207
21639
  GROUP BY f.severity`
21208
- ).all();
21640
+ )
21641
+ );
21209
21642
  const byRow = new Map(rows.map((r) => [r.severity, r]));
21210
21643
  const bySeverity = SEVERITIES.map((severity) => ({
21211
21644
  severity,
@@ -21289,14 +21722,15 @@ var SqliteSecurityRepository = class {
21289
21722
  const numBuckets = granularity === "day" ? lenDays : Math.ceil(lenDays / 7);
21290
21723
  const now = this.now();
21291
21724
  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,
21725
+ const rows = allRows(
21726
+ this.db.prepare(
21727
+ // first_detected_at is the PRESERVED first-detection time (set once on a
21728
+ // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
21729
+ // measures from first sighting not the latest re-scan's event, whose
21730
+ // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
21731
+ // the parent event's occurred_at defends against any legacy/edge row the
21732
+ // backfill left null.
21733
+ `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
21300
21734
  (
21301
21735
  SELECT fr.status FROM finding_resolution fr
21302
21736
  WHERE fr.finding_key = f.finding_key
@@ -21322,14 +21756,16 @@ var SqliteSecurityRepository = class {
21322
21756
  WHERE fr.finding_key = f.finding_key
21323
21757
  AND fr.resolved_at >= :windowStart
21324
21758
  )`
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 });
21759
+ // The EXISTS is a SUPERSET prefilter that bounds the scan to keys with
21760
+ // any resolution activity at/after the window start — a row this method
21761
+ // ultimately counts has its LATEST resolution inside the window, which
21762
+ // implies such a row exists, so nothing wanted is dropped. The exact
21763
+ // latest-wins + status/method + window gate stays in JS below,
21764
+ // dialect-agnostic. Without this, a
21765
+ // 7d request evaluated the store's entire trackable-findings history.
21766
+ ),
21767
+ { windowStart }
21768
+ );
21333
21769
  const sums = /* @__PURE__ */ new Map();
21334
21770
  const counts = /* @__PURE__ */ new Map();
21335
21771
  for (const r of rows) {
@@ -21359,8 +21795,9 @@ var SqliteSecurityRepository = class {
21359
21795
  if (opts.kind === "user") return Promise.resolve({ range, items: [] });
21360
21796
  const now = this.now();
21361
21797
  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
21798
+ const rows = allRows(
21799
+ this.db.prepare(
21800
+ `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
21364
21801
  FROM findings f JOIN events e ON e.id = f.event_id
21365
21802
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21366
21803
  AND json_extract(e.metadata, '$.repo') IS NOT NULL
@@ -21368,7 +21805,9 @@ var SqliteSecurityRepository = class {
21368
21805
  GROUP BY repo
21369
21806
  ORDER BY c DESC, repo
21370
21807
  LIMIT :limit`
21371
- ).all({ from, to: now, limit });
21808
+ ),
21809
+ { from, to: now, limit }
21810
+ );
21372
21811
  const items = rows.map((r) => ({
21373
21812
  id: `repo_${r.repo}`,
21374
21813
  name: r.repo,
@@ -21390,8 +21829,9 @@ var SqliteSecurityRepository = class {
21390
21829
  // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
21391
21830
  // capped at `limit`.
21392
21831
  recentlyResolved(limit = 20) {
21393
- const rows = this.db.prepare(
21394
- `SELECT f.finding_key AS finding_key,
21832
+ const rows = allRows(
21833
+ this.db.prepare(
21834
+ `SELECT f.finding_key AS finding_key,
21395
21835
  f.rule_id AS rule_id,
21396
21836
  f.severity AS severity,
21397
21837
  json_extract(e.metadata, '$.filePath') AS path,
@@ -21425,7 +21865,9 @@ var SqliteSecurityRepository = class {
21425
21865
  ) IS NOT NULL
21426
21866
  ORDER BY latest_resolved_at DESC
21427
21867
  LIMIT :limit`
21428
- ).all({ limit });
21868
+ ),
21869
+ { limit }
21870
+ );
21429
21871
  const items = rows.map((r) => ({
21430
21872
  findingKey: r.finding_key,
21431
21873
  ruleId: r.rule_id,
@@ -21442,12 +21884,15 @@ var SqliteSecurityRepository = class {
21442
21884
  // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
21443
21885
  // numeric and the JS aggregations bucket/split on ms directly.
21444
21886
  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
21887
+ const rows = allRows(
21888
+ this.db.prepare(
21889
+ `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
21447
21890
  FROM findings f JOIN events e ON e.id = f.event_id
21448
21891
  WHERE e.occurred_at >= :from AND e.occurred_at < :to
21449
21892
  ORDER BY e.occurred_at`
21450
- ).all({ from: fromMs, to: toMs });
21893
+ ),
21894
+ { from: fromMs, to: toMs }
21895
+ );
21451
21896
  return rows.map((r) => ({
21452
21897
  occurredAt: r.occurred_at,
21453
21898
  severity: r.severity,
@@ -21461,12 +21906,7 @@ import { randomUUID as randomUUID7 } from "crypto";
21461
21906
  var KIND_ORDER = ["provider", "internal", "ip"];
21462
21907
  var CALL_SITE_EMBED_CAP = 200;
21463
21908
  function parseNetwork(networkJson) {
21464
- if (!networkJson) return null;
21465
- try {
21466
- return JSON.parse(networkJson);
21467
- } catch {
21468
- return null;
21469
- }
21909
+ return safeJson(networkJson, null);
21470
21910
  }
21471
21911
  function toEndpointSummary(row) {
21472
21912
  return {
@@ -21553,27 +21993,39 @@ var SqliteSharesRepository = class {
21553
21993
  }
21554
21994
  db;
21555
21995
  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(
21996
+ const destinations = countScalar(this.db, "SELECT count(*) AS n FROM share_destination");
21997
+ const endpoints = countScalar(this.db, "SELECT count(*) AS n FROM share_endpoint");
21998
+ const callSites = countScalar(this.db, "SELECT count(*) AS n FROM share_call_site");
21999
+ const insecure = countScalar(
22000
+ this.db,
21561
22001
  "SELECT count(DISTINCT destination_id) AS n FROM share_endpoint WHERE transport = 'http'"
21562
22002
  );
21563
- const needsReview = scalar(
22003
+ const needsReview = countScalar(
22004
+ this.db,
21564
22005
  `SELECT count(DISTINCT d.id) AS n
21565
22006
  FROM share_destination d
21566
22007
  LEFT JOIN share_endpoint e ON e.destination_id = d.id AND e.transport = 'http'
21567
22008
  WHERE d.trust IN ('unverified', 'ip') OR e.id IS NOT NULL`
21568
22009
  );
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
- }
22010
+ const kindCounts = countBy(
22011
+ this.db,
22012
+ "SELECT kind AS k, count(*) AS n FROM share_destination GROUP BY kind"
22013
+ );
22014
+ const byKind = {
22015
+ provider: kindCounts.get("provider") ?? 0,
22016
+ internal: kindCounts.get("internal") ?? 0,
22017
+ ip: kindCounts.get("ip") ?? 0
22018
+ };
22019
+ const trustCounts = countBy(
22020
+ this.db,
22021
+ "SELECT trust AS k, count(*) AS n FROM share_destination GROUP BY trust"
22022
+ );
22023
+ const byTrust = {
22024
+ recognized: trustCounts.get("recognized") ?? 0,
22025
+ internal: trustCounts.get("internal") ?? 0,
22026
+ unverified: trustCounts.get("unverified") ?? 0,
22027
+ ip: trustCounts.get("ip") ?? 0
22028
+ };
21577
22029
  return Promise.resolve({
21578
22030
  destinations,
21579
22031
  endpoints,
@@ -21689,7 +22141,7 @@ var SqliteSharesRepository = class {
21689
22141
  }
21690
22142
  let sql;
21691
22143
  if (q) {
21692
- const pattern = `%${escapeLikePattern(q)}%`;
22144
+ const pattern = containsPattern(q);
21693
22145
  conditions.push(
21694
22146
  `(d.name LIKE ? ESCAPE '\\' OR d.category LIKE ? ESCAPE '\\' OR e.url LIKE ? ESCAPE '\\'
21695
22147
  OR c.project LIKE ? ESCAPE '\\' OR c.file LIKE ? ESCAPE '\\')`
@@ -21709,24 +22161,31 @@ var SqliteSharesRepository = class {
21709
22161
  ${conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""}
21710
22162
  ORDER BY d.created_at ASC, d.id ASC`;
21711
22163
  }
21712
- const rows = this.db.prepare(sql).all(...params);
22164
+ const rows = allRows(
22165
+ this.db.prepare(sql),
22166
+ params
22167
+ );
21713
22168
  return rows.map((r) => this.mapDestRow(r));
21714
22169
  }
21715
22170
  fetchDestinationById(destinationId) {
21716
- const row = this.db.prepare(
21717
- `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
22171
+ const row = getRow(
22172
+ this.db.prepare(
22173
+ `SELECT d.id, d.kind, d.name, d.host, d.category, d.trust, d.note,
21718
22174
  d.network_json AS networkJson, d.last_seen AS lastSeenMs,
21719
22175
  o.decision AS overrideDecision
21720
22176
  FROM share_destination d
21721
22177
  LEFT JOIN egress_decision_override o ON o.destination_id = d.id
21722
22178
  WHERE d.id = ?`
21723
- ).get(destinationId);
22179
+ ),
22180
+ [destinationId]
22181
+ );
21724
22182
  return row ? this.mapDestRow(row) : null;
21725
22183
  }
21726
22184
  fetchEndpoints(destinationIds) {
21727
22185
  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,
22186
+ const rows = allRows(
22187
+ this.db.prepare(
22188
+ `SELECT e.id, e.destination_id AS destinationId, e.method, e.transport, e.url,
21730
22189
  e.template, e.data_class AS dataClass, e.last_seen AS lastSeenMs,
21731
22190
  count(c.id) AS callSiteCount
21732
22191
  FROM share_endpoint e
@@ -21734,7 +22193,9 @@ var SqliteSharesRepository = class {
21734
22193
  WHERE e.destination_id IN (${placeholders(destinationIds.length)})
21735
22194
  GROUP BY e.id
21736
22195
  ORDER BY e.created_at ASC, e.id ASC`
21737
- ).all(...destinationIds);
22196
+ ),
22197
+ destinationIds
22198
+ );
21738
22199
  return rows.map((r) => ({
21739
22200
  id: r.id,
21740
22201
  destinationId: r.destinationId,
@@ -21759,13 +22220,16 @@ var SqliteSharesRepository = class {
21759
22220
  }
21760
22221
  fetchCallSites(endpointIds) {
21761
22222
  if (endpointIds.length === 0) return [];
21762
- const rows = this.db.prepare(
21763
- `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
22223
+ const rows = allRows(
22224
+ this.db.prepare(
22225
+ `SELECT id, endpoint_id AS endpointId, project, file, line, snippet, dynamic, vendored,
21764
22226
  project_id AS projectId
21765
22227
  FROM share_call_site
21766
22228
  WHERE endpoint_id IN (${placeholders(endpointIds.length)})
21767
22229
  ORDER BY created_at ASC, id ASC`
21768
- ).all(...endpointIds);
22230
+ ),
22231
+ endpointIds
22232
+ );
21769
22233
  return rows.map((r) => ({
21770
22234
  id: r.id,
21771
22235
  endpointId: r.endpointId,
@@ -21801,27 +22265,36 @@ var SqliteSourceProjectRepository = class {
21801
22265
  upsert(input, now = Date.now()) {
21802
22266
  const id = sourceProjectId(input.url);
21803
22267
  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
- });
22268
+ this.upsertStmt.run(
22269
+ bindParams({
22270
+ id: row.id,
22271
+ url: row.url,
22272
+ name: row.name,
22273
+ attributes: row.attributes,
22274
+ firstSeen: row.firstSeen,
22275
+ lastSeen: row.lastSeen
22276
+ })
22277
+ );
21812
22278
  return id;
21813
22279
  }
21814
22280
  findById(id) {
21815
- return this.db.prepare("SELECT * FROM source_project WHERE id = :id").get({ id });
22281
+ return getRow(
22282
+ this.db.prepare("SELECT * FROM source_project WHERE id = :id"),
22283
+ {
22284
+ id
22285
+ }
22286
+ );
21816
22287
  }
21817
22288
  // Distinct project names — a filter facet, served from the source_project
21818
22289
  // table, never from the audit fact table.
21819
22290
  distinctNames() {
21820
- const rows = this.db.prepare(
21821
- `SELECT DISTINCT name FROM source_project
22291
+ const rows = allRows(
22292
+ this.db.prepare(
22293
+ `SELECT DISTINCT name FROM source_project
21822
22294
  WHERE name IS NOT NULL
21823
22295
  ORDER BY name`
21824
- ).all();
22296
+ )
22297
+ );
21825
22298
  return rows.map((r) => r.name);
21826
22299
  }
21827
22300
  };
@@ -21840,8 +22313,7 @@ function hasLegacySampleRows(db) {
21840
22313
  function purgeSampleData(db) {
21841
22314
  try {
21842
22315
  if (!hasLegacySampleRows(db)) return;
21843
- db.exec("BEGIN");
21844
- try {
22316
+ withTransaction(db, () => {
21845
22317
  db.exec(
21846
22318
  `DELETE FROM share_call_site WHERE endpoint_id IN (
21847
22319
  SELECT e.id FROM share_endpoint e
@@ -21886,11 +22358,7 @@ function purgeSampleData(db) {
21886
22358
  value TEXT NOT NULL
21887
22359
  )`);
21888
22360
  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
- }
22361
+ });
21894
22362
  } catch {
21895
22363
  }
21896
22364
  }
@@ -21909,7 +22377,7 @@ function openWithPragmas(file2) {
21909
22377
  function backupLegacyStore(file2) {
21910
22378
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
21911
22379
  renameSync(file2, backup);
21912
- for (const sidecar of [`${file2}-wal`, `${file2}-shm`]) {
22380
+ for (const sidecar of walSidecars(file2)) {
21913
22381
  if (existsSync(sidecar)) rmSync(sidecar);
21914
22382
  }
21915
22383
  return backup;
@@ -21922,9 +22390,8 @@ function openLocalDatabase(dir) {
21922
22390
  db.close();
21923
22391
  const backup = backupLegacyStore(file2);
21924
22392
  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
- `
22393
+ akaWarn(
22394
+ `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
21928
22395
  );
21929
22396
  }
21930
22397
  applyMigrations(db);
@@ -21952,96 +22419,77 @@ function openLocalDatabase(dir) {
21952
22419
  const configInventory = new SqliteConfigInventoryRepository(db);
21953
22420
  policies.seedDefaults();
21954
22421
  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
- }
22422
+ failOpenTransaction(db, () => {
22423
+ events.insertEvent(event);
22424
+ const sessionId = event.metadata?.sessionId;
22425
+ findings.insertFindings(detected, sessionId ? { sessionId } : {});
22426
+ });
21968
22427
  }
21969
22428
  function ensureInventory(ctx) {
21970
22429
  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;
22430
+ const committed = failOpenTransaction(db, () => {
22431
+ const now = Date.now();
22432
+ if (ctx.host) resolved.hostId = inventory.upsert(ctx.host, now);
22433
+ if (ctx.harness) {
22434
+ resolved.harnessId = inventory.upsert(linkHost(ctx.harness, resolved.hostId), now);
21995
22435
  }
21996
- } catch {
21997
- return {};
21998
- }
21999
- return resolved;
22436
+ resolved.accountId = inventory.upsert(
22437
+ linkHost(
22438
+ {
22439
+ objectType: "user",
22440
+ identityKey: "local",
22441
+ attributes: { source: "local" }
22442
+ },
22443
+ resolved.hostId
22444
+ ),
22445
+ now
22446
+ );
22447
+ if (ctx.project) resolved.sourceProjectId = sourceProject.upsert(ctx.project, now);
22448
+ });
22449
+ return committed ? resolved : {};
22000
22450
  }
22001
22451
  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;
22452
+ failOpenTransaction(db, () => {
22453
+ const now = isoToEpochMillis(record2.scanEvent.startedAt);
22454
+ for (const item of record2.items) inventory.upsert(item, now);
22455
+ auditEvents.insertAuditEvent(record2.scanEvent);
22456
+ const definitionIds = /* @__PURE__ */ new Map();
22457
+ for (const def of record2.definitions ?? []) {
22458
+ definitionIds.set(`${def.ruleId}@${def.version}`, inspectionDefinitions.upsert(def));
22459
+ }
22460
+ for (const finding2 of record2.findings ?? []) {
22461
+ const definitionId = definitionIds.get(`${finding2.ruleId}@${finding2.version}`);
22462
+ if (!definitionId) continue;
22463
+ inspectionFindings.insertFinding({
22464
+ id: randomUUID8(),
22465
+ auditEventId: record2.scanEvent.id,
22466
+ inspectionDefinitionId: definitionId,
22467
+ span: finding2.span,
22468
+ maskedMatch: finding2.maskedMatch,
22469
+ actionTaken: finding2.actionTaken,
22470
+ confidence: finding2.confidence
22471
+ });
22029
22472
  }
22030
- } catch {
22031
- }
22473
+ });
22032
22474
  }
22033
22475
  function recordProjectFiles(projectId, scan2) {
22034
22476
  if (scan2.files.length === 0) return;
22477
+ failOpenTransaction(db, () => {
22478
+ projectFiles.replaceForProject(projectId, scan2, Date.now());
22479
+ });
22480
+ }
22481
+ async function transaction(fn) {
22482
+ db.exec("BEGIN");
22035
22483
  try {
22036
- db.exec("BEGIN");
22484
+ const result = await fn();
22485
+ db.exec("COMMIT");
22486
+ return result;
22487
+ } catch (err) {
22037
22488
  try {
22038
- projectFiles.replaceForProject(projectId, scan2, Date.now());
22039
- db.exec("COMMIT");
22040
- } catch (err) {
22041
22489
  db.exec("ROLLBACK");
22042
- throw err;
22490
+ } catch {
22043
22491
  }
22044
- } catch {
22492
+ throw err;
22045
22493
  }
22046
22494
  }
22047
22495
  function reconcileWorktreeProjects(canonicalId, headRoot, worktreeRoot) {
@@ -22060,8 +22508,7 @@ function openLocalDatabase(dir) {
22060
22508
  patternWin: `${escapeLikePattern(headPosix.split("/").join("\\"))}\\\\.claude\\\\worktrees\\\\%`
22061
22509
  });
22062
22510
  if (stale.length === 0) return;
22063
- db.exec("BEGIN");
22064
- try {
22511
+ withTransaction(db, () => {
22065
22512
  for (const { id } of stale) {
22066
22513
  db.prepare(
22067
22514
  "UPDATE audit_events SET source_project_id = :canonicalId WHERE source_project_id = :id"
@@ -22073,11 +22520,7 @@ function openLocalDatabase(dir) {
22073
22520
  db.prepare("DELETE FROM project_file WHERE project_id = :id").run({ id });
22074
22521
  db.prepare("DELETE FROM source_project WHERE id = :id").run({ id });
22075
22522
  }
22076
- db.exec("COMMIT");
22077
- } catch (err) {
22078
- db.exec("ROLLBACK");
22079
- throw err;
22080
- }
22523
+ });
22081
22524
  } catch {
22082
22525
  }
22083
22526
  }
@@ -22119,6 +22562,7 @@ function openLocalDatabase(dir) {
22119
22562
  purgeSampleData: () => {
22120
22563
  purgeSampleData(db);
22121
22564
  },
22565
+ transaction,
22122
22566
  close: () => {
22123
22567
  db.close();
22124
22568
  }
@@ -22211,12 +22655,27 @@ function readWorkspaceSettings(base = defaultDataDir()) {
22211
22655
  }
22212
22656
  }
22213
22657
  function readJson(file2) {
22658
+ let text;
22214
22659
  try {
22215
- const parsed = JSON.parse(readFileSync2(file2, "utf8"));
22216
- return typeof parsed === "object" && parsed !== null ? parsed : null;
22660
+ text = readFileSync2(file2, "utf8");
22217
22661
  } catch {
22218
22662
  return null;
22219
22663
  }
22664
+ return parseJsonObject(text) ?? null;
22665
+ }
22666
+
22667
+ // ../../packages/persistence/src/warn-era-cap.ts
22668
+ import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
22669
+ import { join as join5 } from "path";
22670
+ var MARKER = "warn-era-capped";
22671
+ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
22672
+ if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
22673
+ const marker = join5(dataDir2, MARKER);
22674
+ if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
22675
+ const capped = db.policies.capCategoryActions();
22676
+ writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
22677
+ `, { mode: DATA_FILE_MODE });
22678
+ return { capped };
22220
22679
  }
22221
22680
 
22222
22681
  // ../../packages/plugin-sdk/src/provider-env.ts
@@ -22291,7 +22750,7 @@ function resolveProviderSafe() {
22291
22750
  // ../../packages/plugin-sdk/src/config-inventory.ts
22292
22751
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
22293
22752
  import { homedir as homedir2 } from "os";
22294
- import { basename as basename3, join as join6 } from "path";
22753
+ import { basename as basename3, join as join7 } from "path";
22295
22754
 
22296
22755
  // ../../packages/detections/src/matchers/keyword.ts
22297
22756
  var KeywordMatcher2 = class {
@@ -24755,8 +25214,8 @@ function maskText(text) {
24755
25214
  }
24756
25215
 
24757
25216
  // ../../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";
25217
+ import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
25218
+ import { basename as basename2, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
24760
25219
  function resolveRepoIdentity(cwd) {
24761
25220
  try {
24762
25221
  const root = findGitRoot(cwd);
@@ -24806,7 +25265,7 @@ function resolveGitBranch(cwd) {
24806
25265
  try {
24807
25266
  const root = findGitRoot(cwd);
24808
25267
  if (!root) return void 0;
24809
- const dotGit = join5(root, ".git");
25268
+ const dotGit = join6(root, ".git");
24810
25269
  let gitdir;
24811
25270
  try {
24812
25271
  gitdir = statSync(dotGit).isDirectory() ? dotGit : resolveWorktreeGitdir(root, dotGit);
@@ -24814,7 +25273,7 @@ function resolveGitBranch(cwd) {
24814
25273
  return void 0;
24815
25274
  }
24816
25275
  if (gitdir === void 0) return void 0;
24817
- const head = safeRead(join5(gitdir, "HEAD"));
25276
+ const head = safeRead(join6(gitdir, "HEAD"));
24818
25277
  if (!head) return void 0;
24819
25278
  return /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(head)?.[1];
24820
25279
  } catch {
@@ -24824,37 +25283,37 @@ function resolveGitBranch(cwd) {
24824
25283
  function resolveWorktreeGitdir(root, dotGitFile) {
24825
25284
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGitFile) ?? "")?.[1];
24826
25285
  if (!target) return void 0;
24827
- return isAbsolute(target) ? target : join5(root, target);
25286
+ return isAbsolute(target) ? target : join6(root, target);
24828
25287
  }
24829
25288
  function findGitRoot(start) {
24830
25289
  let dir = start;
24831
25290
  for (; ; ) {
24832
- if (existsSync2(join5(dir, ".git"))) return dir;
25291
+ if (existsSync3(join6(dir, ".git"))) return dir;
24833
25292
  const parent = dirname(dir);
24834
25293
  if (parent === dir) return void 0;
24835
25294
  dir = parent;
24836
25295
  }
24837
25296
  }
24838
25297
  function resolveGitContext(root) {
24839
- const dotGit = join5(root, ".git");
25298
+ const dotGit = join6(root, ".git");
24840
25299
  try {
24841
25300
  if (statSync(dotGit).isDirectory()) {
24842
- return { configPath: join5(dotGit, "config"), headRoot: root };
25301
+ return { configPath: join6(dotGit, "config"), headRoot: root };
24843
25302
  }
24844
25303
  } catch {
24845
25304
  return void 0;
24846
25305
  }
24847
25306
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
24848
25307
  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 };
25308
+ const gitdir = isAbsolute(target) ? target : join6(root, target);
25309
+ if (existsSync3(join6(gitdir, "config"))) {
25310
+ return { configPath: join6(gitdir, "config"), headRoot: root };
24852
25311
  }
24853
- const commonRaw = safeRead(join5(gitdir, "commondir"))?.trim();
25312
+ const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
24854
25313
  if (!commonRaw) return void 0;
24855
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join5(gitdir, commonRaw);
25314
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
24856
25315
  const headRoot = basename2(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
24857
- return { configPath: join5(commonGitDir, "config"), headRoot };
25316
+ return { configPath: join6(commonGitDir, "config"), headRoot };
24858
25317
  }
24859
25318
  function safeRead(path) {
24860
25319
  try {
@@ -24920,31 +25379,31 @@ function resolveConfigInventory(input) {
24920
25379
  };
24921
25380
  try {
24922
25381
  const home = input.homeDir ?? homedir2();
24923
- const claudeDir = join6(home, ".claude");
25382
+ const claudeDir = join7(home, ".claude");
24924
25383
  const repo = resolveRepoIdentity(input.cwd);
24925
25384
  const repoIdentity = repo?.url ?? input.cwd;
24926
25385
  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");
25386
+ collectSettingsHooks(scan2, join7(claudeDir, "settings.json"), "user");
25387
+ collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.json"), "project");
25388
+ collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.local.json"), "local");
24930
25389
  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"), {
25390
+ collectMcpFile(scan2, join7(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
25391
+ collectUserClaudeJson(scan2, join7(home, ".claude.json"), input.cwd, repoIdentity);
25392
+ collectMcpFile(scan2, join7(claudeDir, "settings.json"), { scope: "user" });
25393
+ collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.json"), projectOrigin);
25394
+ collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.local.json"), {
24936
25395
  scope: "local",
24937
25396
  project: repoIdentity
24938
25397
  });
24939
25398
  collectConfigFiles(scan2, claudeDir, input.cwd);
24940
- collectSkillsDir(scan2, join6(claudeDir, "skills"), { source: "local", scope: "user" });
24941
- collectSkillsDir(scan2, join6(input.cwd, ".claude", "skills"), {
25399
+ collectSkillsDir(scan2, join7(claudeDir, "skills"), { source: "local", scope: "user" });
25400
+ collectSkillsDir(scan2, join7(input.cwd, ".claude", "skills"), {
24942
25401
  source: projectSource,
24943
25402
  scope: "project"
24944
25403
  });
24945
25404
  collectInstalledPlugins(scan2, claudeDir);
24946
25405
  collectMarketplaceSkills(scan2, claudeDir);
24947
- collectSkillsDir(scan2, join6(input.cwd, "skills"), { source: projectSource, scope: "project" });
25406
+ collectSkillsDir(scan2, join7(input.cwd, "skills"), { source: projectSource, scope: "project" });
24948
25407
  scan2.skills = dedupeSkills(scan2.skills);
24949
25408
  scan2.mcpServers = dedupeMcpServers(scan2.mcpServers);
24950
25409
  } catch (err) {
@@ -25073,7 +25532,7 @@ function projectEntryFor(projects, cwd) {
25073
25532
  return void 0;
25074
25533
  }
25075
25534
  function collectPluginManifestMcp(scan2, installPath, origin) {
25076
- const manifestPath = join6(installPath, ".claude-plugin", "plugin.json");
25535
+ const manifestPath = join7(installPath, ".claude-plugin", "plugin.json");
25077
25536
  const raw = readOptional(manifestPath);
25078
25537
  if (raw === void 0) return;
25079
25538
  try {
@@ -25081,7 +25540,7 @@ function collectPluginManifestMcp(scan2, installPath, origin) {
25081
25540
  if (typeof parsed !== "object" || parsed === null) return;
25082
25541
  const declared = parsed.mcpServers;
25083
25542
  if (typeof declared === "string" && declared.length > 0) {
25084
- collectMcpFile(scan2, join6(installPath, declared), origin, { recordErrors: true });
25543
+ collectMcpFile(scan2, join7(installPath, declared), origin, { recordErrors: true });
25085
25544
  } else {
25086
25545
  collectMcpObject(scan2, declared, manifestPath, origin);
25087
25546
  }
@@ -25098,14 +25557,14 @@ var SETTINGS_KEY_LABELS = [
25098
25557
  ["statusLine", "status line"]
25099
25558
  ];
25100
25559
  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");
25560
+ settingsConfigFile(scan2, join7(claudeDir, "settings.json"), "user", "User settings");
25561
+ settingsConfigFile(scan2, join7(cwd, ".claude", "settings.json"), "project", "Project settings");
25562
+ settingsConfigFile(scan2, join7(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
25563
+ memoryConfigFile(scan2, join7(claudeDir, "CLAUDE.md"), "user", "User memory");
25564
+ memoryConfigFile(scan2, join7(cwd, "CLAUDE.md"), "project", "Project memory");
25565
+ mcpJsonConfigFile(scan2, join7(cwd, ".mcp.json"));
25566
+ dirConfigFile(scan2, join7(cwd, ".claude", "commands"), "Slash commands", "command");
25567
+ dirConfigFile(scan2, join7(cwd, ".claude", "agents"), "Subagents", "subagent");
25109
25568
  }
25110
25569
  function configFileEntry(path, scope, kind) {
25111
25570
  try {
@@ -25180,7 +25639,7 @@ function countMarkdownFiles(dir, depth) {
25180
25639
  let count = 0;
25181
25640
  for (const dirent of readdirSync(dir, { withFileTypes: true })) {
25182
25641
  if (dirent.name.startsWith(".")) continue;
25183
- if (dirent.isDirectory()) count += countMarkdownFiles(join6(dir, dirent.name), depth + 1);
25642
+ if (dirent.isDirectory()) count += countMarkdownFiles(join7(dir, dirent.name), depth + 1);
25184
25643
  else if (dirent.name.endsWith(".md")) count += 1;
25185
25644
  }
25186
25645
  return count;
@@ -25193,7 +25652,7 @@ function collectSkillsDir(scan2, dir, origin) {
25193
25652
  return;
25194
25653
  }
25195
25654
  for (const name of names) {
25196
- const skillFile = join6(dir, name, "SKILL.md");
25655
+ const skillFile = join7(dir, name, "SKILL.md");
25197
25656
  try {
25198
25657
  const raw = readOptional(skillFile);
25199
25658
  if (raw === void 0) continue;
@@ -25202,7 +25661,7 @@ function collectSkillsDir(scan2, dir, origin) {
25202
25661
  name: front.name ?? name,
25203
25662
  source: origin.source,
25204
25663
  scope: origin.scope,
25205
- location: join6(dir, name),
25664
+ location: join7(dir, name),
25206
25665
  updatedAt: statSync2(skillFile).mtime.toISOString()
25207
25666
  };
25208
25667
  const version2 = front.version ?? origin.defaultVersion;
@@ -25233,7 +25692,7 @@ function parseFrontmatter(raw) {
25233
25692
  return out;
25234
25693
  }
25235
25694
  function collectInstalledPlugins(scan2, claudeDir) {
25236
- const manifestPath = join6(claudeDir, "plugins", "installed_plugins.json");
25695
+ const manifestPath = join7(claudeDir, "plugins", "installed_plugins.json");
25237
25696
  const raw = readOptional(manifestPath);
25238
25697
  if (raw === void 0) return;
25239
25698
  let plugins;
@@ -25258,7 +25717,7 @@ function collectInstalledPlugins(scan2, claudeDir) {
25258
25717
  if (typeof installPath !== "string" || seen.has(installPath)) continue;
25259
25718
  seen.add(installPath);
25260
25719
  const version2 = install.version;
25261
- const hooksPath = join6(installPath, "hooks", "hooks.json");
25720
+ const hooksPath = join7(installPath, "hooks", "hooks.json");
25262
25721
  const hooksRaw = readOptional(hooksPath);
25263
25722
  if (hooksRaw !== void 0) {
25264
25723
  try {
@@ -25278,22 +25737,22 @@ function collectInstalledPlugins(scan2, claudeDir) {
25278
25737
  }
25279
25738
  const origin = { source: marketplace, scope: "plugin", pluginName };
25280
25739
  if (typeof version2 === "string") origin.defaultVersion = version2;
25281
- collectSkillsDir(scan2, join6(installPath, "skills"), origin);
25740
+ collectSkillsDir(scan2, join7(installPath, "skills"), origin);
25282
25741
  const mcpOrigin = { scope: "plugin", pluginName, marketplace };
25283
- collectMcpFile(scan2, join6(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
25742
+ collectMcpFile(scan2, join7(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
25284
25743
  collectPluginManifestMcp(scan2, installPath, mcpOrigin);
25285
25744
  }
25286
25745
  }
25287
25746
  }
25288
25747
  function collectMarketplaceSkills(scan2, claudeDir) {
25289
- for (const mp of readMarketplaces(join6(claudeDir, "plugins", "known_marketplaces.json"))) {
25748
+ for (const mp of readMarketplaces(join7(claudeDir, "plugins", "known_marketplaces.json"))) {
25290
25749
  if (isClaudeOfficialMarketplace(mp.name, mp.repo)) continue;
25291
- collectSkillsDir(scan2, join6(mp.installLocation, "skills"), {
25750
+ collectSkillsDir(scan2, join7(mp.installLocation, "skills"), {
25292
25751
  source: mp.name,
25293
25752
  scope: "plugin"
25294
25753
  });
25295
- collectPluginSkillDirs(scan2, join6(mp.installLocation, "plugins"), mp.name);
25296
- collectPluginSkillDirs(scan2, join6(mp.installLocation, "external_plugins"), mp.name);
25754
+ collectPluginSkillDirs(scan2, join7(mp.installLocation, "plugins"), mp.name);
25755
+ collectPluginSkillDirs(scan2, join7(mp.installLocation, "external_plugins"), mp.name);
25297
25756
  }
25298
25757
  }
25299
25758
  function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
@@ -25304,7 +25763,7 @@ function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
25304
25763
  return;
25305
25764
  }
25306
25765
  for (const plugin of plugins) {
25307
- collectSkillsDir(scan2, join6(pluginsDir, plugin, "skills"), {
25766
+ collectSkillsDir(scan2, join7(pluginsDir, plugin, "skills"), {
25308
25767
  source: marketplace,
25309
25768
  scope: "plugin",
25310
25769
  pluginName: plugin
@@ -25413,22 +25872,22 @@ function resolveInventoryContext(input) {
25413
25872
  }
25414
25873
 
25415
25874
  // ../../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";
25875
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
25876
+ import { join as join8 } from "path";
25418
25877
  var SESSION_START_MARKER = "session-start-last";
25419
25878
  function claimSessionStart(dataDir2, sessionId) {
25420
25879
  return claimOncePerSession(dataDir2, SESSION_START_MARKER, sessionId);
25421
25880
  }
25422
25881
  function claimOncePerSession(dataDir2, marker, sessionId) {
25423
25882
  if (!sessionId) return true;
25424
- const path = join7(dataDir2, marker);
25883
+ const path = join8(dataDir2, marker);
25425
25884
  try {
25426
25885
  if (readFileSync5(path, "utf8") === sessionId) return false;
25427
25886
  } catch {
25428
25887
  }
25429
25888
  try {
25430
25889
  mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
25431
- writeFileSync3(path, sessionId, { mode: DATA_FILE_MODE });
25890
+ writeFileSync4(path, sessionId, { mode: DATA_FILE_MODE });
25432
25891
  } catch {
25433
25892
  }
25434
25893
  return true;
@@ -25436,8 +25895,8 @@ function claimOncePerSession(dataDir2, marker, sessionId) {
25436
25895
 
25437
25896
  // ../../packages/plugin-sdk/src/project-files.ts
25438
25897
  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";
25898
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
25899
+ import { basename as basename4, join as join9, relative, sep as sep3 } from "path";
25441
25900
  var SKIP_DIRS = /* @__PURE__ */ new Set([
25442
25901
  ".git",
25443
25902
  "node_modules",
@@ -25457,7 +25916,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
25457
25916
  var MAX_FILES = 2e4;
25458
25917
  function readIgnoreLayer(dir) {
25459
25918
  try {
25460
- const content = readFileSync6(join8(dir, ".gitignore"), "utf8");
25919
+ const content = readFileSync6(join9(dir, ".gitignore"), "utf8");
25461
25920
  return { base: dir, matcher: (0, import_ignore.default)().add(content) };
25462
25921
  } catch {
25463
25922
  return void 0;
@@ -25535,10 +25994,10 @@ function resolveProjectFiles(cwd) {
25535
25994
  const layer = readIgnoreLayer(dir);
25536
25995
  const dirLayers = layer ? [...layers, layer] : layers;
25537
25996
  for (const entry of dirents) {
25538
- const fullPath = join8(dir, entry.name);
25997
+ const fullPath = join9(dir, entry.name);
25539
25998
  if (entry.isDirectory()) {
25540
25999
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
25541
- if (existsSync3(join8(fullPath, ".git"))) continue;
26000
+ if (existsSync4(join9(fullPath, ".git"))) continue;
25542
26001
  if (visit2(fullPath, dirLayers)) return true;
25543
26002
  continue;
25544
26003
  }
@@ -25575,18 +26034,21 @@ function resolveProjectFiles(cwd) {
25575
26034
  // ../../packages/plugin-sdk/src/runtime.ts
25576
26035
  import { randomUUID as randomUUID10 } from "crypto";
25577
26036
 
26037
+ // ../../packages/plugin-sdk/src/suppressions.ts
26038
+ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
26039
+
25578
26040
  // ../../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";
26041
+ import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
26042
+ import { join as join10 } from "path";
25581
26043
  function throttled(dataDir2, markerName, windowMs) {
25582
- const marker = join9(dataDir2, markerName);
26044
+ const marker = join10(dataDir2, markerName);
25583
26045
  try {
25584
26046
  if (Date.now() - statSync3(marker).mtimeMs < windowMs) return true;
25585
26047
  } catch {
25586
26048
  }
25587
26049
  try {
25588
26050
  mkdirSync4(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
25589
- writeFileSync4(marker, String(Date.now()), { mode: DATA_FILE_MODE });
26051
+ writeFileSync5(marker, String(Date.now()), { mode: DATA_FILE_MODE });
25590
26052
  } catch {
25591
26053
  }
25592
26054
  return false;
@@ -25786,6 +26248,14 @@ var StandaloneDataGateway = class {
25786
26248
  sweepTerminalExceptions(retentionMs) {
25787
26249
  return this.db.exceptions.sweepTerminal(retentionMs);
25788
26250
  }
26251
+ // The warn-era enforcement cap, standalone-only store maintenance invoked
26252
+ // from SessionStart, not part of the DataGateway port. Returns the number
26253
+ // of block/redact rows capped to warn (0 for a redact-policy store or an
26254
+ // already-capped one).
26255
+ capWarnEraEnforcement(policyMode) {
26256
+ const { capped } = capWarnEraEnforcementOnce(this.db, policyMode, this.dataDir);
26257
+ return { capped };
26258
+ }
25789
26259
  // One project-file scan → the local project_file tree (one transaction inside
25790
26260
  // the LocalDatabase, fail-open there). Like the sweep above, this is
25791
26261
  // NOT part of the DataGateway port: the file tree is a local-store read model.
@@ -25908,6 +26378,15 @@ async function handleSessionStart(input, config2 = loadConfig()) {
25908
26378
  await gateway.sweepTerminalExceptions(EXCEPTION_RETENTION_MS);
25909
26379
  } catch {
25910
26380
  }
26381
+ try {
26382
+ const { capped } = gateway.capWarnEraEnforcement(config2.settings.policy);
26383
+ if (capped > 0) {
26384
+ process.stderr.write(
26385
+ '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'
26386
+ );
26387
+ }
26388
+ } catch {
26389
+ }
25911
26390
  try {
25912
26391
  if (resolved.sourceProjectId) {
25913
26392
  const filesScan = resolveProjectFiles(input.cwd);
@@ -26001,7 +26480,7 @@ function buildSessionRoot(sessionId, input, ctx, resolved, provider, branch) {
26001
26480
 
26002
26481
  // src/history/reconcile-trigger.ts
26003
26482
  import { spawn } from "child_process";
26004
- import { dirname as dirname2, join as join11 } from "path";
26483
+ import { dirname as dirname2, join as join12 } from "path";
26005
26484
  import { fileURLToPath } from "url";
26006
26485
 
26007
26486
  // src/history/tail.ts
@@ -26013,9 +26492,9 @@ import {
26013
26492
  openSync,
26014
26493
  readFileSync as readFileSync7,
26015
26494
  readSync,
26016
- writeFileSync as writeFileSync5
26495
+ writeFileSync as writeFileSync6
26017
26496
  } from "fs";
26018
- import { join as join10 } from "path";
26497
+ import { join as join11 } from "path";
26019
26498
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
26020
26499
  function safeSessionId(sessionId) {
26021
26500
  if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
@@ -26032,7 +26511,7 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
26032
26511
  const marker = `${RECONCILE_MARKER_PREFIX}-${safeSessionId(sessionId)}`;
26033
26512
  if (throttled(dataDir2, marker, RECONCILE_THROTTLE_MS)) return;
26034
26513
  const here = dirname2(fileURLToPath(import.meta.url));
26035
- const child = spawn(process.execPath, [join11(here, "reconcile.js"), sessionId, transcriptPath], {
26514
+ const child = spawn(process.execPath, [join12(here, "reconcile.js"), sessionId, transcriptPath], {
26036
26515
  detached: true,
26037
26516
  stdio: "ignore"
26038
26517
  });