@akasecurity/ai-tc-claude-code 0.9.1 → 0.9.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.
@@ -494,9 +494,13 @@ var require_ignore = __commonJS({
494
494
  // src/hooks/session-start.ts
495
495
  import { readFileSync as readFileSync8 } from "fs";
496
496
 
497
+ // ../../packages/plugin-sdk/src/config.ts
498
+ import { existsSync as existsSync3 } from "fs";
499
+ import { join as join6 } from "path";
500
+
497
501
  // ../../packages/persistence/src/database.ts
498
502
  import { randomUUID as randomUUID8 } from "crypto";
499
- import { existsSync, renameSync, rmSync } from "fs";
503
+ import { existsSync, renameSync as renameSync2, rmSync as rmSync2 } from "fs";
500
504
  import { join, sep } from "path";
501
505
  import { DatabaseSync } from "node:sqlite";
502
506
 
@@ -549,6 +553,18 @@ var SQLITE_MIGRATIONS = [
549
553
  {
550
554
  tag: "0011_egress_writer",
551
555
  sql: '-- Stable per-project reconcile key for egress call sites, plus a host-keyed\n-- egress decision override that survives destination pruning.\n--\n-- DROP INDEX IF EXISTS, not a bare DROP: the applier replays a pending\n-- migration\'s non-index statements verbatim, so a store that lost\n-- uq_share_call_site out of band would throw here \u2014 and on the plugin hook path\n-- that throw is swallowed fail-open, silently stopping capture.\n--\n-- Existing rows carry the old key\'s discriminator into project_key\n-- (\'legacy:\' || project), so the new unique index is a re-encoding of the old\n-- one and cannot collide on rows the old one allowed. Leaving them on the\n-- column default would collapse two projects\' identical file/line hits onto\n-- one key and abort the whole migration. Writer keys are \'git:\'/\'path:\'-\n-- prefixed, so a backfilled row never collides with a captured one either.\n--\n-- egress_decision_override.destination_id becomes nullable with ON DELETE SET\n-- NULL, which SQLite can only do by rebuilding the table. `host` is ADDed\n-- before the rebuild so the copy has a column to read and so the migration\n-- still presents two probeable columns to the applier\'s evidence check.\nALTER TABLE `share_call_site` ADD `project_key` text DEFAULT \'\' NOT NULL;--> statement-breakpoint\nUPDATE `share_call_site` SET `project_key` = \'legacy:\' || `project`;--> statement-breakpoint\nDROP INDEX IF EXISTS `uq_share_call_site`;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_share_call_site` ON `share_call_site` (`endpoint_id`,`project_key`,`file`,`line`);--> statement-breakpoint\nCREATE INDEX `idx_share_call_site_project` ON `share_call_site` (`project_key`,`endpoint_id`);--> statement-breakpoint\nALTER TABLE `egress_decision_override` ADD `host` text;--> statement-breakpoint\nPRAGMA foreign_keys=OFF;--> statement-breakpoint\nCREATE TABLE `__new_egress_decision_override` (\n `id` text PRIMARY KEY NOT NULL,\n `destination_id` text,\n `host` text,\n `decision` text NOT NULL,\n `created_at` integer NOT NULL,\n `updated_at` integer NOT NULL,\n FOREIGN KEY (`destination_id`) REFERENCES `share_destination`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nINSERT INTO `__new_egress_decision_override`("id", "destination_id", "host", "decision", "created_at", "updated_at") SELECT "id", "destination_id", "host", "decision", "created_at", "updated_at" FROM `egress_decision_override`;--> statement-breakpoint\nDROP TABLE `egress_decision_override`;--> statement-breakpoint\nALTER TABLE `__new_egress_decision_override` RENAME TO `egress_decision_override`;--> statement-breakpoint\nPRAGMA foreign_keys=ON;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override` ON `egress_decision_override` (`destination_id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_egress_decision_override_host` ON `egress_decision_override` (`host`) WHERE `host` IS NOT NULL;\n'
556
+ },
557
+ {
558
+ tag: "0012_handy_the_captain",
559
+ sql: "ALTER TABLE `inspection_findings` ADD `finding_key` text;--> statement-breakpoint\nALTER TABLE `inspection_findings` ADD `first_detected_at` integer;--> statement-breakpoint\nCREATE UNIQUE INDEX `uq_inspection_findings_key` ON `inspection_findings` (`finding_key`);"
560
+ },
561
+ {
562
+ tag: "0013_legacy_history_backfill_support",
563
+ sql: "-- Legacy-to-generalized backfill support: the schema the resumable\n-- events/findings -> audit_events/inspection_findings copy needs (the row\n-- copy itself runs as a batched post-migration installer, not here \u2014 see\n-- @akasecurity/persistence's migrations.ts), plus the audit_events read-path\n-- indexes replacing the ones the legacy events table carried (0009/0010).\n\n-- Tracks how far the batched, resumable copy has advanced through each legacy\n-- table, by rowid, so a copy interrupted mid-run resumes instead of\n-- restarting, and a completed copy is a cheap no-op on every later open.\nCREATE TABLE `legacy_copy_watermark` (\n `source` text PRIMARY KEY NOT NULL,\n `last_rowid` integer DEFAULT 0 NOT NULL\n);\n--> statement-breakpoint\n-- Serves the time-range read family that scans a single event_type across a\n-- start/end window (e.g. the Activity timeline) without a full-table scan.\nCREATE INDEX `idx_audit_type_t` ON `audit_events` (`event_type`,`started_at`);\n--> statement-breakpoint\n-- Partial expression index mirroring `idx_events_code_change_path` (0009) for\n-- the generalized audit_events/attributes pair: file-path reads filter\n-- `event_type = 'code_change' AND json_extract(attributes, '$.file_path') = :path`.\nCREATE INDEX `idx_audit_code_change_path` ON `audit_events` (json_extract(`attributes`, '$.file_path')) WHERE `event_type` = 'code_change';\n--> statement-breakpoint\n-- Synthesizes a stub session root (event_type = 'session', no attributes) for\n-- every legacy `events.metadata.sessionId` that has no audit_events row yet.\n-- audit_events.root_session_id is a self-FK, enforced with foreign-key\n-- checking on, and INSERT OR IGNORE does not suppress a foreign-key violation\n-- (only UNIQUE/PK/NOT NULL/CHECK) \u2014 so this must run before the events copy\n-- resolves root_session_id, or every session-scoped row's insert fails.\n-- started_at takes the earliest legacy occurred_at recorded under that\n-- session, so the stub root's own timeline position is never later than\n-- anything it will end up parenting.\nINSERT INTO audit_events (id, event_type, root_session_id, started_at)\nSELECT\n json_extract(metadata, '$.sessionId'),\n 'session',\n NULL,\n min(occurred_at)\nFROM events\nWHERE json_valid(metadata)\n AND json_extract(metadata, '$.sessionId') IS NOT NULL\n AND json_extract(metadata, '$.sessionId') NOT IN (SELECT id FROM audit_events)\nGROUP BY json_extract(metadata, '$.sessionId');\n"
564
+ },
565
+ {
566
+ tag: "0014_drop_legacy_events_findings",
567
+ sql: "-- Custom migration: drops the frozen legacy `events`/`findings` tables\n-- (superseded by audit_events/inspection_definitions/inspection_findings) and\n-- replaces them with read-only views of the same name.\n--\n-- persistence's migrations.ts applies this migration ONLY once the batched\n-- history backfill (see runLegacyHistoryBackfill) has fully drained both\n-- tables, and only after copying the live file aside \u2014 see\n-- backupBeforeLegacyDrop in packages/persistence/src/migrations.ts. A store\n-- still mid-copy keeps its real tables and this migration stays pending.\n--\n-- The views exist for skew: the `aka` CLI and the Claude Code plugin update\n-- independently against one shared store, so an older, already-installed\n-- binary can open a store a newer binary already dropped these tables on.\n-- Every already-shipped repository constructor prepares its SQL eagerly at\n-- open time, so a bare \"table not found\" would fail the WHOLE open, not just\n-- a findings-specific read \u2014 these views keep `prepare()` succeeding so an\n-- old binary's unrelated features keep working; its own reads stay truthful,\n-- and its rare (already fail-open) writes fail at run time instead.\n--\n-- The 0009/0010 expression indexes existed only over the legacy `events`\n-- table; DROP TABLE below would remove them implicitly, but they are\n-- dropped explicitly first so the intent reads clearly. IF EXISTS so a store\n-- that reached here with an index missing out of band (an adopted-tag store\n-- whose physical index was never built) does not throw a deterministic \"no\n-- such index\" out of the fail-open drop path.\nDROP INDEX IF EXISTS `idx_events_code_change_path`;\n--> statement-breakpoint\nDROP INDEX IF EXISTS `idx_events_session_id`;\n--> statement-breakpoint\n-- `findings.event_id` references `events.id` \u2014 drop the child first.\nDROP TABLE `findings`;\n--> statement-breakpoint\nDROP TABLE `events`;\n--> statement-breakpoint\n-- Legacy `events` shape, projected from audit_events: `kind`/`occurred_at`/\n-- `source_tool` become real columns again (source_tool round-trips through\n-- the attributes bag, the only place a capture-typed audit row keeps it);\n-- `metadata` is reconstructed as the old camelCase JSON object from the new\n-- snake_case attributes bag, with `root_session_id` folded back in as\n-- `sessionId` (the one legacy metadata key that became a column, never an\n-- attribute, on the new table). Constrained to the four capture kinds so\n-- structural rows (session/run/tool_call/llm_call/source_lookup/config_scan)\n-- never leak into a legacy reader's result set \u2014 the old `events` table\n-- never held them either.\nCREATE VIEW `events` AS\nSELECT\n id,\n json_extract(attributes, '$.source_tool') AS source_tool,\n event_type AS kind,\n started_at AS occurred_at,\n content_hash,\n content,\n -- Plugin-local bookkeeping column that `ensureSyncedAtColumn` adds to the\n -- real `events` table at open time. A pre-cutover binary runs that probe on\n -- EVERY open; without this projection its `columnNames('events')` check\n -- misses `synced_at` and issues `ALTER TABLE events ADD COLUMN` against this\n -- view, which SQLite rejects (\"Cannot add a column to a view\") \u2014 a hard,\n -- non-fail-open crash of the whole open, the exact skew failure these views\n -- exist to prevent. Projecting it (always NULL; no reader consumes it)\n -- short-circuits that ALTER.\n NULL AS synced_at,\n json_object(\n 'sessionId', root_session_id,\n 'repo', json_extract(attributes, '$.repo'),\n 'filePath', json_extract(attributes, '$.file_path'),\n 'toolName', json_extract(attributes, '$.tool_name'),\n 'gitignored', json_extract(attributes, '$.gitignored'),\n 'wholeFile', json_extract(attributes, '$.whole_file'),\n 'model', json_extract(attributes, '$.model'),\n 'turnIndex', json_extract(attributes, '$.turn_index'),\n 'correlationId', json_extract(attributes, '$.correlation_id'),\n 'traceId', json_extract(attributes, '$.trace_id'),\n 'exceptionIds', json_extract(attributes, '$.exception_ids')\n ) AS metadata\nFROM audit_events\nWHERE event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- A plain single-row INSERT (the old writer's shape \u2014 no ON CONFLICT) is\n-- accepted by prepare() against a view once an INSTEAD OF INSERT trigger\n-- exists, so it fails at run time instead of at open \u2014 landing in the same\n-- fail-open path the caller already wraps its write in.\nCREATE TRIGGER `trg_events_ro`\nINSTEAD OF INSERT ON `events`\nBEGIN SELECT RAISE(ABORT, 'events is read-only; write to audit_events instead'); END;\n--> statement-breakpoint\n-- Legacy `findings` shape: rule_id/category/severity come from the joined\n-- inspection_definitions row (the legacy table inlined them per-row; the\n-- generalized schema normalizes them out into the shared definition). A view\n-- has no rowid of its own, so the underlying table's rowid is re-exposed\n-- explicitly under that name \u2014 a legacy reader ordering by `f.rowid` would\n-- otherwise fail to resolve the column at all. Joined to audit_events for the\n-- same four-capture-kind constraint as the events view above (a finding\n-- attached to a tool_call/config_scan row never existed in the legacy table\n-- either \u2014 see the persistence findings repository's identical predicate).\nCREATE VIEW `findings` AS\nSELECT\n f.id AS id,\n f.rowid AS rowid,\n f.audit_event_id AS event_id,\n d.rule_id AS rule_id,\n d.category AS category,\n d.severity AS severity,\n f.span_start AS span_start,\n f.span_end AS span_end,\n f.masked_match AS masked_match,\n f.action_taken AS action_taken,\n f.confidence AS confidence,\n f.finding_key AS finding_key,\n f.first_detected_at AS first_detected_at\nFROM inspection_findings f\nJOIN audit_events e ON e.id = f.audit_event_id\nJOIN inspection_definitions d ON d.id = f.inspection_definition_id\nWHERE e.event_type IN ('prompt', 'response', 'code_change', 'tool_use');\n--> statement-breakpoint\n-- Defense in depth only: SQLite refuses to plan ANY upsert against a view\n-- (\"cannot UPSERT a view\") no matter what trigger exists, so this can never\n-- rescue the real legacy writer, which always used\n-- `ON CONFLICT (finding_key) DO UPDATE` \u2014 that statement still fails at\n-- prepare() on an old binary, same as it would with no trigger at all. This\n-- only covers a plain-INSERT shape, should one ever exist.\nCREATE TRIGGER `trg_findings_ro`\nINSTEAD OF INSERT ON `findings`\nBEGIN SELECT RAISE(ABORT, 'findings is read-only; write to inspection_findings instead'); END;\n"
552
568
  }
553
569
  ];
554
570
 
@@ -15379,7 +15395,12 @@ var FindingFacets = external_exports.object({
15379
15395
  severity: external_exports.array(FindingFacetItem),
15380
15396
  subtype: external_exports.array(FindingFacetItem),
15381
15397
  provider: external_exports.array(FindingFacetItem),
15382
- action: external_exports.array(FindingFacetItem)
15398
+ action: external_exports.array(FindingFacetItem),
15399
+ // Counts by the group's derived status. The SQLite store derives a status
15400
+ // for every instance, so every group lands in a bucket; a status-less
15401
+ // group (possible only for callers whose rows carry no statuses) is
15402
+ // counted under no value.
15403
+ status: external_exports.array(FindingFacetItem)
15383
15404
  }).meta({ id: "FindingFacets" });
15384
15405
  var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
15385
15406
  var ListGroupedFindingsQuery = external_exports.object({
@@ -15389,6 +15410,10 @@ var ListGroupedFindingsQuery = external_exports.object({
15389
15410
  subtype: external_exports.array(external_exports.string()).optional(),
15390
15411
  provider: external_exports.array(FindingProvider).optional(),
15391
15412
  action: external_exports.array(FindingAction).optional(),
15413
+ // Matches a group's DERIVED status (see FindingGroup.status), not its
15414
+ // individual instances' — so a filtered group's Status column always reads
15415
+ // one of the requested values.
15416
+ status: external_exports.array(FindingStatus).optional(),
15392
15417
  q: external_exports.string().optional(),
15393
15418
  // Scope to findings whose event carries this session id (the Activity page's
15394
15419
  // session → findings drilldown). Findings without a session never match.
@@ -15579,6 +15604,33 @@ var ToolCallAttributes = external_exports.object({
15579
15604
  parent_uuid: external_exports.string().optional(),
15580
15605
  run_key: external_exports.string().optional()
15581
15606
  }).catchall(external_exports.unknown());
15607
+ var CaptureAttributes = external_exports.object({
15608
+ // The harness/tool that produced the capture (`claude-code`, `cli`, …). A
15609
+ // column on the legacy `events` table; here it rides the bag because a
15610
+ // capture-typed audit row has no equivalent column of its own.
15611
+ source_tool: external_exports.string().optional(),
15612
+ file_path: external_exports.string().optional(),
15613
+ repo: external_exports.string().optional(),
15614
+ // The host tool whose input/output was scanned (e.g. 'Bash', 'WebFetch') —
15615
+ // gives a non-file capture a display location ("via Bash") when file_path
15616
+ // is absent. The tool NAME only, never its arguments/output.
15617
+ tool_name: external_exports.string().optional(),
15618
+ // Presence-only provenance flag: set when the file is excluded by the
15619
+ // repo's .gitignore. Omitted (not false) for tracked files.
15620
+ gitignored: external_exports.boolean().optional(),
15621
+ // Set ONLY when the capture is a COMPLETE file snapshot (a worktree scan
15622
+ // reading from disk), never a partial fragment (a hook-captured edit).
15623
+ whole_file: external_exports.boolean().optional(),
15624
+ // Distributed-tracing correlation: `correlation_id` ties the capture back to
15625
+ // the request that produced it; `trace_id` is the originating span's W3C
15626
+ // trace id when telemetry is enabled.
15627
+ correlation_id: external_exports.uuid().optional(),
15628
+ trace_id: external_exports.string().regex(/^[0-9a-f]{32}$/).optional(),
15629
+ // Ids of the detection exceptions that downgraded findings in this capture
15630
+ // to 'allow' — the enforcement audit trail's link back to the grant that
15631
+ // authorized the bypass.
15632
+ exception_ids: external_exports.array(external_exports.guid()).optional()
15633
+ }).catchall(external_exports.unknown());
15582
15634
  var ToolCallInspection = external_exports.object({
15583
15635
  ruleId: external_exports.string().min(1),
15584
15636
  ruleName: external_exports.string(),
@@ -15665,7 +15717,18 @@ var InspectionFindingInput = external_exports.object({
15665
15717
  span: Span,
15666
15718
  maskedMatch: external_exports.string(),
15667
15719
  actionTaken: ActionTaken,
15668
- confidence: external_exports.number().min(0).max(1)
15720
+ confidence: external_exports.number().min(0).max(1),
15721
+ // Stable, content-addressed key correlating this finding across re-detections
15722
+ // — mirrors the legacy `findings.finding_key` (uq_inspection_findings_key is
15723
+ // its unique index). Optional: only an at-rest/re-scannable finding carries
15724
+ // one; an in-flight capture (prompt/response) has nothing to re-detect
15725
+ // against and leaves it unset, so every insert is a fresh row.
15726
+ findingKey: external_exports.string().optional(),
15727
+ // The ORIGINAL detection time, preserved across a later re-detection of the
15728
+ // same findingKey — mirrors the legacy `findings.first_detected_at`.
15729
+ // Optional: when omitted, the writer derives it from the referenced audit
15730
+ // event's startedAt on first insert (see SqliteInspectionFindingsRepository).
15731
+ firstDetectedAt: external_exports.iso.datetime().optional()
15669
15732
  });
15670
15733
  var InventoryContext = external_exports.object({
15671
15734
  host: InventoryInput.optional(),
@@ -15867,6 +15930,7 @@ var ActivityOverviewResponse = external_exports.object({
15867
15930
 
15868
15931
  // ../../packages/schema/src/zod/event.ts
15869
15932
  var EventKind = external_exports.enum(["prompt", "response", "code_change", "tool_use"]).meta({ id: "EventKind" });
15933
+ var CAPTURE_EVENT_TYPES_SQL = EventKind.options.map((k) => `'${k}'`).join(",");
15870
15934
  var SourceTool = external_exports.enum(["claude-code", "claude-desktop", "cursor", "chatgpt", "github-copilot", "cli", "unknown"]).meta({ id: "SourceTool" });
15871
15935
  var EventMetadata = external_exports.object({
15872
15936
  sessionId: external_exports.string().optional(),
@@ -16356,6 +16420,12 @@ var PolicyBundle = external_exports.object({
16356
16420
  // on-disk caches — that omit the field still parse; consumers read
16357
16421
  // `bundle.exceptions ?? []`.
16358
16422
  exceptions: external_exports.array(ExceptionBundleEntry).optional(),
16423
+ // Installed pack version, keyed by ruleId, for rules in `rules` that came
16424
+ // from a versioned installed pack. Optional so older backends — and older
16425
+ // on-disk caches — that omit the field still parse; consumers fall back to
16426
+ // the rule's own spec version. NOT the bundle version above — see
16427
+ // installedRuleset's ruleVersions for the source of truth.
16428
+ ruleVersions: external_exports.record(external_exports.string(), external_exports.string()).optional(),
16359
16429
  customKeywords: external_exports.array(external_exports.string()),
16360
16430
  fetchedAt: external_exports.iso.datetime()
16361
16431
  }).meta({ id: "PolicyBundle" });
@@ -17269,6 +17339,15 @@ function groupActions(g) {
17269
17339
  actionsCache.set(g, actions);
17270
17340
  return actions;
17271
17341
  }
17342
+ function countInstancesByStatus(statusInputs, statuses) {
17343
+ const statusSet = new Set(statuses);
17344
+ let sum = 0;
17345
+ for (const input of statusInputs) {
17346
+ if (input.count === void 0) return null;
17347
+ if (statusSet.has(deriveFindingStatus(input))) sum += input.count;
17348
+ }
17349
+ return sum;
17350
+ }
17272
17351
  function applyFindingFilters(groups, opts) {
17273
17352
  let filtered = groups;
17274
17353
  if (opts.severity && opts.severity.length > 0) {
@@ -17287,6 +17366,10 @@ function applyFindingFilters(groups, opts) {
17287
17366
  const subtypeSet = new Set(opts.subtype);
17288
17367
  filtered = filtered.filter((g) => subtypeSet.has(g.subtype));
17289
17368
  }
17369
+ if (opts.statuses && opts.statuses.length > 0) {
17370
+ const statusSet = new Set(opts.statuses);
17371
+ filtered = filtered.filter((g) => g.status !== void 0 && statusSet.has(g.status));
17372
+ }
17290
17373
  if (opts.q) {
17291
17374
  const q = opts.q.toLowerCase();
17292
17375
  filtered = filtered.filter((g) => groupHaystack(g).includes(q));
@@ -17308,6 +17391,7 @@ function computeFindingFacets(allGroups, opts) {
17308
17391
  const forSeverity = applyFindingFilters(allGroups, {
17309
17392
  providers: opts.providers,
17310
17393
  actions: opts.actions,
17394
+ statuses: opts.statuses,
17311
17395
  q: opts.q,
17312
17396
  subtype: opts.subtype
17313
17397
  });
@@ -17317,6 +17401,7 @@ function computeFindingFacets(allGroups, opts) {
17317
17401
  }
17318
17402
  const forProvider = applyFindingFilters(allGroups, {
17319
17403
  actions: opts.actions,
17404
+ statuses: opts.statuses,
17320
17405
  q: opts.q,
17321
17406
  subtype: opts.subtype,
17322
17407
  severity: opts.severity
@@ -17327,6 +17412,7 @@ function computeFindingFacets(allGroups, opts) {
17327
17412
  }
17328
17413
  const forAction = applyFindingFilters(allGroups, {
17329
17414
  providers: opts.providers,
17415
+ statuses: opts.statuses,
17330
17416
  q: opts.q,
17331
17417
  subtype: opts.subtype,
17332
17418
  severity: opts.severity
@@ -17338,17 +17424,30 @@ function computeFindingFacets(allGroups, opts) {
17338
17424
  const forSubtype = applyFindingFilters(allGroups, {
17339
17425
  providers: opts.providers,
17340
17426
  actions: opts.actions,
17427
+ statuses: opts.statuses,
17341
17428
  q: opts.q,
17342
17429
  severity: opts.severity
17343
17430
  });
17344
17431
  const subtypeMap = /* @__PURE__ */ new Map();
17345
17432
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
17433
+ const forStatus = applyFindingFilters(allGroups, {
17434
+ providers: opts.providers,
17435
+ actions: opts.actions,
17436
+ q: opts.q,
17437
+ subtype: opts.subtype,
17438
+ severity: opts.severity
17439
+ });
17440
+ const statusMap = /* @__PURE__ */ new Map();
17441
+ for (const g of forStatus) {
17442
+ if (g.status !== void 0) statusMap.set(g.status, (statusMap.get(g.status) ?? 0) + 1);
17443
+ }
17346
17444
  const toItems = (m) => [...m.entries()].map(([value, count]) => ({ value, count }));
17347
17445
  return {
17348
17446
  severity: toItems(severityMap),
17349
17447
  provider: toItems(providerMap),
17350
17448
  action: toItems(actionMap),
17351
- subtype: toItems(subtypeMap)
17449
+ subtype: toItems(subtypeMap),
17450
+ status: toItems(statusMap)
17352
17451
  };
17353
17452
  }
17354
17453
 
@@ -17383,10 +17482,14 @@ var PatchInstalledPackRequest = external_exports.object({
17383
17482
  }).meta({ id: "PatchInstalledPackRequest" });
17384
17483
 
17385
17484
  // ../../packages/schema/src/zod/local.ts
17386
- var WORKSPACE_SETTINGS_SPEC_VERSION = 3;
17485
+ var WORKSPACE_SETTINGS_SPEC_VERSION = 4;
17387
17486
  var RunMode = external_exports.enum(["standalone"]);
17388
17487
  var SimpleDetectionPolicy = external_exports.enum(["redact", "warn"]);
17389
17488
  var HistoricalAccess = external_exports.enum(["full", "session-only"]);
17489
+ var ModelJudgeConsent = external_exports.object({
17490
+ acknowledgedAt: external_exports.iso.datetime(),
17491
+ payloadVersion: external_exports.number().int().positive()
17492
+ });
17390
17493
  var WorkspaceSettings = external_exports.object({
17391
17494
  specVersion: external_exports.number().int().positive().default(WORKSPACE_SETTINGS_SPEC_VERSION),
17392
17495
  // Settings files written by earlier releases may carry the retired 'attached'
@@ -17402,37 +17505,16 @@ var WorkspaceSettings = external_exports.object({
17402
17505
  // Shares writes.
17403
17506
  dataSharesInPlace: external_exports.boolean().default(true),
17404
17507
  // Absent until /aka:setup completes; its presence is what "onboarded" means.
17405
- onboardedAt: external_exports.iso.datetime().optional()
17508
+ onboardedAt: external_exports.iso.datetime().optional(),
17509
+ // Records that the user consented to sending findings to the model API for
17510
+ // the /aka:setup judge, along with the payload-shape version they agreed to.
17511
+ // Absent until granted; a stale payloadVersion means the consent no longer
17512
+ // covers the current payload and must be re-granted.
17513
+ modelJudgeConsent: ModelJudgeConsent.optional()
17406
17514
  });
17407
17515
  function defaultWorkspaceSettings() {
17408
17516
  return WorkspaceSettings.parse({});
17409
17517
  }
17410
- function toEventRow(event) {
17411
- return {
17412
- id: event.id,
17413
- sourceTool: event.sourceTool,
17414
- kind: event.kind,
17415
- occurredAt: isoToEpochMillis(event.occurredAt),
17416
- contentHash: event.contentHash,
17417
- content: event.content,
17418
- metadata: event.metadata ? JSON.stringify(event.metadata) : null
17419
- };
17420
- }
17421
- function toFindingRow(finding2) {
17422
- return {
17423
- id: finding2.id,
17424
- eventId: finding2.eventId,
17425
- ruleId: finding2.ruleId,
17426
- category: finding2.category,
17427
- severity: finding2.severity,
17428
- spanStart: finding2.span.start,
17429
- spanEnd: finding2.span.end,
17430
- maskedMatch: finding2.maskedMatch,
17431
- actionTaken: finding2.actionTaken,
17432
- confidence: finding2.confidence,
17433
- findingKey: finding2.findingKey ?? null
17434
- };
17435
- }
17436
17518
  function toInventoryRow(input, id, now) {
17437
17519
  return {
17438
17520
  id,
@@ -17502,7 +17584,42 @@ function toInspectionFindingRow(input) {
17502
17584
  spanEnd: input.span.end,
17503
17585
  maskedMatch: input.maskedMatch,
17504
17586
  actionTaken: input.actionTaken,
17505
- confidence: input.confidence
17587
+ confidence: input.confidence,
17588
+ findingKey: input.findingKey ?? null,
17589
+ firstDetectedAt: input.firstDetectedAt ? isoToEpochMillis(input.firstDetectedAt) : null
17590
+ };
17591
+ }
17592
+ function toCaptureAttributes(event) {
17593
+ const metadata = event.metadata;
17594
+ return {
17595
+ source_tool: event.sourceTool,
17596
+ ...metadata?.repo !== void 0 ? { repo: metadata.repo } : {},
17597
+ ...metadata?.filePath !== void 0 ? { file_path: metadata.filePath } : {},
17598
+ ...metadata?.toolName !== void 0 ? { tool_name: metadata.toolName } : {},
17599
+ ...metadata?.gitignored !== void 0 ? { gitignored: metadata.gitignored } : {},
17600
+ ...metadata?.wholeFile !== void 0 ? { whole_file: metadata.wholeFile } : {},
17601
+ ...metadata?.correlationId !== void 0 ? { correlation_id: metadata.correlationId } : {},
17602
+ ...metadata?.traceId !== void 0 ? { trace_id: metadata.traceId } : {},
17603
+ ...metadata?.exceptionIds !== void 0 ? { exception_ids: metadata.exceptionIds } : {},
17604
+ // `model`/`turnIndex` have no dedicated CaptureAttributes field (no writer
17605
+ // has ever populated either), but every legacy metadata key still rides
17606
+ // the bag rather than being silently dropped — CaptureAttributes'
17607
+ // `.catchall(z.unknown())` carries the long tail.
17608
+ ...metadata?.model !== void 0 ? { model: metadata.model } : {},
17609
+ ...metadata?.turnIndex !== void 0 ? { turn_index: metadata.turnIndex } : {}
17610
+ };
17611
+ }
17612
+ function captureDefinitionVersion(finding2) {
17613
+ return `capture/${finding2.category}/${finding2.severity}`;
17614
+ }
17615
+ function toCaptureDefinitionInput(finding2) {
17616
+ return {
17617
+ ruleId: finding2.ruleId,
17618
+ version: captureDefinitionVersion(finding2),
17619
+ name: finding2.ruleId,
17620
+ category: finding2.category,
17621
+ severity: finding2.severity,
17622
+ definition: JSON.stringify({ ruleId: finding2.ruleId })
17506
17623
  };
17507
17624
  }
17508
17625
 
@@ -17930,6 +18047,48 @@ function reviewSeverityRank(reasons) {
17930
18047
  return Math.min(...reasons.map((r) => REVIEW_SEVERITY_RANK[r]));
17931
18048
  }
17932
18049
 
18050
+ // ../../packages/persistence/src/ids.ts
18051
+ import { createHash } from "crypto";
18052
+ function sha256Hex(input) {
18053
+ return createHash("sha256").update(input).digest("hex");
18054
+ }
18055
+ function inventoryId(objectType, identityKey) {
18056
+ return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18057
+ }
18058
+ function sourceProjectId(url2) {
18059
+ return sha256Hex(canonicalIdentity(["source_project", url2]));
18060
+ }
18061
+ function classifiedDataId(cls) {
18062
+ return sha256Hex(canonicalIdentity(["classified_data", cls]));
18063
+ }
18064
+ function inspectionDefinitionId(ruleId, version2) {
18065
+ return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18066
+ }
18067
+ function llmCallId(sessionId, messageId) {
18068
+ return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18069
+ }
18070
+ function toolCallId(sessionId, toolUseId) {
18071
+ return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18072
+ }
18073
+ function inspectionFindingId(auditEventId, ruleId, spanStart, spanEnd) {
18074
+ return sha256Hex(
18075
+ canonicalIdentity([
18076
+ "inspection_finding",
18077
+ auditEventId,
18078
+ ruleId,
18079
+ String(spanStart),
18080
+ String(spanEnd)
18081
+ ])
18082
+ );
18083
+ }
18084
+ var NO_SESSION = "no_session";
18085
+ var NO_PATH = "no_path";
18086
+ function captureId(sessionId, contentHash, filePath = null) {
18087
+ return sha256Hex(
18088
+ canonicalIdentity(["capture", sessionId ?? NO_SESSION, contentHash, filePath ?? NO_PATH])
18089
+ );
18090
+ }
18091
+
17933
18092
  // ../../packages/persistence/src/internal/sql-text.ts
17934
18093
  function escapeLikePattern(s) {
17935
18094
  return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
@@ -18026,39 +18185,81 @@ function evidenceExists(db, object2) {
18026
18185
  return schemaObjectExists(db, "table", object2.name);
18027
18186
  }
18028
18187
 
18029
- // ../../packages/persistence/src/ids.ts
18030
- import { createHash } from "crypto";
18031
- function sha256Hex(input) {
18032
- return createHash("sha256").update(input).digest("hex");
18188
+ // ../../packages/persistence/src/internal/rows.ts
18189
+ function allRows(stmt, params) {
18190
+ if (params === void 0) return stmt.all();
18191
+ if (Array.isArray(params)) return stmt.all(...params);
18192
+ return stmt.all(params);
18033
18193
  }
18034
- function inventoryId(objectType, identityKey) {
18035
- return sha256Hex(canonicalIdentity(["inventory", objectType, identityKey]));
18194
+ function getRow(stmt, params) {
18195
+ if (params === void 0) return stmt.get();
18196
+ if (Array.isArray(params)) return stmt.get(...params);
18197
+ return stmt.get(params);
18036
18198
  }
18037
- function sourceProjectId(url2) {
18038
- return sha256Hex(canonicalIdentity(["source_project", url2]));
18199
+ function intToBool(raw) {
18200
+ return raw === 1 || raw === true;
18039
18201
  }
18040
- function classifiedDataId(cls) {
18041
- return sha256Hex(canonicalIdentity(["classified_data", cls]));
18202
+ function boolToInt(b) {
18203
+ return b ? 1 : 0;
18042
18204
  }
18043
- function inspectionDefinitionId(ruleId, version2) {
18044
- return sha256Hex(canonicalIdentity(["inspection_definition", ruleId, version2]));
18205
+ function bindParams(row) {
18206
+ const out = {};
18207
+ for (const [key, value] of Object.entries(row)) {
18208
+ out[key] = value === void 0 ? null : value;
18209
+ }
18210
+ return out;
18045
18211
  }
18046
- function llmCallId(sessionId, messageId) {
18047
- return sha256Hex(canonicalIdentity(["audit_event_llm_call", sessionId, messageId]));
18212
+ function countScalar(db, sql, params) {
18213
+ return getRow(db.prepare(sql), params)?.n ?? 0;
18048
18214
  }
18049
- function toolCallId(sessionId, toolUseId) {
18050
- return sha256Hex(canonicalIdentity(["audit_event_tool_call", sessionId, toolUseId]));
18215
+ function countBy(db, sql, params) {
18216
+ const map2 = /* @__PURE__ */ new Map();
18217
+ for (const row of allRows(db.prepare(sql), params)) {
18218
+ map2.set(row.k, row.n);
18219
+ }
18220
+ return map2;
18051
18221
  }
18052
- function inspectionFindingId(auditEventId, definitionId, spanStart, spanEnd) {
18053
- return sha256Hex(
18054
- canonicalIdentity([
18055
- "inspection_finding",
18056
- auditEventId,
18057
- definitionId,
18058
- String(spanStart),
18059
- String(spanEnd)
18060
- ])
18061
- );
18222
+ function mapRowsTolerant(rows, map2) {
18223
+ const out = [];
18224
+ for (const row of rows) {
18225
+ try {
18226
+ out.push(map2(row));
18227
+ } catch {
18228
+ }
18229
+ }
18230
+ return out;
18231
+ }
18232
+
18233
+ // ../../packages/persistence/src/paths.ts
18234
+ import { chmodSync, lstatSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
18235
+ var DATA_DIR_MODE = 448;
18236
+ var DATA_FILE_MODE = 384;
18237
+ var DB_FILENAME = "aka.db";
18238
+ function chmodBestEffort(path, mode) {
18239
+ try {
18240
+ chmodSync(path, mode);
18241
+ } catch {
18242
+ }
18243
+ }
18244
+ function tightenDir(dir) {
18245
+ chmodBestEffort(dir, DATA_DIR_MODE);
18246
+ }
18247
+ function ensureDataDirSync(dir) {
18248
+ mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18249
+ tightenDir(dir);
18250
+ }
18251
+ function dbSidecars(file2) {
18252
+ return [`${file2}-wal`, `${file2}-shm`, `${file2}-journal`];
18253
+ }
18254
+ function tightenFile(file2) {
18255
+ try {
18256
+ if (lstatSync(file2).isSymbolicLink()) return;
18257
+ } catch {
18258
+ }
18259
+ chmodBestEffort(file2, DATA_FILE_MODE);
18260
+ }
18261
+ function tightenPerms(file2) {
18262
+ for (const path of [file2, ...dbSidecars(file2)]) chmodBestEffort(path, DATA_FILE_MODE);
18062
18263
  }
18063
18264
 
18064
18265
  // ../../packages/persistence/src/migrations.ts
@@ -18072,7 +18273,8 @@ function createdIndexName(statement) {
18072
18273
  const body = statement.replace(/^(?:\s*--[^\n]*\n?)+/, "").trimStart();
18073
18274
  return /^CREATE (?:UNIQUE )?INDEX (?:IF NOT EXISTS )?`([^`]+)`/.exec(body)?.[1];
18074
18275
  }
18075
- function applyMigrations(db) {
18276
+ var LEGACY_DROP_MIGRATION_TAG = "0014_drop_legacy_events_findings";
18277
+ function applyMigrations(db, file2) {
18076
18278
  const legacyCount = db.prepare("PRAGMA user_version").get().user_version;
18077
18279
  db.exec(
18078
18280
  "CREATE TABLE IF NOT EXISTS migration_ledger (tag TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)"
@@ -18086,6 +18288,7 @@ function applyMigrations(db) {
18086
18288
  );
18087
18289
  for (const [index, migration] of SQLITE_MIGRATIONS.entries()) {
18088
18290
  if (applied.has(migration.tag)) continue;
18291
+ if (migration.tag === LEGACY_DROP_MIGRATION_TAG) continue;
18089
18292
  const evidence = evidenceObjects(migration.sql);
18090
18293
  const present = evidence.filter((o) => evidenceExists(db, o));
18091
18294
  if (present.length > 0 && present.length < evidence.length) {
@@ -18130,7 +18333,6 @@ function applyMigrations(db) {
18130
18333
  if (legacyCount < SQLITE_MIGRATIONS.length) {
18131
18334
  db.exec(`PRAGMA user_version = ${String(SQLITE_MIGRATIONS.length)}`);
18132
18335
  }
18133
- ensureSyncedAtColumn(db, "events");
18134
18336
  ensureSyncedAtColumn(db, "audit_events");
18135
18337
  ensureScanLedgerTable(db);
18136
18338
  ensureBlockedDetectionsTable(db);
@@ -18138,6 +18340,47 @@ function applyMigrations(db) {
18138
18340
  ensureWriteGateTrigger(db);
18139
18341
  ensureTokenUsageColumns(db);
18140
18342
  reconcileSourceProjectIds(db);
18343
+ if (!applied.has(LEGACY_DROP_MIGRATION_TAG)) {
18344
+ const drained = runLegacyHistoryBackfill(db);
18345
+ if (drained) applyLegacyDropMigration(db, file2);
18346
+ }
18347
+ }
18348
+ function applyLegacyDropMigration(db, file2) {
18349
+ const migration = SQLITE_MIGRATIONS.find((m) => m.tag === LEGACY_DROP_MIGRATION_TAG);
18350
+ if (!migration) return;
18351
+ if (file2) {
18352
+ try {
18353
+ backupBeforeLegacyDrop(db, file2);
18354
+ } catch (error51) {
18355
+ akaWarn(`legacy events/findings backup failed; deferring the drop: ${String(error51)}`);
18356
+ return;
18357
+ }
18358
+ }
18359
+ try {
18360
+ withTransaction(
18361
+ db,
18362
+ () => {
18363
+ const alreadyDropped = db.prepare("SELECT 1 FROM migration_ledger WHERE tag = ?").get(migration.tag);
18364
+ if (alreadyDropped) return;
18365
+ for (const statement of splitStatements(migration.sql)) {
18366
+ db.exec(statement);
18367
+ }
18368
+ db.prepare("INSERT OR IGNORE INTO migration_ledger (tag, applied_at) VALUES (?, ?)").run(
18369
+ migration.tag,
18370
+ Date.now()
18371
+ );
18372
+ },
18373
+ "IMMEDIATE"
18374
+ );
18375
+ } catch (error51) {
18376
+ akaWarn(`legacy events/findings drop failed; deferring: ${String(error51)}`);
18377
+ }
18378
+ }
18379
+ function backupBeforeLegacyDrop(db, file2) {
18380
+ const backup = `${file2}.pre-drop.${String(Date.now())}.bak`;
18381
+ db.prepare("VACUUM INTO ?").run(backup);
18382
+ tightenFile(backup);
18383
+ return backup;
18141
18384
  }
18142
18385
  var TOKEN_USAGE_COLUMNS = [
18143
18386
  {
@@ -18166,6 +18409,7 @@ var TOKEN_USAGE_COLUMNS = [
18166
18409
  }
18167
18410
  ];
18168
18411
  function ensureTokenUsageColumns(db) {
18412
+ if (!schemaObjectExists(db, "table", "audit_events")) return;
18169
18413
  const existing = new Set(columnNames(db, "audit_events", { includeGenerated: true }));
18170
18414
  for (const column of TOKEN_USAGE_COLUMNS) {
18171
18415
  if (!existing.has(column.name)) {
@@ -18231,11 +18475,187 @@ function reconcileSourceProjectIds(db) {
18231
18475
  akaWarn(`source_project id reconcile failed: ${String(error51)}`);
18232
18476
  }
18233
18477
  }
18478
+ var LEGACY_BACKFILL_BATCH_SIZE = 200;
18479
+ var LEGACY_BACKFILL_MAX_ROWS_PER_CALL = 1e3;
18480
+ function getLegacyCopyWatermark(db, source) {
18481
+ const row = db.prepare("SELECT last_rowid AS lastRowid FROM legacy_copy_watermark WHERE source = ?").get(source);
18482
+ return row?.lastRowid ?? 0;
18483
+ }
18484
+ function setLegacyCopyWatermark(db, source, lastRowid) {
18485
+ db.prepare(
18486
+ `INSERT INTO legacy_copy_watermark (source, last_rowid) VALUES (?, ?)
18487
+ ON CONFLICT(source) DO UPDATE SET last_rowid = excluded.last_rowid`
18488
+ ).run(source, lastRowid);
18489
+ }
18490
+ function drainLegacyTable(db, source, selectStmt, handleRows) {
18491
+ let watermark = getLegacyCopyWatermark(db, source);
18492
+ let processed = 0;
18493
+ while (processed < LEGACY_BACKFILL_MAX_ROWS_PER_CALL) {
18494
+ const rows = selectStmt.all(watermark, LEGACY_BACKFILL_BATCH_SIZE);
18495
+ if (rows.length === 0) return true;
18496
+ withTransaction(
18497
+ db,
18498
+ () => {
18499
+ handleRows(rows);
18500
+ watermark = rows[rows.length - 1]?.rowid ?? watermark;
18501
+ setLegacyCopyWatermark(db, source, watermark);
18502
+ },
18503
+ "IMMEDIATE"
18504
+ );
18505
+ processed += rows.length;
18506
+ if (rows.length < LEGACY_BACKFILL_BATCH_SIZE) return true;
18507
+ }
18508
+ return false;
18509
+ }
18510
+ function parseLegacyEventMetadata(raw) {
18511
+ if (raw === null) return void 0;
18512
+ try {
18513
+ return JSON.parse(raw);
18514
+ } catch {
18515
+ return void 0;
18516
+ }
18517
+ }
18518
+ function toLegacyAuditAttributesJson(row) {
18519
+ return JSON.stringify(
18520
+ toCaptureAttributes({
18521
+ id: row.id,
18522
+ sourceTool: row.sourceTool,
18523
+ kind: row.kind,
18524
+ occurredAt: new Date(row.occurredAt).toISOString(),
18525
+ contentHash: row.contentHash,
18526
+ content: row.content,
18527
+ metadata: row.metadata
18528
+ })
18529
+ );
18530
+ }
18531
+ function copyLegacyEvents(db) {
18532
+ const selectStmt = db.prepare(
18533
+ `SELECT rowid AS rowid, id, source_tool AS sourceTool, kind, occurred_at AS occurredAt,
18534
+ content_hash AS contentHash, content, metadata
18535
+ FROM events WHERE rowid > ? ORDER BY rowid LIMIT ?`
18536
+ );
18537
+ const insertStmt = db.prepare(
18538
+ `INSERT OR IGNORE INTO audit_events
18539
+ (id, parent_id, root_session_id, event_type, started_at, content, content_hash, attributes)
18540
+ VALUES (:id, :parentId, :rootSessionId, :eventType, :startedAt, :content, :contentHash, :attributes)`
18541
+ );
18542
+ const stubRootStmt = db.prepare(
18543
+ `INSERT OR IGNORE INTO audit_events (id, event_type, started_at) VALUES (?, 'session', ?)`
18544
+ );
18545
+ return drainLegacyTable(
18546
+ db,
18547
+ "events",
18548
+ selectStmt,
18549
+ (rows) => {
18550
+ for (const row of rows) {
18551
+ const metadata = parseLegacyEventMetadata(row.metadata);
18552
+ const sessionId = metadata?.sessionId ?? null;
18553
+ if (sessionId !== null) stubRootStmt.run(sessionId, row.occurredAt);
18554
+ insertStmt.run(
18555
+ bindParams({
18556
+ id: row.id,
18557
+ parentId: sessionId,
18558
+ rootSessionId: sessionId,
18559
+ eventType: row.kind,
18560
+ startedAt: row.occurredAt,
18561
+ content: row.content,
18562
+ contentHash: row.contentHash,
18563
+ attributes: toLegacyAuditAttributesJson({ ...row, metadata })
18564
+ })
18565
+ );
18566
+ }
18567
+ }
18568
+ );
18569
+ }
18570
+ function copyLegacyFindings(db) {
18571
+ const selectStmt = db.prepare(
18572
+ `SELECT rowid AS rowid, id, event_id AS eventId, rule_id AS ruleId, category, severity,
18573
+ span_start AS spanStart, span_end AS spanEnd, masked_match AS maskedMatch,
18574
+ action_taken AS actionTaken, confidence, finding_key AS findingKey,
18575
+ first_detected_at AS firstDetectedAt
18576
+ FROM findings WHERE rowid > ? ORDER BY rowid LIMIT ?`
18577
+ );
18578
+ const definitionStmt = db.prepare(
18579
+ `INSERT OR IGNORE INTO inspection_definitions
18580
+ (id, rule_id, name, category, severity, definition, version)
18581
+ VALUES (:id, :ruleId, :name, :category, :severity, :definition, :version)`
18582
+ );
18583
+ const findingStmt = db.prepare(
18584
+ `INSERT INTO inspection_findings
18585
+ (id, audit_event_id, inspection_definition_id, classified_data_id,
18586
+ span_start, span_end, masked_match, action_taken, confidence,
18587
+ finding_key, first_detected_at)
18588
+ VALUES
18589
+ (:id, :auditEventId, :inspectionDefinitionId, NULL,
18590
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
18591
+ :findingKey, :firstDetectedAt)
18592
+ ON CONFLICT(id) DO NOTHING
18593
+ ON CONFLICT (finding_key) DO UPDATE SET
18594
+ first_detected_at = CASE
18595
+ WHEN first_detected_at IS NULL THEN excluded.first_detected_at
18596
+ WHEN excluded.first_detected_at IS NULL THEN first_detected_at
18597
+ ELSE min(first_detected_at, excluded.first_detected_at)
18598
+ END`
18599
+ );
18600
+ return drainLegacyTable(
18601
+ db,
18602
+ "findings",
18603
+ selectStmt,
18604
+ (rows) => {
18605
+ const definitionIds = /* @__PURE__ */ new Map();
18606
+ for (const row of rows) {
18607
+ const tupleKey = JSON.stringify([row.ruleId, row.category, row.severity]);
18608
+ let definitionId = definitionIds.get(tupleKey);
18609
+ if (definitionId === void 0) {
18610
+ const version2 = `unmigrated/${row.category}/${row.severity}`;
18611
+ definitionId = inspectionDefinitionId(row.ruleId, version2);
18612
+ definitionStmt.run(
18613
+ bindParams({
18614
+ id: definitionId,
18615
+ ruleId: row.ruleId,
18616
+ name: row.ruleId,
18617
+ category: row.category,
18618
+ severity: row.severity,
18619
+ definition: "",
18620
+ version: version2
18621
+ })
18622
+ );
18623
+ definitionIds.set(tupleKey, definitionId);
18624
+ }
18625
+ findingStmt.run(
18626
+ bindParams({
18627
+ id: row.id,
18628
+ auditEventId: row.eventId,
18629
+ inspectionDefinitionId: definitionId,
18630
+ spanStart: row.spanStart,
18631
+ spanEnd: row.spanEnd,
18632
+ maskedMatch: row.maskedMatch,
18633
+ actionTaken: row.actionTaken,
18634
+ confidence: row.confidence,
18635
+ findingKey: row.findingKey,
18636
+ firstDetectedAt: row.firstDetectedAt
18637
+ })
18638
+ );
18639
+ }
18640
+ }
18641
+ );
18642
+ }
18643
+ function runLegacyHistoryBackfill(db) {
18644
+ try {
18645
+ const eventsCaughtUp = copyLegacyEvents(db);
18646
+ if (!eventsCaughtUp) return false;
18647
+ return copyLegacyFindings(db);
18648
+ } catch (error51) {
18649
+ akaWarn(`legacy history backfill failed: ${String(error51)}`);
18650
+ return false;
18651
+ }
18652
+ }
18234
18653
  function isForeignSqliteLineage(db) {
18235
18654
  if (schemaObjectExists(db, "table", "tenants")) return true;
18236
18655
  return columnNames(db, "events").includes("tenant_id");
18237
18656
  }
18238
18657
  function ensureSyncedAtColumn(db, table) {
18658
+ if (!schemaObjectExists(db, "table", table)) return;
18239
18659
  if (!columnNames(db, table).includes("synced_at")) {
18240
18660
  db.exec(`ALTER TABLE ${table} ADD COLUMN synced_at integer`);
18241
18661
  }
@@ -18256,6 +18676,7 @@ function ensureWriteGateTrigger(db) {
18256
18676
  CONSTRAINT "ck_pack_write_gate_single_row" CHECK("_pack_write_gate"."id" = 1)
18257
18677
  )`);
18258
18678
  db.exec("INSERT OR IGNORE INTO _pack_write_gate (id, open) VALUES (1, 0)");
18679
+ if (!schemaObjectExists(db, "table", "installed_packs")) return;
18259
18680
  db.exec(`CREATE TRIGGER IF NOT EXISTS trg_installed_packs_write_gate
18260
18681
  BEFORE UPDATE OF version, name, rules_json ON installed_packs
18261
18682
  WHEN (SELECT open FROM _pack_write_gate WHERE id = 1) IS NOT 1
@@ -18283,30 +18704,6 @@ function ensureRuleProbeCacheTable(db) {
18283
18704
  )`);
18284
18705
  }
18285
18706
 
18286
- // ../../packages/persistence/src/paths.ts
18287
- import { chmodSync, mkdirSync } from "fs";
18288
- var DATA_DIR_MODE = 448;
18289
- var DATA_FILE_MODE = 384;
18290
- var DB_FILENAME = "aka.db";
18291
- function ensureDataDirSync(dir) {
18292
- mkdirSync(dir, { recursive: true, mode: DATA_DIR_MODE });
18293
- try {
18294
- chmodSync(dir, DATA_DIR_MODE);
18295
- } catch {
18296
- }
18297
- }
18298
- function walSidecars(file2) {
18299
- return [`${file2}-wal`, `${file2}-shm`];
18300
- }
18301
- function tightenPerms(file2) {
18302
- for (const path of [file2, ...walSidecars(file2)]) {
18303
- try {
18304
- chmodSync(path, DATA_FILE_MODE);
18305
- } catch {
18306
- }
18307
- }
18308
- }
18309
-
18310
18707
  // ../../packages/persistence/src/internal/json.ts
18311
18708
  function safeJson(s, fallback) {
18312
18709
  if (s == null) return fallback;
@@ -18326,51 +18723,6 @@ function parseJsonObject(s) {
18326
18723
  return void 0;
18327
18724
  }
18328
18725
 
18329
- // ../../packages/persistence/src/internal/rows.ts
18330
- function allRows(stmt, params) {
18331
- if (params === void 0) return stmt.all();
18332
- if (Array.isArray(params)) return stmt.all(...params);
18333
- return stmt.all(params);
18334
- }
18335
- function getRow(stmt, params) {
18336
- if (params === void 0) return stmt.get();
18337
- if (Array.isArray(params)) return stmt.get(...params);
18338
- return stmt.get(params);
18339
- }
18340
- function intToBool(raw) {
18341
- return raw === 1 || raw === true;
18342
- }
18343
- function boolToInt(b) {
18344
- return b ? 1 : 0;
18345
- }
18346
- function bindParams(row) {
18347
- const out = {};
18348
- for (const [key, value] of Object.entries(row)) {
18349
- out[key] = value === void 0 ? null : value;
18350
- }
18351
- return out;
18352
- }
18353
- function countScalar(db, sql, params) {
18354
- return getRow(db.prepare(sql), params)?.n ?? 0;
18355
- }
18356
- function countBy(db, sql, params) {
18357
- const map2 = /* @__PURE__ */ new Map();
18358
- for (const row of allRows(db.prepare(sql), params)) {
18359
- map2.set(row.k, row.n);
18360
- }
18361
- return map2;
18362
- }
18363
- function mapRowsTolerant(rows, map2) {
18364
- const out = [];
18365
- for (const row of rows) {
18366
- try {
18367
- out.push(map2(row));
18368
- } catch {
18369
- }
18370
- }
18371
- return out;
18372
- }
18373
-
18374
18726
  // ../../packages/persistence/src/repositories/activity.ts
18375
18727
  var DAY_MS = 864e5;
18376
18728
  var LIVE_ACTIVITY_WINDOW_MS = 30 * 6e4;
@@ -18987,6 +19339,21 @@ var SqliteAuditEventsRepository = class {
18987
19339
  })
18988
19340
  );
18989
19341
  }
19342
+ // Idempotent stub of a session's structural root. Session-scoped leaves
19343
+ // (captures, llm_call, tool_call) FK parent_id/root_session_id onto this row;
19344
+ // INSERT OR IGNORE does NOT suppress a foreign-key violation (only
19345
+ // UNIQUE/PK/NOT NULL/CHECK), so a session-scoped insert with no root row
19346
+ // raises SQLITE_CONSTRAINT and rolls its whole transaction back — silently
19347
+ // dropping the write under failOpenTransaction. SessionStart's own root write
19348
+ // is itself fail-open and marks "attempted", not "succeeded", so a session
19349
+ // with no root row yet is a real, permanent condition, not a transient race.
19350
+ // The stub carries no dimensions/attributes; an authoritative root
19351
+ // (SessionStart / the reconciler's buildSessionRoot) wins by first-write-wins
19352
+ // on the id PK, so the stub never shadows real data. This is the single named
19353
+ // home for that FK invariant — call it before writing any session-scoped row.
19354
+ ensureSessionRoot(sessionId, startedAt) {
19355
+ this.insertAuditEvent({ id: sessionId, eventType: "session", startedAt });
19356
+ }
18990
19357
  // Insert one transcript-derived `llm_call` leaf. Unlike `insertAuditEvent`
18991
19358
  // (which takes a caller-supplied random id), the id here is MINTED internally
18992
19359
  // from the natural key — `llmCallId(sessionId, messageId)` — tenant-free like the
@@ -19448,8 +19815,14 @@ var SqliteDetectionsRepository = class {
19448
19815
  )
19449
19816
  );
19450
19817
  }
19451
- // Findings whose parent event occurred in the last 30 days and whose rule_id is
19452
- // in the given set. Mirrors the security repo's findings⋈events window join.
19818
+ // Findings whose parent audit event occurred in the last 30 days, is one of
19819
+ // the four capture kinds, and whose definition's rule_id is in the given set.
19820
+ // Mirrors the security repo's inspection_findings⋈audit_events window join.
19821
+ // rule_id lives on inspection_definitions, not the finding row, so the join
19822
+ // chains through it. audit_events also holds structural rows (session, run,
19823
+ // tool_call, llm_call, source_lookup, config_scan) that never had a legacy
19824
+ // events counterpart, so the event_type predicate keeps this count identical
19825
+ // to the old findings⋈events one.
19453
19826
  countFindingsLast30d(ruleIds) {
19454
19827
  if (ruleIds.length === 0) return 0;
19455
19828
  const since = this.now() - 30 * DAY_MS2;
@@ -19457,8 +19830,12 @@ var SqliteDetectionsRepository = class {
19457
19830
  return countScalar(
19458
19831
  this.db,
19459
19832
  `SELECT count(*) AS n
19460
- FROM findings f JOIN events e ON e.id = f.event_id
19461
- WHERE e.occurred_at >= ? AND f.rule_id IN (${inClause})`,
19833
+ FROM inspection_findings f
19834
+ JOIN audit_events e ON e.id = f.audit_event_id
19835
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19836
+ WHERE e.started_at >= ?
19837
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
19838
+ AND d.rule_id IN (${inClause})`,
19462
19839
  [since, ...ruleIds]
19463
19840
  );
19464
19841
  }
@@ -19468,35 +19845,24 @@ var SqliteDetectionsRepository = class {
19468
19845
  var SqliteEventsRepository = class {
19469
19846
  constructor(db) {
19470
19847
  this.db = db;
19471
- this.insertStmt = db.prepare(
19472
- `INSERT INTO events (id, source_tool, kind, occurred_at, content_hash, content, metadata)
19473
- VALUES (:id, :sourceTool, :kind, :occurredAt, :contentHash, :content, :metadata)`
19474
- );
19475
19848
  }
19476
19849
  db;
19477
- insertStmt;
19478
- insertEvent(event) {
19479
- const row = toEventRow(event);
19480
- this.insertStmt.run(
19481
- bindParams({
19482
- id: row.id,
19483
- sourceTool: row.sourceTool,
19484
- kind: row.kind,
19485
- occurredAt: row.occurredAt,
19486
- contentHash: row.contentHash,
19487
- content: row.content,
19488
- metadata: row.metadata
19489
- })
19490
- );
19491
- }
19492
- // Every recorded event's content hash — the historical backfill loads this once
19493
- // to skip transcript messages it has already stored, so re-running the scan
19494
- // never duplicates findings.
19850
+ // Every recorded capture's content hash — the historical backfill loads this
19851
+ // once to skip transcript messages it has already stored, so re-running the
19852
+ // scan never duplicates findings.
19495
19853
  // Async (Promise.resolve over synchronous node:sqlite) so it satisfies the
19496
19854
  // async EventsReadPort contract.
19855
+ //
19856
+ // audit_events also holds structural rows (session, run, tool_call, llm_call,
19857
+ // source_lookup, config_scan) with a NULL content_hash, so the capture-kind
19858
+ // predicate isn't load-bearing here — it documents intent and keeps the scan
19859
+ // index-friendly rather than walking rows that can never match.
19497
19860
  contentHashes() {
19498
19861
  const rows = allRows(
19499
- this.db.prepare("SELECT content_hash FROM events")
19862
+ this.db.prepare(
19863
+ `SELECT content_hash FROM audit_events
19864
+ WHERE event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
19865
+ )
19500
19866
  );
19501
19867
  return Promise.resolve(new Set(rows.map((r) => r.content_hash)));
19502
19868
  }
@@ -19832,17 +20198,20 @@ function parseExceptionRow(row) {
19832
20198
  }
19833
20199
 
19834
20200
  // ../../packages/persistence/src/repositories/resolution-sql.ts
19835
- function latestResolutionStatusSql(findingsAlias) {
20201
+ function latestResolutionColumnSql(column, findingsAlias) {
19836
20202
  return `(
19837
- SELECT fr.status FROM finding_resolution fr
20203
+ SELECT fr.${column} FROM finding_resolution fr
19838
20204
  WHERE fr.finding_key = ${findingsAlias}.finding_key
19839
20205
  ORDER BY fr.created_at DESC, fr.rowid DESC
19840
20206
  LIMIT 1
19841
20207
  )`;
19842
20208
  }
20209
+ function latestResolutionStatusSql(findingsAlias) {
20210
+ return latestResolutionColumnSql("status", findingsAlias);
20211
+ }
19843
20212
  var LATEST_RESOLUTION_BY_KEY_SQL = `(
19844
- SELECT finding_key, status FROM (
19845
- SELECT fr.finding_key, fr.status,
20213
+ SELECT finding_key, status, method, resolved_at FROM (
20214
+ SELECT fr.finding_key, fr.status, fr.method, fr.resolved_at,
19846
20215
  ROW_NUMBER() OVER (
19847
20216
  PARTITION BY fr.finding_key
19848
20217
  ORDER BY fr.created_at DESC, fr.rowid DESC
@@ -19869,68 +20238,21 @@ var DAY_MS3 = 864e5;
19869
20238
  var SqliteFindingsRepository = class {
19870
20239
  constructor(db) {
19871
20240
  this.db = db;
19872
- this.insertStmt = db.prepare(
19873
- `INSERT INTO findings (id, event_id, rule_id, category, severity, span_start, span_end, masked_match, action_taken, confidence, finding_key, first_detected_at)
19874
- VALUES (:id, :eventId, :ruleId, :category, :severity, :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence, :findingKey,
19875
- (SELECT occurred_at FROM events WHERE id = :eventId))
19876
- ON CONFLICT (finding_key) DO UPDATE SET
19877
- event_id = excluded.event_id,
19878
- category = excluded.category,
19879
- severity = excluded.severity,
19880
- span_start = excluded.span_start,
19881
- span_end = excluded.span_end,
19882
- masked_match = excluded.masked_match,
19883
- action_taken = excluded.action_taken,
19884
- confidence = excluded.confidence`
19885
- );
19886
- this.sessionDupStmt = db.prepare(
19887
- `SELECT 1 FROM findings f JOIN events e ON e.id = f.event_id
19888
- WHERE f.rule_id = :ruleId AND f.masked_match = :maskedMatch
19889
- AND json_extract(e.metadata, '$.sessionId') = :sessionId
19890
- LIMIT 1`
19891
- );
19892
20241
  }
19893
20242
  db;
19894
- insertStmt;
19895
- sessionDupStmt;
19896
- insertFindings(findings, scope = {}) {
19897
- for (const finding2 of findings) {
19898
- if (scope.sessionId && this.isSessionDuplicate(finding2, scope.sessionId)) continue;
19899
- const row = toFindingRow(finding2);
19900
- this.insertStmt.run({
19901
- id: row.id,
19902
- eventId: row.eventId,
19903
- ruleId: row.ruleId,
19904
- category: row.category,
19905
- severity: row.severity,
19906
- spanStart: row.spanStart,
19907
- spanEnd: row.spanEnd,
19908
- maskedMatch: row.maskedMatch,
19909
- actionTaken: row.actionTaken,
19910
- confidence: row.confidence,
19911
- findingKey: row.findingKey ?? null
19912
- });
19913
- }
19914
- }
19915
- // True when an earlier event in the same session already recorded a finding
19916
- // with the same rule and masked value. The current event is inserted before
19917
- // its findings, but carries no findings yet, so this never self-matches.
19918
- isSessionDuplicate(finding2, sessionId) {
19919
- const hit = this.sessionDupStmt.get({
19920
- ruleId: finding2.ruleId,
19921
- maskedMatch: finding2.maskedMatch,
19922
- sessionId
19923
- });
19924
- return hit !== void 0;
19925
- }
19926
20243
  recentFindings(opts) {
19927
20244
  const limit = opts?.limit ?? 50;
19928
20245
  const rows = allRows(
19929
20246
  this.db.prepare(
19930
- `SELECT f.id, f.event_id, f.rule_id, f.category, f.severity, f.masked_match,
19931
- f.action_taken, f.confidence, e.occurred_at, e.source_tool, e.kind
19932
- FROM findings f JOIN events e ON e.id = f.event_id
19933
- ORDER BY e.occurred_at DESC, f.rowid DESC
20247
+ `SELECT f.id, f.audit_event_id AS event_id, d.rule_id, d.category, d.severity,
20248
+ f.masked_match, f.action_taken, f.confidence, e.started_at AS occurred_at,
20249
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
20250
+ e.event_type AS kind
20251
+ FROM inspection_findings f
20252
+ JOIN audit_events e ON e.id = f.audit_event_id
20253
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20254
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20255
+ ORDER BY e.started_at DESC, f.rowid DESC
19934
20256
  LIMIT :limit`
19935
20257
  ),
19936
20258
  { limit }
@@ -19952,25 +20274,34 @@ var SqliteFindingsRepository = class {
19952
20274
  );
19953
20275
  }
19954
20276
  /** Live-enforced findings recorded for one session — a bare COUNT over the
19955
- * session-stamped events (served by idx_events_session_id), so the Activity
20277
+ * session-stamped audit_events (served by idx_audit_session), so the Activity
19956
20278
  * page can label its findings link without the grouped pipeline. */
19957
20279
  sessionFindingsCount(sessionId) {
19958
20280
  if (!sessionId) return Promise.resolve(0);
19959
20281
  return Promise.resolve(
19960
20282
  countScalar(
19961
20283
  this.db,
19962
- `SELECT count(*) AS n FROM findings f
19963
- JOIN events e ON e.id = f.event_id
19964
- WHERE json_extract(e.metadata, '$.sessionId') = :sessionId`,
20284
+ `SELECT count(*) AS n FROM inspection_findings f
20285
+ JOIN audit_events e ON e.id = f.audit_event_id
20286
+ WHERE e.root_session_id = :sessionId
20287
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`,
19965
20288
  { sessionId }
19966
20289
  )
19967
20290
  );
19968
20291
  }
19969
- /** Per-rule transcript firing tally for one session — reads the OTHER finding
19970
- * store (inspection_findings, keyed to audit_events): every detection the
19971
- * transcript pass recorded, counted per firing rather than per unique value.
19972
- * Rides on session-scoped grouped responses so the findings view can
19973
- * reconcile the Activity page's tally with the deduped groups it lists. */
20292
+ /** Per-rule transcript firing tally for one session — every detection the
20293
+ * transcript-reconciler pass recorded against the session's `tool_call` rows,
20294
+ * counted per firing rather than per unique value. Rides on session-scoped
20295
+ * grouped responses so the findings view can reconcile the Activity page's
20296
+ * tally with the deduped groups it lists.
20297
+ *
20298
+ * `inspection_findings`/`audit_events` are now the SAME physical tables the
20299
+ * rest of this class reads for the live-capture list above (they used to be
20300
+ * a separate store), so this excludes the four capture kinds those rows
20301
+ * already carry — without that exclusion, every live-capture finding in the
20302
+ * session would be tallied here too, double-counting against the grouped
20303
+ * list this response rides alongside. The reconciler attaches its findings
20304
+ * only to `tool_call` rows, which the exclusion leaves untouched. */
19974
20305
  sessionFirings(sessionId) {
19975
20306
  return Object.fromEntries(
19976
20307
  countBy(
@@ -19980,18 +20311,25 @@ var SqliteFindingsRepository = class {
19980
20311
  JOIN audit_events e ON e.id = f.audit_event_id
19981
20312
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
19982
20313
  WHERE e.root_session_id = :sessionId
20314
+ AND e.event_type NOT IN (${CAPTURE_EVENT_TYPES_SQL})
19983
20315
  GROUP BY d.rule_id`,
19984
20316
  { sessionId }
19985
20317
  )
19986
20318
  );
19987
20319
  }
19988
20320
  /**
19989
- * Grouped findings for the dashboard — joins findingsevents (repo/file/
19990
- * toolName from event metadata), groups by ruleId, computes per-filter-excluded facets,
19991
- * applies the requested filters, and sorts by severity then recency. Filtering
20321
+ * Grouped findings for the dashboard — joins inspection_findingsaudit_events
20322
+ * ⋈inspection_definitions (repo/file/toolName from the audit event's
20323
+ * attributes bag, rule_id/category/severity from the definition), scoped to
20324
+ * the four capture kinds (audit_events also holds structural/reconciler/scan
20325
+ * rows this list must never surface), groups by ruleId, computes
20326
+ * per-filter-excluded facets, applies the requested filters, and sorts by
20327
+ * severity then recency. Filtering
19992
20328
  * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
19993
20329
  * reflect the full filtered set; `items` is the requested
19994
- * page (default 50); no cursor (nextCursor is always null).
20330
+ * page (default 50); no cursor (nextCursor is always null). Under a `status`
20331
+ * filter, `totals.findings` counts only instances whose derived status was
20332
+ * requested, and each item's instance preview is narrowed the same way.
19995
20333
  *
19996
20334
  * Two reads, neither of which materializes a row per finding:
19997
20335
  * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
@@ -20004,10 +20342,11 @@ var SqliteFindingsRepository = class {
20004
20342
  * rule is ever restated in SQL.
20005
20343
  */
20006
20344
  listGroupedFindings(query) {
20007
- const sessionPredicate = query.sessionId ? `WHERE json_extract(e.metadata, '$.sessionId') = :sessionId` : "";
20345
+ const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
20346
+ const predicate = `WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})${sessionPredicate}`;
20008
20347
  const sessionParams = query.sessionId ? { sessionId: query.sessionId } : {};
20009
20348
  const aggregates = this.groupAggregates(query.q !== void 0 && query.q !== "", {
20010
- predicate: sessionPredicate,
20349
+ predicate,
20011
20350
  params: sessionParams
20012
20351
  });
20013
20352
  const rows = allRows(
@@ -20015,24 +20354,26 @@ var SqliteFindingsRepository = class {
20015
20354
  `SELECT id, rule_id, category, severity, masked_match, action_taken, confidence,
20016
20355
  occurred_at, source_tool, repo, file, tool_name, kind, finding_key, latest_status
20017
20356
  FROM (
20018
- SELECT f.id AS id, f.rule_id AS rule_id, f.category AS category,
20019
- f.severity AS severity, f.masked_match AS masked_match,
20357
+ SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
20358
+ d.severity AS severity, f.masked_match AS masked_match,
20020
20359
  f.action_taken AS action_taken, f.confidence AS confidence,
20021
- e.occurred_at AS occurred_at, e.source_tool AS source_tool,
20022
- json_extract(e.metadata, '$.repo') AS repo,
20023
- json_extract(e.metadata, '$.filePath') AS file,
20024
- json_extract(e.metadata, '$.toolName') AS tool_name,
20025
- e.kind AS kind, f.finding_key AS finding_key,
20360
+ e.started_at AS occurred_at,
20361
+ json_extract(e.attributes, '$.source_tool') AS source_tool,
20362
+ json_extract(e.attributes, '$.repo') AS repo,
20363
+ json_extract(e.attributes, '$.file_path') AS file,
20364
+ json_extract(e.attributes, '$.tool_name') AS tool_name,
20365
+ e.event_type AS kind, f.finding_key AS finding_key,
20026
20366
  latest.status AS latest_status,
20027
20367
  ROW_NUMBER() OVER (
20028
- PARTITION BY f.rule_id
20029
- ORDER BY e.occurred_at DESC, f.id DESC
20368
+ PARTITION BY d.rule_id
20369
+ ORDER BY e.started_at DESC, f.id DESC
20030
20370
  ) AS rn
20031
- FROM findings f
20032
- JOIN events e ON e.id = f.event_id
20371
+ FROM inspection_findings f
20372
+ JOIN audit_events e ON e.id = f.audit_event_id
20373
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20033
20374
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20034
20375
  ON latest.finding_key = f.finding_key
20035
- ${sessionPredicate}
20376
+ ${predicate}
20036
20377
  )
20037
20378
  WHERE rn <= :cap
20038
20379
  ORDER BY occurred_at DESC, id DESC`
@@ -20059,17 +20400,29 @@ var SqliteFindingsRepository = class {
20059
20400
  severity: query.severity,
20060
20401
  providers: query.provider,
20061
20402
  actions: query.action,
20403
+ statuses: query.status,
20062
20404
  subtype: query.subtype,
20063
20405
  q: query.q
20064
20406
  };
20065
20407
  const facets = computeFindingFacets(allGroups, filterOpts);
20066
20408
  const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
20409
+ const statusFilter = query.status ?? [];
20067
20410
  const totals = {
20068
- findings: sorted.reduce((acc, g) => acc + g.instanceCount, 0),
20411
+ findings: sorted.reduce((acc, g) => {
20412
+ if (statusFilter.length === 0) return acc + g.instanceCount;
20413
+ const agg = aggregates.get(g.id);
20414
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
20415
+ }, 0),
20069
20416
  groups: sorted.length
20070
20417
  };
20071
20418
  const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
20072
- const items = sorted.slice(0, limit);
20419
+ const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
20420
+ const items = sorted.slice(0, limit).map(
20421
+ (g) => statusSet ? {
20422
+ ...g,
20423
+ instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
20424
+ } : g
20425
+ );
20073
20426
  return Promise.resolve({
20074
20427
  totals,
20075
20428
  facets,
@@ -20083,45 +20436,62 @@ var SqliteFindingsRepository = class {
20083
20436
  * buildFindingGroups cannot recover from a preview. Bounded by the number of
20084
20437
  * distinct rule_ids (the installed packs' rules), not by the store's size.
20085
20438
  *
20086
- * The per-instance sets ride back as group_concat lists of RAW DB values —
20087
- * source_tool, action_taken, and the (kind, has-key, latest-status) triples
20088
- * deriveFindingStatus consumes. Aggregating the status INPUTS rather than a
20089
- * status keeps the classifier itself in @akasecurity/schema, where
20090
- * severitySummary's SQL and this query can't drift apart on what 'resolved'
20091
- * means (see resolution-sql.ts). Each of those sets is bounded by an enum, so
20439
+ * A single scan, folded in two levels: the inner SELECT groups by
20440
+ * (rule_id, status tuple) so each (kind, has-key, latest-status) combination
20441
+ * carries its instance count countInstancesByStatus needs those counts for
20442
+ * status-scoped totals — and the outer SELECT folds the tuples back to one
20443
+ * row per rule. The per-instance sets ride back as group_concat lists of RAW
20444
+ * DB values source_tool, action_taken, and the tuples deriveFindingStatus
20445
+ * consumes. Aggregating the status INPUTS rather than a status keeps the
20446
+ * classifier itself in @akasecurity/schema, where severitySummary's SQL and
20447
+ * this query can't drift apart on what 'resolved' means (see
20448
+ * resolution-sql.ts). The concat-of-concats can repeat a value across
20449
+ * tuples; the schema mappers dedupe, and each set is bounded by an enum, so
20092
20450
  * a group's row stays small however many findings it holds.
20093
20451
  *
20094
20452
  * `withSearchText` is the exception, and the one column here that does NOT
20095
- * stay small: the group's distinct repos/filePaths, whose size tracks how many
20096
- * distinct paths a rule fired across — for a rule hitting mostly-unique paths
20097
- * that is a string proportional to the store (~8MB over 200k distinct paths,
20098
- * and buildHaystack lowercases a second copy). It buys `q` the ability to
20099
- * match an instance outside the preview, which searching the preview alone
20100
- * would silently lose, so it is fetched only when the request actually
20101
- * carries a `q`.
20453
+ * stay small: the group's per-tuple-distinct repos/filePaths, whose size
20454
+ * tracks how many distinct paths a rule fired across — for a rule hitting
20455
+ * mostly-unique paths that is a string proportional to the store (~8MB over
20456
+ * 200k distinct paths, and buildHaystack lowercases a second copy). It buys
20457
+ * `q` the ability to match an instance outside the preview, which searching
20458
+ * the preview alone would silently lose, so it is fetched only when the
20459
+ * request actually carries a `q`. (Substring matching is unaffected by a
20460
+ * path repeating across tuples.)
20102
20461
  */
20103
20462
  groupAggregates(withSearchText, scope) {
20104
- const searchTextColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.metadata, '$.repo')) AS repos,
20105
- group_concat(DISTINCT json_extract(e.metadata, '$.filePath')) AS files,
20106
- group_concat(DISTINCT 'via ' || json_extract(e.metadata, '$.toolName')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
20463
+ const innerSearchColumns = withSearchText ? `, group_concat(DISTINCT json_extract(e.attributes, '$.repo')) AS repos,
20464
+ group_concat(DISTINCT json_extract(e.attributes, '$.file_path')) AS files,
20465
+ group_concat(DISTINCT 'via ' || json_extract(e.attributes, '$.tool_name')) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
20107
20466
  const rows = this.db.prepare(
20108
- `SELECT f.rule_id AS rule_id,
20109
- count(*) AS instance_count,
20110
- max(e.occurred_at) AS latest_at,
20111
- group_concat(DISTINCT e.source_tool) AS source_tools,
20112
- group_concat(DISTINCT f.action_taken) AS actions_taken,
20113
- group_concat(DISTINCT (
20114
- e.kind || '${TUPLE_SEP}' ||
20115
- (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
20116
- coalesce(latest.status, '')
20117
- )) AS status_inputs
20118
- ${searchTextColumns}
20119
- FROM findings f
20120
- JOIN events e ON e.id = f.event_id
20121
- LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20122
- ON latest.finding_key = f.finding_key
20123
- ${scope.predicate}
20124
- GROUP BY f.rule_id`
20467
+ `SELECT rule_id,
20468
+ sum(tuple_count) AS instance_count,
20469
+ max(latest_at) AS latest_at,
20470
+ group_concat(source_tools) AS source_tools,
20471
+ group_concat(actions_taken) AS actions_taken,
20472
+ group_concat(status_tuple || '${TUPLE_SEP}' || tuple_count) AS status_inputs,
20473
+ group_concat(repos) AS repos,
20474
+ group_concat(files) AS files,
20475
+ group_concat(tool_names) AS tool_names
20476
+ FROM (
20477
+ SELECT d.rule_id AS rule_id,
20478
+ e.event_type || '${TUPLE_SEP}' ||
20479
+ (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
20480
+ coalesce(latest.status, '') AS status_tuple,
20481
+ count(*) AS tuple_count,
20482
+ max(e.started_at) AS latest_at,
20483
+ group_concat(DISTINCT json_extract(e.attributes, '$.source_tool')) AS source_tools,
20484
+ group_concat(DISTINCT f.action_taken) AS actions_taken
20485
+ ${innerSearchColumns}
20486
+ FROM inspection_findings f
20487
+ JOIN audit_events e ON e.id = f.audit_event_id
20488
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20489
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20490
+ ON latest.finding_key = f.finding_key
20491
+ ${scope.predicate}
20492
+ GROUP BY d.rule_id, status_tuple
20493
+ )
20494
+ GROUP BY rule_id`
20125
20495
  ).all(scope.params);
20126
20496
  return new Map(
20127
20497
  rows.map((r) => [
@@ -20131,13 +20501,14 @@ var SqliteFindingsRepository = class {
20131
20501
  sourceTools: splitConcat(r.source_tools),
20132
20502
  actionsTaken: splitConcat(r.actions_taken),
20133
20503
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
20134
- const [kind = "", keyMarker = "", latestStatus = ""] = tuple2.split(TUPLE_SEP);
20504
+ const [kind = "", keyMarker = "", latestStatus = "", count = ""] = tuple2.split(TUPLE_SEP);
20135
20505
  return {
20136
20506
  // deriveFindingStatus only distinguishes null from non-null here,
20137
20507
  // so the marker stands in for the key itself (never rendered).
20138
20508
  kind,
20139
20509
  findingKey: keyMarker === "" ? null : keyMarker,
20140
- latestResolutionStatus: latestStatus === "" ? null : latestStatus
20510
+ latestResolutionStatus: latestStatus === "" ? null : latestStatus,
20511
+ count: Number(count)
20141
20512
  };
20142
20513
  }),
20143
20514
  latestDetectedAt: epochMillisToIso(r.latest_at),
@@ -20154,10 +20525,21 @@ var SqliteFindingsRepository = class {
20154
20525
  );
20155
20526
  }
20156
20527
  healthSummary() {
20157
- const total = countScalar(this.db, "SELECT count(*) AS n FROM findings");
20528
+ const total = countScalar(
20529
+ this.db,
20530
+ `SELECT count(*) AS n FROM inspection_findings f
20531
+ JOIN audit_events e ON e.id = f.audit_event_id
20532
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})`
20533
+ );
20158
20534
  const byAction = Object.fromEntries(ACTION_TAKEN_KEYS.map((a) => [a, 0]));
20159
20535
  const grouped = allRows(
20160
- this.db.prepare("SELECT action_taken, count(*) AS c FROM findings GROUP BY action_taken")
20536
+ this.db.prepare(
20537
+ `SELECT f.action_taken AS action_taken, count(*) AS c
20538
+ FROM inspection_findings f
20539
+ JOIN audit_events e ON e.id = f.audit_event_id
20540
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20541
+ GROUP BY f.action_taken`
20542
+ )
20161
20543
  );
20162
20544
  for (const row of grouped) {
20163
20545
  if (row.action_taken in byAction) byAction[row.action_taken] = row.c;
@@ -20165,12 +20547,15 @@ var SqliteFindingsRepository = class {
20165
20547
  const bySeverity = { critical: 0, high: 0, medium: 0, low: 0 };
20166
20548
  const sevRows = allRows(
20167
20549
  this.db.prepare(
20168
- `SELECT f.severity AS severity, count(*) AS c
20169
- FROM findings f
20550
+ `SELECT d.severity AS severity, count(*) AS c
20551
+ FROM inspection_findings f
20552
+ JOIN audit_events e ON e.id = f.audit_event_id
20553
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20170
20554
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
20171
20555
  ON latest.finding_key = f.finding_key
20172
- WHERE latest.status IS NULL OR latest.status != 'resolved'
20173
- GROUP BY f.severity`
20556
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20557
+ AND (latest.status IS NULL OR latest.status != 'resolved')
20558
+ GROUP BY d.severity`
20174
20559
  )
20175
20560
  );
20176
20561
  for (const row of sevRows) {
@@ -20191,9 +20576,11 @@ var SqliteFindingsRepository = class {
20191
20576
  const since = startOfUtcDay(Date.now()) - (days - 1) * DAY_MS3;
20192
20577
  const rows = allRows(
20193
20578
  this.db.prepare(
20194
- `SELECT date(e.occurred_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20195
- FROM findings f JOIN events e ON e.id = f.event_id
20196
- WHERE e.occurred_at >= :since
20579
+ `SELECT date(e.started_at / 1000, 'unixepoch') AS day, f.action_taken AS action, count(*) AS c
20580
+ FROM inspection_findings f
20581
+ JOIN audit_events e ON e.id = f.audit_event_id
20582
+ WHERE e.started_at >= :since
20583
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20197
20584
  GROUP BY day, f.action_taken`
20198
20585
  ),
20199
20586
  { since }
@@ -20258,15 +20645,59 @@ var SqliteInspectionFindingsRepository = class {
20258
20645
  this.insertStmt = db.prepare(
20259
20646
  `INSERT INTO inspection_findings
20260
20647
  (id, audit_event_id, inspection_definition_id, classified_data_id,
20261
- span_start, span_end, masked_match, action_taken, confidence)
20648
+ span_start, span_end, masked_match, action_taken, confidence,
20649
+ finding_key, first_detected_at)
20262
20650
  VALUES
20263
20651
  (:id, :auditEventId, :inspectionDefinitionId, :classifiedDataId,
20264
- :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence)
20265
- ON CONFLICT(id) DO NOTHING`
20652
+ :spanStart, :spanEnd, :maskedMatch, :actionTaken, :confidence,
20653
+ :findingKey,
20654
+ COALESCE(:firstDetectedAt, (SELECT started_at FROM audit_events WHERE id = :auditEventId)))
20655
+ ON CONFLICT(id) DO UPDATE SET
20656
+ inspection_definition_id = excluded.inspection_definition_id
20657
+ ON CONFLICT (finding_key) DO UPDATE SET
20658
+ audit_event_id = excluded.audit_event_id,
20659
+ inspection_definition_id = excluded.inspection_definition_id,
20660
+ classified_data_id = excluded.classified_data_id,
20661
+ span_start = excluded.span_start,
20662
+ span_end = excluded.span_end,
20663
+ masked_match = excluded.masked_match,
20664
+ action_taken = excluded.action_taken,
20665
+ confidence = excluded.confidence`
20666
+ );
20667
+ this.sessionDupStmt = db.prepare(
20668
+ `SELECT 1 FROM inspection_findings f
20669
+ JOIN audit_events e ON e.id = f.audit_event_id
20670
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20671
+ WHERE d.rule_id = :ruleId AND f.masked_match = :maskedMatch
20672
+ AND e.root_session_id = :sessionId
20673
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
20674
+ LIMIT 1`
20675
+ );
20676
+ this.eventDupStmt = db.prepare(
20677
+ `SELECT 1 FROM inspection_findings f
20678
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
20679
+ WHERE f.audit_event_id = :auditEventId AND d.rule_id = :ruleId
20680
+ AND f.masked_match = :maskedMatch
20681
+ AND f.span_start = :spanStart AND f.span_end = :spanEnd
20682
+ LIMIT 1`
20266
20683
  );
20267
20684
  }
20268
20685
  db;
20269
20686
  insertStmt;
20687
+ sessionDupStmt;
20688
+ eventDupStmt;
20689
+ // True when an earlier event in the same session already recorded a finding
20690
+ // with the same rule and masked value. The current event's own findings are
20691
+ // inserted one at a time in caller order, so an earlier finding in the SAME
20692
+ // recordCapture call is visible to a later duplicate check within it too.
20693
+ isSessionDuplicate(ruleId, maskedMatch, sessionId) {
20694
+ return this.sessionDupStmt.get({ ruleId, maskedMatch, sessionId }) !== void 0;
20695
+ }
20696
+ // True when this exact detection (rule + masked value + span) is already
20697
+ // recorded against the given audit event.
20698
+ isEventDuplicate(auditEventId, ruleId, maskedMatch, spanStart, spanEnd) {
20699
+ return this.eventDupStmt.get({ auditEventId, ruleId, maskedMatch, spanStart, spanEnd }) !== void 0;
20700
+ }
20270
20701
  insertFinding(input) {
20271
20702
  const row = toInspectionFindingRow(input);
20272
20703
  this.insertStmt.run(
@@ -20279,7 +20710,9 @@ var SqliteInspectionFindingsRepository = class {
20279
20710
  spanEnd: row.spanEnd,
20280
20711
  maskedMatch: row.maskedMatch,
20281
20712
  actionTaken: row.actionTaken,
20282
- confidence: row.confidence
20713
+ confidence: row.confidence,
20714
+ findingKey: row.findingKey,
20715
+ firstDetectedAt: row.firstDetectedAt
20283
20716
  })
20284
20717
  );
20285
20718
  }
@@ -20551,7 +20984,7 @@ var SqliteInstalledPacksRepository = class {
20551
20984
  installedRuleset() {
20552
20985
  const rows = allRows(
20553
20986
  this.db.prepare(
20554
- `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson FROM installed_packs`
20987
+ `SELECT enabled, policy_id AS policyId, rules_json AS rulesJson, version FROM installed_packs`
20555
20988
  )
20556
20989
  );
20557
20990
  const out = {
@@ -20559,7 +20992,8 @@ var SqliteInstalledPacksRepository = class {
20559
20992
  enabledPacks: 0,
20560
20993
  rules: [],
20561
20994
  invalidRules: 0,
20562
- ruleActions: /* @__PURE__ */ new Map()
20995
+ ruleActions: /* @__PURE__ */ new Map(),
20996
+ ruleVersions: /* @__PURE__ */ new Map()
20563
20997
  };
20564
20998
  for (const row of rows) {
20565
20999
  if (!intToBool(row.enabled)) continue;
@@ -20581,6 +21015,7 @@ var SqliteInstalledPacksRepository = class {
20581
21015
  if (parsed.success) {
20582
21016
  out.rules.push(parsed.data);
20583
21017
  out.ruleActions.set(parsed.data.id, action);
21018
+ out.ruleVersions.set(parsed.data.id, row.version);
20584
21019
  } else out.invalidRules += 1;
20585
21020
  }
20586
21021
  }
@@ -21755,19 +22190,19 @@ var SqliteResolutionsRepository = class {
21755
22190
  );
21756
22191
  this.openAtRestStmt = db.prepare(
21757
22192
  `SELECT DISTINCT f.finding_key AS finding_key
21758
- FROM findings f
21759
- JOIN events e ON e.id = f.event_id
21760
- WHERE e.kind = 'code_change'
21761
- AND json_extract(e.metadata, '$.filePath') = :path
22193
+ FROM inspection_findings f
22194
+ JOIN audit_events e ON e.id = f.audit_event_id
22195
+ WHERE e.event_type = 'code_change'
22196
+ AND json_extract(e.attributes, '$.file_path') = :path
21762
22197
  AND f.finding_key IS NOT NULL
21763
22198
  AND ${latestResolutionStatusSql("f")} IS NOT 'resolved'`
21764
22199
  );
21765
22200
  this.resolvedAtRestStmt = db.prepare(
21766
22201
  `SELECT DISTINCT f.finding_key AS finding_key
21767
- FROM findings f
21768
- JOIN events e ON e.id = f.event_id
21769
- WHERE e.kind = 'code_change'
21770
- AND json_extract(e.metadata, '$.filePath') = :path
22202
+ FROM inspection_findings f
22203
+ JOIN audit_events e ON e.id = f.audit_event_id
22204
+ WHERE e.event_type = 'code_change'
22205
+ AND json_extract(e.attributes, '$.file_path') = :path
21771
22206
  AND f.finding_key IS NOT NULL
21772
22207
  AND ${latestResolutionStatusSql("f")} = 'resolved'`
21773
22208
  );
@@ -21990,25 +22425,27 @@ var SqliteSecurityRepository = class {
21990
22425
  severitySummary() {
21991
22426
  const rows = allRows(
21992
22427
  this.db.prepare(
21993
- `SELECT f.severity AS severity,
22428
+ `SELECT d.severity AS severity,
21994
22429
  COUNT(*) AS count,
21995
22430
  SUM(CASE
21996
- WHEN e.kind != 'code_change' THEN 1
22431
+ WHEN e.event_type != 'code_change' THEN 1
21997
22432
  WHEN f.finding_key IS NULL THEN 0
21998
22433
  WHEN latest.status = 'resolved' THEN 1
21999
22434
  ELSE 0
22000
22435
  END) AS caught,
22001
22436
  SUM(CASE
22002
- WHEN e.kind = 'code_change'
22437
+ WHEN e.event_type = 'code_change'
22003
22438
  AND f.finding_key IS NOT NULL
22004
22439
  AND (latest.status IS NULL OR latest.status != 'resolved') THEN 1
22005
22440
  ELSE 0
22006
22441
  END) AS open_at_rest
22007
- FROM findings f
22008
- JOIN events e ON e.id = f.event_id
22442
+ FROM inspection_findings f
22443
+ JOIN audit_events e ON e.id = f.audit_event_id
22444
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22009
22445
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22010
22446
  ON latest.finding_key = f.finding_key
22011
- GROUP BY f.severity`
22447
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22448
+ GROUP BY d.severity`
22012
22449
  )
22013
22450
  );
22014
22451
  const byRow = new Map(rows.map((r) => [r.severity, r]));
@@ -22074,7 +22511,7 @@ var SqliteSecurityRepository = class {
22074
22511
  // Mean time-to-remediate per bucket, split by severity — a sibling of
22075
22512
  // findingsTimeseries that reuses the same window/bucket/UTC math, but buckets
22076
22513
  // on a different timestamp: findingsTimeseries buckets by first-detection
22077
- // (events.occurred_at), this buckets by resolution time (the latest
22514
+ // (audit_events.started_at), this buckets by resolution time (the latest
22078
22515
  // finding_resolution row's resolved_at) — it's a "resolved in this bucket"
22079
22516
  // trend, not a "detected in this bucket" one. Only findings whose LATEST
22080
22517
  // resolution row (latest-resolution-wins, same correlated subquery as
@@ -22099,30 +22536,20 @@ var SqliteSecurityRepository = class {
22099
22536
  // first_detected_at is the PRESERVED first-detection time (set once on a
22100
22537
  // finding's INSERT, never overwritten on the re-detection upsert), so MTTR
22101
22538
  // measures from first sighting — not the latest re-scan's event, whose
22102
- // occurred_at the upsert overwrites onto findings.event_id. COALESCE onto
22103
- // the parent event's occurred_at defends against any legacy/edge row the
22104
- // backfill left null.
22105
- `SELECT COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at, f.severity AS severity,
22106
- (
22107
- SELECT fr.status FROM finding_resolution fr
22108
- WHERE fr.finding_key = f.finding_key
22109
- ORDER BY fr.created_at DESC, fr.rowid DESC
22110
- LIMIT 1
22111
- ) AS latest_status,
22112
- (
22113
- SELECT fr.method FROM finding_resolution fr
22114
- WHERE fr.finding_key = f.finding_key
22115
- ORDER BY fr.created_at DESC, fr.rowid DESC
22116
- LIMIT 1
22117
- ) AS latest_method,
22118
- (
22119
- SELECT fr.resolved_at FROM finding_resolution fr
22120
- WHERE fr.finding_key = f.finding_key
22121
- ORDER BY fr.created_at DESC, fr.rowid DESC
22122
- LIMIT 1
22123
- ) AS latest_resolved_at
22124
- FROM findings f JOIN events e ON e.id = f.event_id
22539
+ // started_at the upsert overwrites onto inspection_findings.audit_event_id.
22540
+ // COALESCE onto the parent event's started_at defends against any
22541
+ // legacy/edge row the backfill left null.
22542
+ `SELECT COALESCE(f.first_detected_at, e.started_at) AS first_detected_at, d.severity AS severity,
22543
+ latest.status AS latest_status,
22544
+ latest.method AS latest_method,
22545
+ latest.resolved_at AS latest_resolved_at
22546
+ FROM inspection_findings f
22547
+ JOIN audit_events e ON e.id = f.audit_event_id
22548
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22549
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22550
+ ON latest.finding_key = f.finding_key
22125
22551
  WHERE f.finding_key IS NOT NULL
22552
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22126
22553
  AND EXISTS (
22127
22554
  SELECT 1 FROM finding_resolution fr
22128
22555
  WHERE fr.finding_key = f.finding_key
@@ -22169,11 +22596,13 @@ var SqliteSecurityRepository = class {
22169
22596
  const from = now - RANGE_DAYS[range] * DAY_MS4;
22170
22597
  const rows = allRows(
22171
22598
  this.db.prepare(
22172
- `SELECT json_extract(e.metadata, '$.repo') AS repo, count(*) AS c
22173
- FROM findings f JOIN events e ON e.id = f.event_id
22174
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
22175
- AND json_extract(e.metadata, '$.repo') IS NOT NULL
22176
- AND json_extract(e.metadata, '$.repo') != ''
22599
+ `SELECT json_extract(e.attributes, '$.repo') AS repo, count(*) AS c
22600
+ FROM inspection_findings f
22601
+ JOIN audit_events e ON e.id = f.audit_event_id
22602
+ WHERE e.started_at >= :from AND e.started_at < :to
22603
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22604
+ AND json_extract(e.attributes, '$.repo') IS NOT NULL
22605
+ AND json_extract(e.attributes, '$.repo') != ''
22177
22606
  GROUP BY repo
22178
22607
  ORDER BY c DESC, repo
22179
22608
  LIMIT :limit`
@@ -22197,44 +22626,28 @@ var SqliteSecurityRepository = class {
22197
22626
  // secret came back) is excluded — it is not currently resolved. Legacy
22198
22627
  // at-rest findings with finding_key IS NULL are excluded outright (the
22199
22628
  // resolution lifecycle can never attach to them). Path comes from the
22200
- // finding's parent event (kind 'code_change', metadata.filePath) — mirrors
22201
- // resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at DESC,
22202
- // capped at `limit`.
22629
+ // finding's parent event (event_type 'code_change', attributes.file_path) —
22630
+ // mirrors resolutions.ts's openAtRestStmt accessor. Ordered by resolved_at
22631
+ // DESC, capped at `limit`.
22203
22632
  recentlyResolved(limit = 20) {
22204
22633
  const rows = allRows(
22205
22634
  this.db.prepare(
22206
22635
  `SELECT f.finding_key AS finding_key,
22207
- f.rule_id AS rule_id,
22208
- f.severity AS severity,
22209
- json_extract(e.metadata, '$.filePath') AS path,
22210
- COALESCE(f.first_detected_at, e.occurred_at) AS first_detected_at,
22211
- (
22212
- SELECT fr.resolved_at FROM finding_resolution fr
22213
- WHERE fr.finding_key = f.finding_key
22214
- ORDER BY fr.created_at DESC, fr.rowid DESC
22215
- LIMIT 1
22216
- ) AS latest_resolved_at
22217
- FROM findings f JOIN events e ON e.id = f.event_id
22218
- WHERE e.kind = 'code_change'
22636
+ d.rule_id AS rule_id,
22637
+ d.severity AS severity,
22638
+ json_extract(e.attributes, '$.file_path') AS path,
22639
+ COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
22640
+ latest.resolved_at AS latest_resolved_at
22641
+ FROM inspection_findings f
22642
+ JOIN audit_events e ON e.id = f.audit_event_id
22643
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22644
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
22645
+ ON latest.finding_key = f.finding_key
22646
+ WHERE e.event_type = 'code_change'
22219
22647
  AND f.finding_key IS NOT NULL
22220
- AND (
22221
- SELECT fr.status FROM finding_resolution fr
22222
- WHERE fr.finding_key = f.finding_key
22223
- ORDER BY fr.created_at DESC, fr.rowid DESC
22224
- LIMIT 1
22225
- ) = 'resolved'
22226
- AND (
22227
- SELECT fr.method FROM finding_resolution fr
22228
- WHERE fr.finding_key = f.finding_key
22229
- ORDER BY fr.created_at DESC, fr.rowid DESC
22230
- LIMIT 1
22231
- ) = 'fixed-at-source'
22232
- AND (
22233
- SELECT fr.resolved_at FROM finding_resolution fr
22234
- WHERE fr.finding_key = f.finding_key
22235
- ORDER BY fr.created_at DESC, fr.rowid DESC
22236
- LIMIT 1
22237
- ) IS NOT NULL
22648
+ AND latest.status = 'resolved'
22649
+ AND latest.method = 'fixed-at-source'
22650
+ AND latest.resolved_at IS NOT NULL
22238
22651
  ORDER BY latest_resolved_at DESC
22239
22652
  LIMIT :limit`
22240
22653
  ),
@@ -22253,15 +22666,18 @@ var SqliteSecurityRepository = class {
22253
22666
  return Promise.resolve({ items });
22254
22667
  }
22255
22668
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
22256
- // epoch-millis timestamp. occurred_at is an INTEGER column, so the bounds stay
22669
+ // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
22257
22670
  // numeric and the JS aggregations bucket/split on ms directly.
22258
22671
  findingsInRange(fromMs, toMs) {
22259
22672
  const rows = allRows(
22260
22673
  this.db.prepare(
22261
- `SELECT e.occurred_at AS occurred_at, f.severity AS severity, f.action_taken AS action_taken
22262
- FROM findings f JOIN events e ON e.id = f.event_id
22263
- WHERE e.occurred_at >= :from AND e.occurred_at < :to
22264
- ORDER BY e.occurred_at`
22674
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
22675
+ FROM inspection_findings f
22676
+ JOIN audit_events e ON e.id = f.audit_event_id
22677
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
22678
+ WHERE e.started_at >= :from AND e.started_at < :to
22679
+ AND e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
22680
+ ORDER BY e.started_at`
22265
22681
  ),
22266
22682
  { from: fromMs, to: toMs }
22267
22683
  );
@@ -23085,9 +23501,10 @@ function openWithPragmas(file2) {
23085
23501
  }
23086
23502
  function backupLegacyStore(file2) {
23087
23503
  const backup = `${file2}.legacy.${String(Date.now())}.bak`;
23088
- renameSync(file2, backup);
23089
- for (const sidecar of walSidecars(file2)) {
23090
- if (existsSync(sidecar)) rmSync(sidecar);
23504
+ renameSync2(file2, backup);
23505
+ tightenFile(backup);
23506
+ for (const sidecar of dbSidecars(file2)) {
23507
+ if (existsSync(sidecar)) rmSync2(sidecar);
23091
23508
  }
23092
23509
  return backup;
23093
23510
  }
@@ -23103,7 +23520,7 @@ function openLocalDatabase(dir) {
23103
23520
  `Detected an older, incompatible (tenant-bearing) ${DB_FILENAME}; backed it up to ${backup} and created a fresh store.`
23104
23521
  );
23105
23522
  }
23106
- applyMigrations(db);
23523
+ applyMigrations(db, file2);
23107
23524
  tightenPerms(file2);
23108
23525
  const events = new SqliteEventsRepository(db);
23109
23526
  const findings = new SqliteFindingsRepository(db);
@@ -23130,9 +23547,56 @@ function openLocalDatabase(dir) {
23130
23547
  policies.seedDefaults();
23131
23548
  function recordCapture(event, detected) {
23132
23549
  failOpenTransaction(db, () => {
23133
- events.insertEvent(event);
23134
23550
  const sessionId = event.metadata?.sessionId;
23135
- findings.insertFindings(detected, sessionId ? { sessionId } : {});
23551
+ if (sessionId) {
23552
+ auditEvents.ensureSessionRoot(sessionId, event.occurredAt);
23553
+ }
23554
+ const auditEventId = captureId(
23555
+ sessionId ?? null,
23556
+ event.contentHash,
23557
+ event.metadata?.filePath ?? null
23558
+ );
23559
+ auditEvents.insertAuditEvent({
23560
+ id: auditEventId,
23561
+ eventType: event.kind,
23562
+ startedAt: event.occurredAt,
23563
+ parentId: sessionId,
23564
+ rootSessionId: sessionId,
23565
+ content: event.content,
23566
+ contentHash: event.contentHash,
23567
+ attributes: toCaptureAttributes(event)
23568
+ });
23569
+ const definitionIds = /* @__PURE__ */ new Map();
23570
+ for (const finding2 of detected) {
23571
+ if (sessionId && inspectionFindings.isSessionDuplicate(finding2.ruleId, finding2.maskedMatch, sessionId)) {
23572
+ continue;
23573
+ }
23574
+ if (inspectionFindings.isEventDuplicate(
23575
+ auditEventId,
23576
+ finding2.ruleId,
23577
+ finding2.maskedMatch,
23578
+ finding2.span.start,
23579
+ finding2.span.end
23580
+ )) {
23581
+ continue;
23582
+ }
23583
+ const key = `${finding2.ruleId}@${captureDefinitionVersion(finding2)}`;
23584
+ let definitionId = definitionIds.get(key);
23585
+ if (!definitionId) {
23586
+ definitionId = inspectionDefinitions.upsert(toCaptureDefinitionInput(finding2));
23587
+ definitionIds.set(key, definitionId);
23588
+ }
23589
+ inspectionFindings.insertFinding({
23590
+ id: finding2.id,
23591
+ auditEventId,
23592
+ inspectionDefinitionId: definitionId,
23593
+ span: finding2.span,
23594
+ maskedMatch: finding2.maskedMatch,
23595
+ actionTaken: finding2.actionTaken,
23596
+ confidence: finding2.confidence,
23597
+ findingKey: finding2.findingKey ?? void 0
23598
+ });
23599
+ }
23136
23600
  });
23137
23601
  }
23138
23602
  function ensureInventory(ctx) {
@@ -23280,9 +23744,12 @@ function openLocalDatabase(dir) {
23280
23744
  };
23281
23745
  }
23282
23746
 
23747
+ // ../../packages/persistence/src/finding-key.ts
23748
+ import { createHash as createHash3 } from "crypto";
23749
+
23283
23750
  // ../../packages/persistence/src/fingerprint.ts
23284
23751
  import { createHmac, randomBytes } from "crypto";
23285
- import { chmodSync as chmodSync2, readFileSync, renameSync as renameSync2, writeFileSync } from "fs";
23752
+ import { readFileSync } from "fs";
23286
23753
  import { join as join2 } from "path";
23287
23754
  var KEY_FILENAME = "exception.key";
23288
23755
  var KEY_MATERIAL_BYTES = 32;
@@ -23319,8 +23786,8 @@ function readFingerprintKey(dataDir2) {
23319
23786
  }
23320
23787
 
23321
23788
  // ../../packages/persistence/src/local-layout.ts
23322
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync2, renameSync as renameSync3 } from "fs";
23323
- import { chmod, mkdir } from "fs/promises";
23789
+ import { renameSync as renameSync3 } from "fs";
23790
+ import { mkdir } from "fs/promises";
23324
23791
  import { homedir } from "os";
23325
23792
  import { join as join3 } from "path";
23326
23793
  function defaultDataDir() {
@@ -23335,6 +23802,9 @@ function dataDir(base = defaultDataDir()) {
23335
23802
  function dbPath(base = defaultDataDir()) {
23336
23803
  return join3(dataDir(base), "aka.db");
23337
23804
  }
23805
+ function ensureLayoutDirSync(dir = defaultDataDir()) {
23806
+ ensureDataDirSync(dir);
23807
+ }
23338
23808
  function migrateLegacyLayout(base = defaultDataDir()) {
23339
23809
  const moves = [
23340
23810
  { name: "config.json", dest: settingsDir(base) },
@@ -23342,19 +23812,17 @@ function migrateLegacyLayout(base = defaultDataDir()) {
23342
23812
  ];
23343
23813
  for (const { name, dest } of moves) {
23344
23814
  try {
23345
- mkdirSync2(dest, { recursive: true, mode: DATA_DIR_MODE });
23346
- try {
23347
- chmodSync3(dest, DATA_DIR_MODE);
23348
- } catch {
23349
- }
23350
- renameSync3(join3(base, name), join3(dest, name));
23815
+ ensureDataDirSync(dest);
23816
+ const moved = join3(dest, name);
23817
+ renameSync3(join3(base, name), moved);
23818
+ tightenFile(moved);
23351
23819
  } catch {
23352
23820
  }
23353
23821
  }
23354
23822
  }
23355
23823
 
23356
23824
  // ../../packages/persistence/src/settings.ts
23357
- import { readFileSync as readFileSync2, renameSync as renameSync4, writeFileSync as writeFileSync2 } from "fs";
23825
+ import { readFileSync as readFileSync2 } from "fs";
23358
23826
  import { join as join4 } from "path";
23359
23827
  function readWorkspaceSettings(base = defaultDataDir()) {
23360
23828
  const record2 = readJson(join4(settingsDir(base), "settings.json"));
@@ -23376,7 +23844,7 @@ function readJson(file2) {
23376
23844
  }
23377
23845
 
23378
23846
  // ../../packages/persistence/src/warn-era-cap.ts
23379
- import { existsSync as existsSync2, writeFileSync as writeFileSync3 } from "fs";
23847
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
23380
23848
  import { join as join5 } from "path";
23381
23849
  var MARKER = "warn-era-capped";
23382
23850
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
@@ -23384,7 +23852,7 @@ function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
23384
23852
  const marker = join5(dataDir2, MARKER);
23385
23853
  if (existsSync2(marker)) return { capped: 0, skipped: "already-run" };
23386
23854
  const capped = db.policies.capCategoryActions();
23387
- writeFileSync3(marker, `${new Date(Date.now()).toISOString()}
23855
+ writeFileSync2(marker, `${new Date(Date.now()).toISOString()}
23388
23856
  `, { mode: DATA_FILE_MODE });
23389
23857
  return { capped };
23390
23858
  }
@@ -23439,6 +23907,12 @@ function resolveProvider() {
23439
23907
 
23440
23908
  // ../../packages/plugin-sdk/src/config.ts
23441
23909
  function loadConfig(base = defaultDataDir()) {
23910
+ try {
23911
+ ensureLayoutDirSync(base);
23912
+ const settingsFile = join6(settingsDir(base), "settings.json");
23913
+ if (existsSync3(settingsFile)) tightenFile(settingsFile);
23914
+ } catch {
23915
+ }
23442
23916
  migrateLegacyLayout(base);
23443
23917
  const settings = readWorkspaceSettings(base);
23444
23918
  return {
@@ -23461,7 +23935,7 @@ function resolveProviderSafe() {
23461
23935
  // ../../packages/plugin-sdk/src/config-inventory.ts
23462
23936
  import { readdirSync, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
23463
23937
  import { homedir as homedir2 } from "os";
23464
- import { basename as basename3, join as join7 } from "path";
23938
+ import { basename as basename3, join as join8 } from "path";
23465
23939
 
23466
23940
  // ../../packages/detections/src/egress/registry.ts
23467
23941
  var EXTRACTOR_VERSION = "1";
@@ -26494,7 +26968,7 @@ function ensureBundledPacks() {
26494
26968
  return false;
26495
26969
  }
26496
26970
  }
26497
- function scanText(text) {
26971
+ function scanText(text, ruleVersions) {
26498
26972
  if (!ensureBundledPacks()) return { masked: "[REDACTED]", findings: [] };
26499
26973
  try {
26500
26974
  const rules = getLoadedRules();
@@ -26506,7 +26980,7 @@ function scanText(text) {
26506
26980
  return {
26507
26981
  ruleId: m.ruleId,
26508
26982
  ruleName: rule?.name ?? m.ruleId,
26509
- ruleVersion: String(rule?.specVersion ?? 1),
26983
+ ruleVersion: ruleVersions?.[m.ruleId] ?? String(rule?.specVersion ?? 1),
26510
26984
  category: m.category,
26511
26985
  severity: m.severity,
26512
26986
  span: m.span,
@@ -26524,8 +26998,8 @@ function maskText(text) {
26524
26998
  }
26525
26999
 
26526
27000
  // ../../packages/plugin-sdk/src/repo.ts
26527
- import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
26528
- import { basename as basename2, dirname, isAbsolute, join as join6, sep as sep2 } from "path";
27001
+ import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
27002
+ import { basename as basename2, dirname, isAbsolute, join as join7, sep as sep2 } from "path";
26529
27003
  function resolveRepoIdentity(cwd) {
26530
27004
  try {
26531
27005
  const root = findGitRoot(cwd);
@@ -26575,7 +27049,7 @@ function resolveGitBranch(cwd) {
26575
27049
  try {
26576
27050
  const root = findGitRoot(cwd);
26577
27051
  if (!root) return void 0;
26578
- const dotGit = join6(root, ".git");
27052
+ const dotGit = join7(root, ".git");
26579
27053
  let gitdir;
26580
27054
  try {
26581
27055
  gitdir = statSync(dotGit).isDirectory() ? dotGit : resolveWorktreeGitdir(root, dotGit);
@@ -26583,7 +27057,7 @@ function resolveGitBranch(cwd) {
26583
27057
  return void 0;
26584
27058
  }
26585
27059
  if (gitdir === void 0) return void 0;
26586
- const head = safeRead(join6(gitdir, "HEAD"));
27060
+ const head = safeRead(join7(gitdir, "HEAD"));
26587
27061
  if (!head) return void 0;
26588
27062
  return /^ref:\s*refs\/heads\/(.+?)\s*$/m.exec(head)?.[1];
26589
27063
  } catch {
@@ -26593,37 +27067,37 @@ function resolveGitBranch(cwd) {
26593
27067
  function resolveWorktreeGitdir(root, dotGitFile) {
26594
27068
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGitFile) ?? "")?.[1];
26595
27069
  if (!target) return void 0;
26596
- return isAbsolute(target) ? target : join6(root, target);
27070
+ return isAbsolute(target) ? target : join7(root, target);
26597
27071
  }
26598
27072
  function findGitRoot(start) {
26599
27073
  let dir = start;
26600
27074
  for (; ; ) {
26601
- if (existsSync3(join6(dir, ".git"))) return dir;
27075
+ if (existsSync4(join7(dir, ".git"))) return dir;
26602
27076
  const parent = dirname(dir);
26603
27077
  if (parent === dir) return void 0;
26604
27078
  dir = parent;
26605
27079
  }
26606
27080
  }
26607
27081
  function resolveGitContext(root) {
26608
- const dotGit = join6(root, ".git");
27082
+ const dotGit = join7(root, ".git");
26609
27083
  try {
26610
27084
  if (statSync(dotGit).isDirectory()) {
26611
- return { configPath: join6(dotGit, "config"), headRoot: root };
27085
+ return { configPath: join7(dotGit, "config"), headRoot: root };
26612
27086
  }
26613
27087
  } catch {
26614
27088
  return void 0;
26615
27089
  }
26616
27090
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
26617
27091
  if (!target) return void 0;
26618
- const gitdir = isAbsolute(target) ? target : join6(root, target);
26619
- if (existsSync3(join6(gitdir, "config"))) {
26620
- return { configPath: join6(gitdir, "config"), headRoot: root };
27092
+ const gitdir = isAbsolute(target) ? target : join7(root, target);
27093
+ if (existsSync4(join7(gitdir, "config"))) {
27094
+ return { configPath: join7(gitdir, "config"), headRoot: root };
26621
27095
  }
26622
- const commonRaw = safeRead(join6(gitdir, "commondir"))?.trim();
27096
+ const commonRaw = safeRead(join7(gitdir, "commondir"))?.trim();
26623
27097
  if (!commonRaw) return void 0;
26624
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join6(gitdir, commonRaw);
27098
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join7(gitdir, commonRaw);
26625
27099
  const headRoot = basename2(commonGitDir) === ".git" ? dirname(commonGitDir) : root;
26626
- return { configPath: join6(commonGitDir, "config"), headRoot };
27100
+ return { configPath: join7(commonGitDir, "config"), headRoot };
26627
27101
  }
26628
27102
  function safeRead(path) {
26629
27103
  try {
@@ -26689,31 +27163,31 @@ function resolveConfigInventory(input) {
26689
27163
  };
26690
27164
  try {
26691
27165
  const home = input.homeDir ?? homedir2();
26692
- const claudeDir = join7(home, ".claude");
27166
+ const claudeDir = join8(home, ".claude");
26693
27167
  const repo = resolveRepoIdentity(input.cwd);
26694
27168
  const repoIdentity = repo?.url ?? input.cwd;
26695
27169
  const projectSource = `project:${repoIdentity}`;
26696
- collectSettingsHooks(scan2, join7(claudeDir, "settings.json"), "user");
26697
- collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.json"), "project");
26698
- collectSettingsHooks(scan2, join7(input.cwd, ".claude", "settings.local.json"), "local");
27170
+ collectSettingsHooks(scan2, join8(claudeDir, "settings.json"), "user");
27171
+ collectSettingsHooks(scan2, join8(input.cwd, ".claude", "settings.json"), "project");
27172
+ collectSettingsHooks(scan2, join8(input.cwd, ".claude", "settings.local.json"), "local");
26699
27173
  const projectOrigin = { scope: "project", project: repoIdentity };
26700
- collectMcpFile(scan2, join7(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
26701
- collectUserClaudeJson(scan2, join7(home, ".claude.json"), input.cwd, repoIdentity);
26702
- collectMcpFile(scan2, join7(claudeDir, "settings.json"), { scope: "user" });
26703
- collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.json"), projectOrigin);
26704
- collectMcpFile(scan2, join7(input.cwd, ".claude", "settings.local.json"), {
27174
+ collectMcpFile(scan2, join8(input.cwd, ".mcp.json"), projectOrigin, { recordErrors: true });
27175
+ collectUserClaudeJson(scan2, join8(home, ".claude.json"), input.cwd, repoIdentity);
27176
+ collectMcpFile(scan2, join8(claudeDir, "settings.json"), { scope: "user" });
27177
+ collectMcpFile(scan2, join8(input.cwd, ".claude", "settings.json"), projectOrigin);
27178
+ collectMcpFile(scan2, join8(input.cwd, ".claude", "settings.local.json"), {
26705
27179
  scope: "local",
26706
27180
  project: repoIdentity
26707
27181
  });
26708
27182
  collectConfigFiles(scan2, claudeDir, input.cwd);
26709
- collectSkillsDir(scan2, join7(claudeDir, "skills"), { source: "local", scope: "user" });
26710
- collectSkillsDir(scan2, join7(input.cwd, ".claude", "skills"), {
27183
+ collectSkillsDir(scan2, join8(claudeDir, "skills"), { source: "local", scope: "user" });
27184
+ collectSkillsDir(scan2, join8(input.cwd, ".claude", "skills"), {
26711
27185
  source: projectSource,
26712
27186
  scope: "project"
26713
27187
  });
26714
27188
  collectInstalledPlugins(scan2, claudeDir);
26715
27189
  collectMarketplaceSkills(scan2, claudeDir);
26716
- collectSkillsDir(scan2, join7(input.cwd, "skills"), { source: projectSource, scope: "project" });
27190
+ collectSkillsDir(scan2, join8(input.cwd, "skills"), { source: projectSource, scope: "project" });
26717
27191
  scan2.skills = dedupeSkills(scan2.skills);
26718
27192
  scan2.mcpServers = dedupeMcpServers(scan2.mcpServers);
26719
27193
  } catch (err) {
@@ -26842,7 +27316,7 @@ function projectEntryFor(projects, cwd) {
26842
27316
  return void 0;
26843
27317
  }
26844
27318
  function collectPluginManifestMcp(scan2, installPath, origin) {
26845
- const manifestPath = join7(installPath, ".claude-plugin", "plugin.json");
27319
+ const manifestPath = join8(installPath, ".claude-plugin", "plugin.json");
26846
27320
  const raw = readOptional(manifestPath);
26847
27321
  if (raw === void 0) return;
26848
27322
  try {
@@ -26850,7 +27324,7 @@ function collectPluginManifestMcp(scan2, installPath, origin) {
26850
27324
  if (typeof parsed !== "object" || parsed === null) return;
26851
27325
  const declared = parsed.mcpServers;
26852
27326
  if (typeof declared === "string" && declared.length > 0) {
26853
- collectMcpFile(scan2, join7(installPath, declared), origin, { recordErrors: true });
27327
+ collectMcpFile(scan2, join8(installPath, declared), origin, { recordErrors: true });
26854
27328
  } else {
26855
27329
  collectMcpObject(scan2, declared, manifestPath, origin);
26856
27330
  }
@@ -26867,14 +27341,14 @@ var SETTINGS_KEY_LABELS = [
26867
27341
  ["statusLine", "status line"]
26868
27342
  ];
26869
27343
  function collectConfigFiles(scan2, claudeDir, cwd) {
26870
- settingsConfigFile(scan2, join7(claudeDir, "settings.json"), "user", "User settings");
26871
- settingsConfigFile(scan2, join7(cwd, ".claude", "settings.json"), "project", "Project settings");
26872
- settingsConfigFile(scan2, join7(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
26873
- memoryConfigFile(scan2, join7(claudeDir, "CLAUDE.md"), "user", "User memory");
26874
- memoryConfigFile(scan2, join7(cwd, "CLAUDE.md"), "project", "Project memory");
26875
- mcpJsonConfigFile(scan2, join7(cwd, ".mcp.json"));
26876
- dirConfigFile(scan2, join7(cwd, ".claude", "commands"), "Slash commands", "command");
26877
- dirConfigFile(scan2, join7(cwd, ".claude", "agents"), "Subagents", "subagent");
27344
+ settingsConfigFile(scan2, join8(claudeDir, "settings.json"), "user", "User settings");
27345
+ settingsConfigFile(scan2, join8(cwd, ".claude", "settings.json"), "project", "Project settings");
27346
+ settingsConfigFile(scan2, join8(cwd, ".claude", "settings.local.json"), "local", "Local overrides");
27347
+ memoryConfigFile(scan2, join8(claudeDir, "CLAUDE.md"), "user", "User memory");
27348
+ memoryConfigFile(scan2, join8(cwd, "CLAUDE.md"), "project", "Project memory");
27349
+ mcpJsonConfigFile(scan2, join8(cwd, ".mcp.json"));
27350
+ dirConfigFile(scan2, join8(cwd, ".claude", "commands"), "Slash commands", "command");
27351
+ dirConfigFile(scan2, join8(cwd, ".claude", "agents"), "Subagents", "subagent");
26878
27352
  }
26879
27353
  function configFileEntry(path, scope, kind) {
26880
27354
  try {
@@ -26949,7 +27423,7 @@ function countMarkdownFiles(dir, depth) {
26949
27423
  let count = 0;
26950
27424
  for (const dirent of readdirSync(dir, { withFileTypes: true })) {
26951
27425
  if (dirent.name.startsWith(".")) continue;
26952
- if (dirent.isDirectory()) count += countMarkdownFiles(join7(dir, dirent.name), depth + 1);
27426
+ if (dirent.isDirectory()) count += countMarkdownFiles(join8(dir, dirent.name), depth + 1);
26953
27427
  else if (dirent.name.endsWith(".md")) count += 1;
26954
27428
  }
26955
27429
  return count;
@@ -26962,7 +27436,7 @@ function collectSkillsDir(scan2, dir, origin) {
26962
27436
  return;
26963
27437
  }
26964
27438
  for (const name of names) {
26965
- const skillFile = join7(dir, name, "SKILL.md");
27439
+ const skillFile = join8(dir, name, "SKILL.md");
26966
27440
  try {
26967
27441
  const raw = readOptional(skillFile);
26968
27442
  if (raw === void 0) continue;
@@ -26971,7 +27445,7 @@ function collectSkillsDir(scan2, dir, origin) {
26971
27445
  name: front.name ?? name,
26972
27446
  source: origin.source,
26973
27447
  scope: origin.scope,
26974
- location: join7(dir, name),
27448
+ location: join8(dir, name),
26975
27449
  updatedAt: statSync2(skillFile).mtime.toISOString()
26976
27450
  };
26977
27451
  const version2 = front.version ?? origin.defaultVersion;
@@ -27002,7 +27476,7 @@ function parseFrontmatter(raw) {
27002
27476
  return out;
27003
27477
  }
27004
27478
  function collectInstalledPlugins(scan2, claudeDir) {
27005
- const manifestPath = join7(claudeDir, "plugins", "installed_plugins.json");
27479
+ const manifestPath = join8(claudeDir, "plugins", "installed_plugins.json");
27006
27480
  const raw = readOptional(manifestPath);
27007
27481
  if (raw === void 0) return;
27008
27482
  let plugins;
@@ -27027,7 +27501,7 @@ function collectInstalledPlugins(scan2, claudeDir) {
27027
27501
  if (typeof installPath !== "string" || seen.has(installPath)) continue;
27028
27502
  seen.add(installPath);
27029
27503
  const version2 = install.version;
27030
- const hooksPath = join7(installPath, "hooks", "hooks.json");
27504
+ const hooksPath = join8(installPath, "hooks", "hooks.json");
27031
27505
  const hooksRaw = readOptional(hooksPath);
27032
27506
  if (hooksRaw !== void 0) {
27033
27507
  try {
@@ -27047,22 +27521,22 @@ function collectInstalledPlugins(scan2, claudeDir) {
27047
27521
  }
27048
27522
  const origin = { source: marketplace, scope: "plugin", pluginName };
27049
27523
  if (typeof version2 === "string") origin.defaultVersion = version2;
27050
- collectSkillsDir(scan2, join7(installPath, "skills"), origin);
27524
+ collectSkillsDir(scan2, join8(installPath, "skills"), origin);
27051
27525
  const mcpOrigin = { scope: "plugin", pluginName, marketplace };
27052
- collectMcpFile(scan2, join7(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
27526
+ collectMcpFile(scan2, join8(installPath, ".mcp.json"), mcpOrigin, { recordErrors: true });
27053
27527
  collectPluginManifestMcp(scan2, installPath, mcpOrigin);
27054
27528
  }
27055
27529
  }
27056
27530
  }
27057
27531
  function collectMarketplaceSkills(scan2, claudeDir) {
27058
- for (const mp of readMarketplaces(join7(claudeDir, "plugins", "known_marketplaces.json"))) {
27532
+ for (const mp of readMarketplaces(join8(claudeDir, "plugins", "known_marketplaces.json"))) {
27059
27533
  if (isClaudeOfficialMarketplace(mp.name, mp.repo)) continue;
27060
- collectSkillsDir(scan2, join7(mp.installLocation, "skills"), {
27534
+ collectSkillsDir(scan2, join8(mp.installLocation, "skills"), {
27061
27535
  source: mp.name,
27062
27536
  scope: "plugin"
27063
27537
  });
27064
- collectPluginSkillDirs(scan2, join7(mp.installLocation, "plugins"), mp.name);
27065
- collectPluginSkillDirs(scan2, join7(mp.installLocation, "external_plugins"), mp.name);
27538
+ collectPluginSkillDirs(scan2, join8(mp.installLocation, "plugins"), mp.name);
27539
+ collectPluginSkillDirs(scan2, join8(mp.installLocation, "external_plugins"), mp.name);
27066
27540
  }
27067
27541
  }
27068
27542
  function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
@@ -27073,7 +27547,7 @@ function collectPluginSkillDirs(scan2, pluginsDir, marketplace) {
27073
27547
  return;
27074
27548
  }
27075
27549
  for (const plugin of plugins) {
27076
- collectSkillsDir(scan2, join7(pluginsDir, plugin, "skills"), {
27550
+ collectSkillsDir(scan2, join8(pluginsDir, plugin, "skills"), {
27077
27551
  source: marketplace,
27078
27552
  scope: "plugin",
27079
27553
  pluginName: plugin
@@ -27147,10 +27621,7 @@ function str2(value) {
27147
27621
  }
27148
27622
 
27149
27623
  // ../../packages/plugin-sdk/src/events.ts
27150
- import { createHash as createHash3, randomUUID as randomUUID9 } from "crypto";
27151
-
27152
- // ../../packages/plugin-sdk/src/finding-key.ts
27153
- import { createHash as createHash4 } from "crypto";
27624
+ import { createHash as createHash4, randomUUID as randomUUID9 } from "crypto";
27154
27625
 
27155
27626
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
27156
27627
  import { arch, hostname as hostname3, platform, release } from "os";
@@ -27182,22 +27653,22 @@ function resolveInventoryContext(input) {
27182
27653
  }
27183
27654
 
27184
27655
  // ../../packages/plugin-sdk/src/nudge.ts
27185
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
27186
- import { join as join8 } from "path";
27656
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
27657
+ import { join as join9 } from "path";
27187
27658
  var SESSION_START_MARKER = "session-start-last";
27188
27659
  function claimSessionStart(dataDir2, sessionId) {
27189
27660
  return claimOncePerSession(dataDir2, SESSION_START_MARKER, sessionId);
27190
27661
  }
27191
27662
  function claimOncePerSession(dataDir2, marker, sessionId) {
27192
27663
  if (!sessionId) return true;
27193
- const path = join8(dataDir2, marker);
27664
+ const path = join9(dataDir2, marker);
27194
27665
  try {
27195
27666
  if (readFileSync5(path, "utf8") === sessionId) return false;
27196
27667
  } catch {
27197
27668
  }
27198
27669
  try {
27199
- mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
27200
- writeFileSync4(path, sessionId, { mode: DATA_FILE_MODE });
27670
+ mkdirSync2(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
27671
+ writeFileSync3(path, sessionId, { mode: DATA_FILE_MODE });
27201
27672
  } catch {
27202
27673
  }
27203
27674
  return true;
@@ -27209,8 +27680,8 @@ import { basename as basename4, dirname as dirname2, sep as sep3 } from "path";
27209
27680
 
27210
27681
  // ../../packages/plugin-sdk/src/project-files.ts
27211
27682
  var import_ignore = __toESM(require_ignore(), 1);
27212
- import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27213
- import { basename as basename5, join as join9, relative, sep as sep4 } from "path";
27683
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
27684
+ import { basename as basename5, join as join10, relative, sep as sep4 } from "path";
27214
27685
  var SKIP_DIRS = /* @__PURE__ */ new Set([
27215
27686
  ".git",
27216
27687
  "node_modules",
@@ -27230,7 +27701,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
27230
27701
  var MAX_FILES = 2e4;
27231
27702
  function readIgnoreLayer(dir) {
27232
27703
  try {
27233
- const content = readFileSync6(join9(dir, ".gitignore"), "utf8");
27704
+ const content = readFileSync6(join10(dir, ".gitignore"), "utf8");
27234
27705
  return { base: dir, matcher: (0, import_ignore.default)().add(content) };
27235
27706
  } catch {
27236
27707
  return void 0;
@@ -27308,10 +27779,10 @@ function resolveProjectFiles(cwd) {
27308
27779
  const layer = readIgnoreLayer(dir);
27309
27780
  const dirLayers = layer ? [...layers, layer] : layers;
27310
27781
  for (const entry of dirents) {
27311
- const fullPath = join9(dir, entry.name);
27782
+ const fullPath = join10(dir, entry.name);
27312
27783
  if (entry.isDirectory()) {
27313
27784
  if (SKIP_DIRS.has(entry.name) || isIgnored(dirLayers, fullPath, true)) continue;
27314
- if (existsSync4(join9(fullPath, ".git"))) continue;
27785
+ if (existsSync5(join10(fullPath, ".git"))) continue;
27315
27786
  if (visit2(fullPath, dirLayers)) return true;
27316
27787
  continue;
27317
27788
  }
@@ -27352,17 +27823,17 @@ import { randomUUID as randomUUID10 } from "crypto";
27352
27823
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
27353
27824
 
27354
27825
  // ../../packages/plugin-sdk/src/throttle.ts
27355
- import { mkdirSync as mkdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
27356
- import { join as join10 } from "path";
27826
+ import { mkdirSync as mkdirSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
27827
+ import { join as join11 } from "path";
27357
27828
  function throttled(dataDir2, markerName, windowMs) {
27358
- const marker = join10(dataDir2, markerName);
27829
+ const marker = join11(dataDir2, markerName);
27359
27830
  try {
27360
27831
  if (Date.now() - statSync3(marker).mtimeMs < windowMs) return true;
27361
27832
  } catch {
27362
27833
  }
27363
27834
  try {
27364
- mkdirSync4(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
27365
- writeFileSync5(marker, String(Date.now()), { mode: DATA_FILE_MODE });
27835
+ mkdirSync3(dataDir2, { recursive: true, mode: DATA_DIR_MODE });
27836
+ writeFileSync4(marker, String(Date.now()), { mode: DATA_FILE_MODE });
27366
27837
  } catch {
27367
27838
  }
27368
27839
  return false;
@@ -27400,7 +27871,8 @@ var StandaloneDataGateway = class {
27400
27871
  }
27401
27872
  // The id is minted inside the repository from the natural key — the plugin can't
27402
27873
  // import @akasecurity/persistence to compute it, so the gateway is the boundary that
27403
- // hands the natural key across. INSERT OR IGNORE → idempotent re-reads.
27874
+ // hands the natural key across. UPSERT-take-MAX → idempotent re-reads that also
27875
+ // converge a streaming partial/final split (see insertLlmCall).
27404
27876
  recordLlmCall(input) {
27405
27877
  this.db.auditEvents.insertLlmCall(input);
27406
27878
  return Promise.resolve();
@@ -27442,7 +27914,9 @@ var StandaloneDataGateway = class {
27442
27914
  // caller's transaction (Layer 2b). The audit-event id the findings FK into is the
27443
27915
  // SAME content-addressed `toolCallId` the leaf insert mints, so both re-read
27444
27916
  // idempotently. Definitions/classified-data are idempotent upserts; findings are
27445
- // content-addressed INSERT OR IGNORE.
27917
+ // content-addressed upserts (ON CONFLICT(id) DO UPDATE SET inspection_definition_id),
27918
+ // so a re-detection under a bumped rule version repoints the definition FK rather
27919
+ // than no-opping.
27446
27920
  writeToolCall(input) {
27447
27921
  this.db.auditEvents.insertToolCall(input);
27448
27922
  if (input.inspections.length === 0) return;
@@ -27462,7 +27936,7 @@ var StandaloneDataGateway = class {
27462
27936
  });
27463
27937
  const classifiedDataId2 = this.db.classifiedData.upsert({ class: insp.category });
27464
27938
  this.db.inspectionFindings.insertFinding({
27465
- id: inspectionFindingId(auditEventId, definitionId, insp.span.start, insp.span.end),
27939
+ id: inspectionFindingId(auditEventId, insp.ruleId, insp.span.start, insp.span.end),
27466
27940
  auditEventId,
27467
27941
  inspectionDefinitionId: definitionId,
27468
27942
  classifiedDataId: classifiedDataId2,
@@ -27511,10 +27985,17 @@ var StandaloneDataGateway = class {
27511
27985
  try {
27512
27986
  const snapshot = this.db.installedPacks.installedRuleset();
27513
27987
  if (snapshot.installedPacks === 0) return void 0;
27514
- if (snapshot.enabledPacks === 0) return { rules: [], ruleActions: /* @__PURE__ */ new Map(), complete: true };
27988
+ if (snapshot.enabledPacks === 0) {
27989
+ return { rules: [], ruleActions: /* @__PURE__ */ new Map(), ruleVersions: /* @__PURE__ */ new Map(), complete: true };
27990
+ }
27515
27991
  if (snapshot.invalidRules > 0) return void 0;
27516
27992
  if (snapshot.rules.length === 0) return void 0;
27517
- return { rules: snapshot.rules, ruleActions: snapshot.ruleActions, complete: true };
27993
+ return {
27994
+ rules: snapshot.rules,
27995
+ ruleActions: snapshot.ruleActions,
27996
+ ruleVersions: snapshot.ruleVersions,
27997
+ complete: true
27998
+ };
27518
27999
  } catch {
27519
28000
  return void 0;
27520
28001
  }
@@ -27542,6 +28023,7 @@ var StandaloneDataGateway = class {
27542
28023
  policies: [...policies, ...rulePolicies],
27543
28024
  rules: installed ? installed.rules : [],
27544
28025
  ...installed ? { rulesComplete: true } : {},
28026
+ ...installed ? { ruleVersions: Object.fromEntries(installed.ruleVersions) } : {},
27545
28027
  ...exceptions !== void 0 ? { exceptions } : {},
27546
28028
  customKeywords,
27547
28029
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -27807,7 +28289,7 @@ function buildSessionRoot(sessionId, input, ctx, resolved, provider, branch) {
27807
28289
 
27808
28290
  // src/history/reconcile-trigger.ts
27809
28291
  import { spawn } from "child_process";
27810
- import { dirname as dirname3, join as join12 } from "path";
28292
+ import { dirname as dirname3, join as join13 } from "path";
27811
28293
  import { fileURLToPath } from "url";
27812
28294
 
27813
28295
  // src/history/tail.ts
@@ -27815,13 +28297,13 @@ import { createHash as createHash5 } from "crypto";
27815
28297
  import {
27816
28298
  closeSync,
27817
28299
  fstatSync,
27818
- mkdirSync as mkdirSync5,
28300
+ mkdirSync as mkdirSync4,
27819
28301
  openSync,
27820
28302
  readFileSync as readFileSync7,
27821
28303
  readSync,
27822
- writeFileSync as writeFileSync6
28304
+ writeFileSync as writeFileSync5
27823
28305
  } from "fs";
27824
- import { join as join11 } from "path";
28306
+ import { join as join12 } from "path";
27825
28307
  var SAFE_SESSION_ID = /^[A-Za-z0-9._-]+$/;
27826
28308
  function safeSessionId(sessionId) {
27827
28309
  if (SAFE_SESSION_ID.test(sessionId) && sessionId !== "." && sessionId !== "..") {
@@ -27838,7 +28320,7 @@ function triggerReconcile(dataDir2, sessionId, transcriptPath) {
27838
28320
  const marker = `${RECONCILE_MARKER_PREFIX}-${safeSessionId(sessionId)}`;
27839
28321
  if (throttled(dataDir2, marker, RECONCILE_THROTTLE_MS)) return;
27840
28322
  const here = dirname3(fileURLToPath(import.meta.url));
27841
- const child = spawn(process.execPath, [join12(here, "reconcile.js"), sessionId, transcriptPath], {
28323
+ const child = spawn(process.execPath, [join13(here, "reconcile.js"), sessionId, transcriptPath], {
27842
28324
  detached: true,
27843
28325
  stdio: "ignore"
27844
28326
  });