@akasecurity/ai-tc-claude-code 0.9.10 → 0.9.11

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,6 @@ var require_ignore = __commonJS({
494
494
  // src/backfill.ts
495
495
  import { fileURLToPath as fileURLToPath4 } from "url";
496
496
 
497
- // ../../packages/plugin-runtime/src/attached/egress-wire.ts
498
- import { createHash as createHash4 } from "crypto";
499
-
500
497
  // ../../packages/persistence/src/attached-derived.ts
501
498
  import { rmSync } from "fs";
502
499
  import { join } from "path";
@@ -20748,13 +20745,11 @@ var FindingGroup = external_exports.object({
20748
20745
  latestDetectedAt: external_exports.iso.datetime(),
20749
20746
  instances: external_exports.array(FindingInstance),
20750
20747
  // Derived from instances' statuses with open-dominates precedence (see
20751
- // buildFindingGroups). Undefined only when no instance carries a status.
20748
+ // foldGroupStatus). Undefined only when no instance carries a status.
20752
20749
  status: FindingStatus.optional(),
20753
- // The distinct people across the WHOLE group, not just the `instances`
20754
- // preview — from the store's whole-group aggregate when it supplies one,
20755
- // else folded from the rows (see buildFindingGroups). Undefined when no
20756
- // instance carries a user, or when the store supplied whole-group folds
20757
- // without one.
20750
+ // The distinct people across the WHOLE group, not just the instances
20751
+ // carried here. Undefined when no instance carries a user, or when the
20752
+ // store supplied whole-group folds without one.
20758
20753
  users: external_exports.array(FindingUser).optional()
20759
20754
  }).meta({ id: "FindingGroup" });
20760
20755
  var FindingStats = external_exports.object({
@@ -20783,21 +20778,31 @@ var FindingFacets = external_exports.object({
20783
20778
  // counted under no value.
20784
20779
  status: external_exports.array(FindingFacetItem),
20785
20780
  // Host tool (attributes.tool_name). Present only on the instance-level
20786
- // reads, which can filter by it; the grouped read omits the dimension
20781
+ // reads, which can filter by it; the type-level read omits the dimension
20787
20782
  // because a group spans tools.
20788
20783
  tool: external_exports.array(FindingFacetItem).optional()
20789
20784
  }).meta({ id: "FindingFacets" });
20790
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
20791
- var ListGroupedFindingsQuery = external_exports.object({
20785
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20786
+ id: "FindingTypeSummary"
20787
+ });
20788
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
20789
+ var MAX_FINDING_TYPES_LIMIT = 100;
20790
+ var ListFindingTypesQuery = external_exports.object({
20792
20791
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
20793
- // FindingAction.
20792
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
20793
+ // firing version carries, and this list pages types.
20794
+ //
20795
+ // That is NOT a claim the findings of a type share it. A rule can hold several
20796
+ // definition versions at different severities, so a type kept by this filter
20797
+ // can hold findings that individually do not match — see totals.findings on
20798
+ // ListFindingTypesResponse, which counts them all.
20794
20799
  severity: external_exports.array(Severity).optional(),
20795
20800
  subtype: external_exports.array(external_exports.string()).optional(),
20796
20801
  provider: external_exports.array(FindingProvider).optional(),
20797
20802
  action: external_exports.array(FindingAction).optional(),
20798
- // Matches a group's DERIVED status (see FindingGroup.status), not its
20799
- // individual instances' — so a filtered group's Status column always reads
20800
- // one of the requested values.
20803
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
20804
+ // individual findings' — so a filtered row's status always reads one of the
20805
+ // requested values.
20801
20806
  status: external_exports.array(FindingStatus).optional(),
20802
20807
  q: external_exports.string().optional(),
20803
20808
  // Scope to findings whose event carries this session id (the Activity page's
@@ -20807,23 +20812,37 @@ var ListGroupedFindingsQuery = external_exports.object({
20807
20812
  // from a time-scoped page (Activity's range) can carry that scope. Absent
20808
20813
  // means all time — this list has no default window.
20809
20814
  from: external_exports.iso.datetime().optional(),
20810
- // A group or instance id that must appear in the page even when the cursor
20811
- // has already advanced past its sort position. This is what keeps the
20812
- // Findings page's one-shot ?finding= deep link resolving once the list
20813
- // paginates: the target group is appended out of sort order rather than
20814
- // scanning forward for it. Never affects totals, facets or the cursor.
20815
+ // A RULE id that must appear in the page even when the cursor has already
20816
+ // advanced past its sort position. This is what keeps the selected type
20817
+ // visible in the list once it paginates: the target is appended out of sort
20818
+ // order rather than scanned forward for. Never affects totals, facets or the
20819
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
20820
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
20821
+ // and so is not bounded by what any page happens to hold.
20815
20822
  includeId: external_exports.string().optional(),
20816
- groupBy: external_exports.literal("type").optional(),
20817
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
20823
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
20818
20824
  cursor: external_exports.string().optional()
20819
20825
  });
20820
- var ListGroupedFindingsResponse = external_exports.object({
20826
+ var ListFindingTypesResponse = external_exports.object({
20821
20827
  totals: external_exports.object({
20828
+ // Findings belonging to the matching TYPES — not findings that each match
20829
+ // the filters. The filters here select types, so a type that survives
20830
+ // contributes its whole instanceCount.
20831
+ //
20832
+ // `status` is the one exception, narrowed per finding via
20833
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
20834
+ // this can exceed what the instance read reports for the same filters: a
20835
+ // rule whose severity moved between versions is kept on its newest and
20836
+ // still counts its older findings. Narrowing the other three needs
20837
+ // per-dimension counts the aggregate does not carry today.
20822
20838
  findings: external_exports.number().int().nonnegative(),
20823
- groups: external_exports.number().int().nonnegative()
20839
+ // Counts TYPES, which is the unit this read pages. The instance read's
20840
+ // own totals count findings; the two deliberately answer different
20841
+ // questions and are never summed.
20842
+ types: external_exports.number().int().nonnegative()
20824
20843
  }),
20825
20844
  facets: FindingFacets,
20826
- items: external_exports.array(FindingGroup),
20845
+ items: external_exports.array(FindingTypeSummary),
20827
20846
  nextCursor: external_exports.string().nullable(),
20828
20847
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
20829
20848
  // many times that rule fired in the session's persisted transcript. Findings
@@ -20831,7 +20850,7 @@ var ListGroupedFindingsResponse = external_exports.object({
20831
20850
  // every firing, so the two numbers legitimately differ — this map lets a
20832
20851
  // session-scoped view show both.
20833
20852
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
20834
- }).meta({ id: "ListGroupedFindingsResponse" });
20853
+ }).meta({ id: "ListFindingTypesResponse" });
20835
20854
  var ApplyFindingActionRequest = external_exports.object({
20836
20855
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
20837
20856
  // it, so it is excluded from the request contract. The mapping helper
@@ -20861,12 +20880,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
20861
20880
  var MAX_FLAT_FINDINGS_LIMIT = 200;
20862
20881
  var ListFindingInstancesQuery = external_exports.object({
20863
20882
  severity: external_exports.array(Severity).optional(),
20864
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
20883
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
20884
+ // ONE of them is how the master/detail view scopes its right-hand panel.
20865
20885
  subtype: external_exports.array(external_exports.string()).optional(),
20866
20886
  provider: external_exports.array(FindingProvider).optional(),
20867
20887
  action: external_exports.array(FindingAction).optional(),
20868
20888
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
20869
- // the grouped query's group-level fold.
20889
+ // the types query's type-level fold.
20870
20890
  status: external_exports.array(FindingStatus).optional(),
20871
20891
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20872
20892
  // where the free-text `q` can only match the rendered "via Bash" label.
@@ -20883,37 +20903,47 @@ var ListFindingInstancesQuery = external_exports.object({
20883
20903
  });
20884
20904
  var ListFindingInstancesResponse = external_exports.object({
20885
20905
  // Instances matching the filters across the whole scope, not just this
20886
- // page — cursor-independent, like the grouped list's totals.
20906
+ // page — cursor-independent, like the types list's totals.
20887
20907
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
20888
- // Counts in INSTANCES here, where the grouped response counts groups. Each
20908
+ // Counts in INSTANCES here, where the types response counts types. Each
20889
20909
  // dimension still excludes its own filter.
20890
20910
  facets: FindingFacets,
20891
20911
  items: external_exports.array(FindingInstanceDetail),
20892
20912
  nextCursor: external_exports.string().nullable()
20893
20913
  }).meta({ id: "ListFindingInstancesResponse" });
20894
- var FindingLocationFile = external_exports.object({
20895
- // Empty when the instances carried no file path (a prompt or a tool call
20896
- // with no file attribution).
20897
- file: external_exports.string(),
20898
- instanceCount: external_exports.number().int().nonnegative(),
20899
- maxSeverity: Severity,
20900
- latestDetectedAt: external_exports.iso.datetime(),
20901
- // Folded from the instances' derived statuses with the same
20902
- // open-dominates precedence a group uses.
20903
- status: FindingStatus.optional(),
20904
- // Distinct rules seen at this location, capped — the row shows them as
20905
- // chips, and the count is what conveys scale.
20906
- ruleIds: external_exports.array(external_exports.string())
20907
- }).meta({ id: "FindingLocationFile" });
20908
- var FindingLocationRepo = external_exports.object({
20914
+ var FindingLocationSummary = external_exports.object({
20915
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
20916
+ // because a location's identity is two values and a URL param carries one:
20917
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
20918
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
20919
+ // client's page dedupe — never decoded, and never a sort key.
20920
+ id: external_exports.string(),
20909
20921
  /** Empty when the instances carried no repo attribute. */
20910
20922
  repo: external_exports.string(),
20923
+ // Empty when the instances carried no file path (a prompt, or a tool call
20924
+ // with no file attribution). Both halves empty is a real location — usually
20925
+ // the largest one in a store — and is selectable like any other.
20926
+ file: external_exports.string(),
20911
20927
  instanceCount: external_exports.number().int().nonnegative(),
20928
+ // The WORST severity present, not the first row's. It is this list's primary
20929
+ // sort key, so it is also what explains why a row is where it is, and it is
20930
+ // how a reader decides what to open without opening everything.
20912
20931
  maxSeverity: Severity,
20913
20932
  latestDetectedAt: external_exports.iso.datetime(),
20933
+ // Folded from the instances' derived statuses with the same open-dominates
20934
+ // precedence a group uses, so it answers "is anything left to do here" and
20935
+ // not much more: a location holding 1 open among 40 resolved reads like one
20936
+ // holding 40 open. That loss is accepted — the panel beside this list
20937
+ // carries each finding's own status, and instanceCount sits next to the
20938
+ // badge.
20914
20939
  status: FindingStatus.optional(),
20915
- files: external_exports.array(FindingLocationFile)
20916
- }).meta({ id: "FindingLocationRepo" });
20940
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
20941
+ // tally rather than a sample and a row can say how many there are. Bounded
20942
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
20943
+ ruleIds: external_exports.array(external_exports.string())
20944
+ }).meta({ id: "FindingLocationSummary" });
20945
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
20946
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
20917
20947
  var ListFindingLocationsQuery = external_exports.object({
20918
20948
  severity: external_exports.array(Severity).optional(),
20919
20949
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -20926,18 +20956,42 @@ var ListFindingLocationsQuery = external_exports.object({
20926
20956
  q: external_exports.string().optional(),
20927
20957
  sessionId: external_exports.string().optional(),
20928
20958
  from: external_exports.iso.datetime().optional(),
20929
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
20959
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
20960
+ // even when the cursor has already advanced past its sort position — the
20961
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
20962
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
20963
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
20964
+ // into the thousands, a selection sitting off page 0 is the ordinary case
20965
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
20966
+ includeId: external_exports.string().optional(),
20967
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
20968
+ cursor: external_exports.string().optional()
20930
20969
  });
20931
20970
  var ListFindingLocationsResponse = external_exports.object({
20932
20971
  totals: external_exports.object({
20972
+ // Findings matching the filters across the whole scope. Unlike the types
20973
+ // read's same-named field this needs no caveat: the filters here narrow
20974
+ // per finding, so this is the sum of every row's instanceCount.
20933
20975
  findings: external_exports.number().int().nonnegative(),
20934
- repos: external_exports.number().int().nonnegative(),
20935
- files: external_exports.number().int().nonnegative()
20976
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
20977
+ // states. The facets beside it count FINDINGS (see below); a surface
20978
+ // showing both says which is which.
20979
+ locations: external_exports.number().int().nonnegative()
20936
20980
  }),
20937
- /** Sorted by max severity, then most recent. */
20938
- items: external_exports.array(FindingLocationRepo),
20939
- /** Whether `limit` truncated the repo list. */
20940
- hasMore: external_exports.boolean()
20981
+ // Counts in FINDINGS, where the types response counts types, each dimension
20982
+ // still excluding its own filter. Deliberately not locations: counting those
20983
+ // needs a set of location keys per dimension per value — memory tracking the
20984
+ // store times the vocabulary, in a read whose scan promises flat memory —
20985
+ // and the cheap per-location version is not an approximation but WRONG. A
20986
+ // location holding {claudecode, block} and {codex, warn} would survive
20987
+ // provider=claudecode AND action=warn, under which no single finding
20988
+ // matches, so the facet would contradict the instanceCount this whole view
20989
+ // rests on. Findings also keep the toolbar in the same unit as the page
20990
+ // tally and the panel it sits above.
20991
+ facets: FindingFacets,
20992
+ /** Sorted by max severity, then most recent, then (repo, file). */
20993
+ items: external_exports.array(FindingLocationSummary),
20994
+ nextCursor: external_exports.string().nullable()
20941
20995
  }).meta({ id: "ListFindingLocationsResponse" });
20942
20996
 
20943
20997
  // ../../packages/schema/src/zod/meta.ts
@@ -22106,6 +22160,14 @@ var ControlPlaneErrorBody = external_exports.object({
22106
22160
  message: external_exports.string().optional()
22107
22161
  }).optional()
22108
22162
  });
22163
+ var RemoteFailureKind = external_exports.enum([
22164
+ "unauthorized",
22165
+ "forbidden",
22166
+ "route-absent",
22167
+ "invalid-request",
22168
+ "rejected",
22169
+ "unreachable"
22170
+ ]);
22109
22171
  var AttachDeviceRequest = external_exports.object({
22110
22172
  // This machine's own continuity id, so re-attaching ROTATES the credential
22111
22173
  // on one machine record instead of producing a second one. Client-minted
@@ -22799,139 +22861,62 @@ function deriveFindingStatus(row) {
22799
22861
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
22800
22862
  return "open";
22801
22863
  }
22802
- function distinctUsers(instances) {
22803
- const seen = /* @__PURE__ */ new Set();
22804
- const users = [];
22805
- for (const i of instances) {
22806
- if (i.user === void 0 || seen.has(i.user.id)) continue;
22807
- seen.add(i.user.id);
22808
- users.push(i.user);
22809
- }
22810
- return users;
22811
- }
22812
22864
  function sortUsers(users) {
22813
22865
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
22814
22866
  }
22815
- function buildFindingGroups(rows, opts = {}) {
22816
- const overrides = opts.overrides;
22867
+ function buildFindingTypes(aggregates, opts = {}) {
22817
22868
  const packNames = opts.packNames;
22818
- const aggregates = opts.aggregates;
22819
- const byRuleId = /* @__PURE__ */ new Map();
22820
- for (const row of rows) {
22821
- const existing = byRuleId.get(row.ruleId);
22822
- if (existing) existing.push(row);
22823
- else byRuleId.set(row.ruleId, [row]);
22824
- }
22825
- const groups = [];
22826
- for (const [ruleId, ruleRows] of byRuleId) {
22827
- const instances = ruleRows.map((r) => {
22828
- const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
22829
- return {
22830
- id: r.id,
22831
- provider: toApiProvider(r.sourceTool),
22832
- repo: r.repo,
22833
- file: r.file,
22834
- ...r.toolName === void 0 ? {} : { toolName: r.toolName },
22835
- ...r.eventId === void 0 ? {} : { eventId: r.eventId },
22836
- ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
22837
- ...r.user === void 0 ? {} : { user: r.user },
22838
- action: toApiAction(effectiveDbAction),
22839
- detectedAt: r.occurredAt,
22840
- confidence: r.confidence,
22841
- status: r.status
22842
- };
22843
- });
22844
- const agg = aggregates?.get(ruleId);
22845
- const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
22846
- const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
22847
- (max, r) => r.occurredAt > max ? r.occurredAt : max,
22848
- ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
22849
- );
22850
- const seenProviders = /* @__PURE__ */ new Set();
22851
- const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
22852
- if (seenProviders.has(p)) return false;
22853
- seenProviders.add(p);
22854
- return true;
22855
- });
22856
- const actionSet = new Set(
22857
- agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
22858
- );
22869
+ const types = [];
22870
+ for (const [ruleId, agg] of aggregates) {
22871
+ const users = sortUsers(agg.users ?? []);
22872
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
22873
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
22859
22874
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
22860
- const severity = ruleRows[0]?.severity ?? "low";
22861
- const detection = {
22862
- id: ruleId,
22863
- name: packNames?.get(ruleId) ?? null
22864
- };
22865
- const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
22866
- const policy = { id: `category:${apiCategory}`, name: apiCategory };
22867
- const match = {
22868
- maskedValue: ruleRows[0]?.maskedMatch ?? "",
22869
- contextPrefix: ""
22870
- // empty (pending privacy review)
22871
- };
22872
- const status = foldGroupStatus(
22873
- agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
22874
- );
22875
- const group = {
22875
+ const apiCategory = toApiCategory(agg.category ?? "custom");
22876
+ const type = {
22876
22877
  id: ruleId,
22877
22878
  category: apiCategory,
22878
22879
  subtype: ruleId,
22879
22880
  // human label comes with pack metadata later
22880
- severity,
22881
- match,
22882
- detection,
22883
- policy,
22884
- instanceCount: agg?.instanceCount ?? instances.length,
22881
+ severity: agg.severity ?? "low",
22882
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
22883
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
22884
+ instanceCount: agg.instanceCount,
22885
22885
  providers,
22886
22886
  aggregateAction,
22887
- latestDetectedAt,
22888
- instances,
22889
- status,
22887
+ latestDetectedAt: agg.latestDetectedAt,
22888
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
22890
22889
  ...users.length > 0 ? { users } : {}
22891
22890
  };
22892
- if (agg) {
22893
- actionsCache.set(group, [...actionSet]);
22894
- if (agg.searchText !== void 0) {
22895
- haystackCache.set(group, buildHaystack(group, agg.searchText));
22896
- }
22891
+ actionsCache.set(type, [...actionSet]);
22892
+ if (agg.searchText !== void 0) {
22893
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
22897
22894
  }
22898
- groups.push(group);
22895
+ types.push(type);
22899
22896
  }
22900
- return groups;
22897
+ return types;
22901
22898
  }
22902
22899
  var haystackCache = /* @__PURE__ */ new WeakMap();
22903
- function buildHaystack(g, extra) {
22900
+ function buildHaystack(t, extra) {
22904
22901
  return [
22905
- g.subtype,
22906
- g.category,
22907
- g.match.maskedValue,
22908
- g.policy.name,
22909
- g.id,
22910
- ...g.instances.map((i) => i.repo),
22911
- ...g.instances.map((i) => i.file),
22912
- ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
22913
- ...g.instances.map((i) => i.id),
22914
- // The people: the whole group's list when the store folded one, plus the
22915
- // preview's own — the two overlap, and a haystack does not mind.
22916
- ...(g.users ?? []).map((u) => u.name),
22917
- ...g.instances.map((i) => i.user?.name ?? ""),
22902
+ t.subtype,
22903
+ t.category,
22904
+ t.policy.name,
22905
+ t.id,
22906
+ ...(t.users ?? []).map((u) => u.name),
22918
22907
  ...extra === void 0 ? [] : [extra]
22919
22908
  ].join(" ").toLowerCase();
22920
22909
  }
22921
- function groupHaystack(g) {
22922
- const cached2 = haystackCache.get(g);
22910
+ function typeHaystack(t) {
22911
+ const cached2 = haystackCache.get(t);
22923
22912
  if (cached2 !== void 0) return cached2;
22924
- const haystack = buildHaystack(g);
22925
- haystackCache.set(g, haystack);
22913
+ const haystack = buildHaystack(t);
22914
+ haystackCache.set(t, haystack);
22926
22915
  return haystack;
22927
22916
  }
22928
22917
  var actionsCache = /* @__PURE__ */ new WeakMap();
22929
- function groupActions(g) {
22930
- const cached2 = actionsCache.get(g);
22931
- if (cached2 !== void 0) return cached2;
22932
- const actions = [...new Set(g.instances.map((i) => i.action))];
22933
- actionsCache.set(g, actions);
22934
- return actions;
22918
+ function typeActions(t) {
22919
+ return actionsCache.get(t) ?? [];
22935
22920
  }
22936
22921
  function countInstancesByStatus(statusInputs, statuses) {
22937
22922
  const statusSet = new Set(statuses);
@@ -22942,8 +22927,8 @@ function countInstancesByStatus(statusInputs, statuses) {
22942
22927
  }
22943
22928
  return sum;
22944
22929
  }
22945
- function applyFindingFilters(groups, opts) {
22946
- let filtered = groups;
22930
+ function applyFindingFilters(types, opts) {
22931
+ let filtered = types;
22947
22932
  if (opts.severity && opts.severity.length > 0) {
22948
22933
  const sevSet = new Set(opts.severity);
22949
22934
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -22954,7 +22939,7 @@ function applyFindingFilters(groups, opts) {
22954
22939
  }
22955
22940
  if (opts.actions && opts.actions.length > 0) {
22956
22941
  const actionSet = new Set(opts.actions);
22957
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
22942
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
22958
22943
  }
22959
22944
  if (opts.subtype && opts.subtype.length > 0) {
22960
22945
  const subtypeSet = new Set(opts.subtype);
@@ -22966,7 +22951,7 @@ function applyFindingFilters(groups, opts) {
22966
22951
  }
22967
22952
  if (opts.q) {
22968
22953
  const q = opts.q.toLowerCase();
22969
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
22954
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
22970
22955
  }
22971
22956
  return filtered;
22972
22957
  }
@@ -22981,11 +22966,11 @@ function compareFindingGroupOrder(a, b) {
22981
22966
  if (recencyDiff !== 0) return recencyDiff;
22982
22967
  return a.id.localeCompare(b.id);
22983
22968
  }
22984
- function sortFindingGroups(groups) {
22985
- return [...groups].sort(compareFindingGroupOrder);
22969
+ function sortFindingTypes(types) {
22970
+ return [...types].sort(compareFindingGroupOrder);
22986
22971
  }
22987
- function computeFindingFacets(allGroups, opts) {
22988
- const forSeverity = applyFindingFilters(allGroups, {
22972
+ function computeFindingFacets(allTypes, opts) {
22973
+ const forSeverity = applyFindingFilters(allTypes, {
22989
22974
  providers: opts.providers,
22990
22975
  actions: opts.actions,
22991
22976
  statuses: opts.statuses,
@@ -22996,7 +22981,7 @@ function computeFindingFacets(allGroups, opts) {
22996
22981
  for (const g of forSeverity) {
22997
22982
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
22998
22983
  }
22999
- const forProvider = applyFindingFilters(allGroups, {
22984
+ const forProvider = applyFindingFilters(allTypes, {
23000
22985
  actions: opts.actions,
23001
22986
  statuses: opts.statuses,
23002
22987
  q: opts.q,
@@ -23007,7 +22992,7 @@ function computeFindingFacets(allGroups, opts) {
23007
22992
  for (const g of forProvider) {
23008
22993
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
23009
22994
  }
23010
- const forAction = applyFindingFilters(allGroups, {
22995
+ const forAction = applyFindingFilters(allTypes, {
23011
22996
  providers: opts.providers,
23012
22997
  statuses: opts.statuses,
23013
22998
  q: opts.q,
@@ -23016,9 +23001,9 @@ function computeFindingFacets(allGroups, opts) {
23016
23001
  });
23017
23002
  const actionMap = /* @__PURE__ */ new Map();
23018
23003
  for (const g of forAction) {
23019
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23004
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23020
23005
  }
23021
- const forSubtype = applyFindingFilters(allGroups, {
23006
+ const forSubtype = applyFindingFilters(allTypes, {
23022
23007
  providers: opts.providers,
23023
23008
  actions: opts.actions,
23024
23009
  statuses: opts.statuses,
@@ -23027,7 +23012,7 @@ function computeFindingFacets(allGroups, opts) {
23027
23012
  });
23028
23013
  const subtypeMap = /* @__PURE__ */ new Map();
23029
23014
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
23030
- const forStatus = applyFindingFilters(allGroups, {
23015
+ const forStatus = applyFindingFilters(allTypes, {
23031
23016
  providers: opts.providers,
23032
23017
  actions: opts.actions,
23033
23018
  q: opts.q,
@@ -23075,10 +23060,20 @@ function matchesDimension(row, opts, dimension) {
23075
23060
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23076
23061
  case "tools":
23077
23062
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23063
+ // An EMPTY value is a real filter here, not an absent one. The location
23064
+ // list buckets a finding whose event recorded no repo — or no file — under
23065
+ // the empty string, and selecting that bucket has to narrow the panel to
23066
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23067
+ // row omits the key, which every call site already does.
23068
+ //
23069
+ // Reading '' as unset is what this replaced, and it failed in the one place
23070
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23071
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23072
+ // — a row reading 3 findings beside a panel listing every finding there is.
23078
23073
  case "repo":
23079
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23074
+ return opts.repo === void 0 || row.repo === opts.repo;
23080
23075
  case "file":
23081
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23076
+ return opts.file === void 0 || row.file === opts.file;
23082
23077
  case "q":
23083
23078
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23084
23079
  }
@@ -23192,6 +23187,23 @@ function addToLocation(acc, row) {
23192
23187
  acc.statuses.push(row.status);
23193
23188
  acc.ruleIds.add(row.ruleId);
23194
23189
  }
23190
+ function compareLocationOrder(a, b) {
23191
+ const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23192
+ const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23193
+ if (rankA !== rankB) return rankA - rankB;
23194
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23195
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23196
+ }
23197
+ if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23198
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23199
+ return 0;
23200
+ }
23201
+ function encodeLocationId(repo, file2) {
23202
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23203
+ }
23204
+ function encodePart(value) {
23205
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23206
+ }
23195
23207
 
23196
23208
  // ../../packages/schema/src/zod/installed-pack.ts
23197
23209
  var InstalledPack = external_exports.object({
@@ -23705,11 +23717,11 @@ var WorkspaceSettings = external_exports.object({
23705
23717
  // covers the current payload and must be re-granted.
23706
23718
  modelJudgeConsent: ModelJudgeConsent.optional(),
23707
23719
  // Records that the user consented to the DEFERRED send — the outbox — along
23708
- // with the payload shape and the endpoint they agreed to. Since payload v2
23709
- // that covers both the pre-attach backlog and undelivered captures (which
23710
- // carry prompt/reply text in `content`); the key name predates the widening.
23711
- // Absent until granted, and a grant for a different endpoint or an older
23712
- // payload no longer counts.
23720
+ // with the payload shape and the endpoint they agreed to. Since payload v3
23721
+ // that covers the pre-attach backlog AND undelivered captures alike, and both
23722
+ // carry prompt/reply/tool-output text in `content`; the key name predates
23723
+ // both widenings. Absent until granted, and a grant for a different endpoint
23724
+ // or an older payload no longer counts.
23713
23725
  historySyncConsent: HistorySyncConsent.optional()
23714
23726
  });
23715
23727
  function defaultWorkspaceSettings() {
@@ -23840,6 +23852,9 @@ var ManagedSettingKey = external_exports.enum([
23840
23852
  "dataSharesInPlace",
23841
23853
  "redactFallback"
23842
23854
  ]).meta({ id: "ManagedSettingKey" });
23855
+ function isManagedSettingKey(value) {
23856
+ return ManagedSettingKey.safeParse(value).success;
23857
+ }
23843
23858
  var ManagedSettingsValues = external_exports.object({
23844
23859
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
23845
23860
  controlPlane: external_exports.object({
@@ -23864,7 +23879,27 @@ var ManagedSettings = external_exports.object({
23864
23879
  // Which of those the user may not change. A key here with no matching value
23865
23880
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23866
23881
  // the user may still override. The two are separable on purpose.
23867
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23882
+ //
23883
+ // Parsed as NAMES rather than as the enum, and split below: a name this
23884
+ // build does not know is dropped from the locked set and reported, never a
23885
+ // reason to refuse the file. The same shape reaches an older build whenever
23886
+ // an administrator locks a key a newer build added, and refusing it there
23887
+ // ran that build entirely unmanaged — every pin and lock gone — on exactly
23888
+ // the fleets most likely to carry a version skew. A name outside the enum
23889
+ // is still never HONOURED: the lockable set stays explicit above.
23890
+ lockedFields: external_exports.array(external_exports.string()).default([])
23891
+ }).transform(({ lockedFields, ...rest }) => {
23892
+ const known = [];
23893
+ const unknown2 = [];
23894
+ for (const name of lockedFields) {
23895
+ if (isManagedSettingKey(name)) known.push(name);
23896
+ else unknown2.push(name);
23897
+ }
23898
+ return {
23899
+ ...rest,
23900
+ lockedFields: known,
23901
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
23902
+ };
23868
23903
  }).meta({ id: "ManagedSettings" });
23869
23904
 
23870
23905
  // ../../packages/schema/src/zod/project-files.ts
@@ -23988,7 +24023,11 @@ var FindingsTimeseriesPoint = external_exports.object({
23988
24023
  timestamp: external_exports.iso.date(),
23989
24024
  critical: external_exports.number().int().nonnegative(),
23990
24025
  high: external_exports.number().int().nonnegative(),
23991
- medium: external_exports.number().int().nonnegative()
24026
+ medium: external_exports.number().int().nonnegative(),
24027
+ // Optional and additive, so a producer written against the earlier
24028
+ // three-series contract keeps validating. A consumer plotting it resolves the
24029
+ // absent case itself — the chart point requires a number.
24030
+ low: external_exports.number().int().nonnegative().optional()
23992
24031
  }).meta({ id: "FindingsTimeseriesPoint" });
23993
24032
  var FindingsTimeseriesResponse = external_exports.object({
23994
24033
  range: TimeRange,
@@ -24014,6 +24053,10 @@ var ResolvedFeedItem = external_exports.object({
24014
24053
  findingKey: external_exports.string(),
24015
24054
  ruleId: external_exports.string(),
24016
24055
  severity: Severity,
24056
+ // Repository slug, and the file path RELATIVE to it. The pair is what
24057
+ // identifies the file: a bare path matches the same name in every repo.
24058
+ // Optional and additive; empty when the event carried no repo.
24059
+ repo: external_exports.string().optional(),
24017
24060
  path: external_exports.string(),
24018
24061
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
24019
24062
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -25587,7 +25630,8 @@ var SqliteActivityRepository = class {
25587
25630
  SELECT 1 FROM audit_events d
25588
25631
  WHERE d.root_session_id = audit_events.id
25589
25632
  AND (d.content LIKE ? ESCAPE '\\'
25590
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
25633
+ OR coalesce(json_extract(d.attributes, '$.detail'),
25634
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
25591
25635
  );
25592
25636
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
25593
25637
  }
@@ -26925,23 +26969,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26925
26969
  )`;
26926
26970
 
26927
26971
  // ../../packages/persistence/src/repositories/findings.ts
26928
- var PREVIEW_INSTANCES_PER_GROUP = 200;
26929
- var DEFAULT_LOCATIONS_LIMIT = 100;
26930
- var LOCATION_RULE_IDS_CAP = 20;
26931
- function compareLocationOrder(a, b) {
26932
- return compareFindingGroupOrder(
26933
- {
26934
- severity: a.maxSeverity,
26935
- latestDetectedAt: a.latestDetectedAt,
26936
- id: ""
26937
- },
26938
- {
26939
- severity: b.maxSeverity,
26940
- latestDetectedAt: b.latestDetectedAt,
26941
- id: ""
26942
- }
26943
- );
26944
- }
26945
26972
  var CONCAT_SEP = ",";
26946
26973
  var TUPLE_SEP = "|";
26947
26974
  function splitConcat(value) {
@@ -26993,13 +27020,48 @@ function decodeGroupCursor(cursor) {
26993
27020
  return null;
26994
27021
  }
26995
27022
  function firstAfter(sorted, cursor) {
26996
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
27023
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
26997
27024
  return index === -1 ? sorted.length : index;
26998
27025
  }
26999
27026
  function findDeepLinked(sorted, page, id) {
27000
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
27001
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
27027
+ if (page.some((t) => t.id === id)) return void 0;
27028
+ return sorted.find((t) => t.id === id);
27002
27029
  }
27030
+ function encodeLocationCursor(location) {
27031
+ const payload = {
27032
+ sev: location.maxSeverity,
27033
+ t: location.latestDetectedAt,
27034
+ r: location.repo,
27035
+ f: location.file
27036
+ };
27037
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
27038
+ }
27039
+ function decodeLocationCursor(cursor) {
27040
+ const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
27041
+ if (parsed2 !== void 0 && typeof parsed2.sev === "string" && typeof parsed2.t === "string" && typeof parsed2.r === "string" && typeof parsed2.f === "string") {
27042
+ return { maxSeverity: parsed2.sev, latestDetectedAt: parsed2.t, repo: parsed2.r, file: parsed2.f };
27043
+ }
27044
+ return null;
27045
+ }
27046
+ function firstLocationAfter(sorted, cursor) {
27047
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
27048
+ return index === -1 ? sorted.length : index;
27049
+ }
27050
+ function findDeepLinkedLocation(sorted, page, id) {
27051
+ if (page.some((l) => l.id === id)) return void 0;
27052
+ return sorted.find((l) => l.id === id);
27053
+ }
27054
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
27055
+ d.severity AS severity, f.masked_match AS masked_match,
27056
+ f.action_taken AS action_taken, f.confidence AS confidence,
27057
+ e.started_at AS occurred_at,
27058
+ e.source_tool AS source_tool,
27059
+ e.repo AS repo,
27060
+ e.file_path AS file,
27061
+ e.tool_name AS tool_name,
27062
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
27063
+ e.event_type AS kind, f.finding_key AS finding_key,
27064
+ ${latestResolutionStatusSql("f")} AS latest_status`;
27003
27065
  var DAY_MS3 = 864e5;
27004
27066
  var SqliteFindingsRepository = class {
27005
27067
  constructor(db) {
@@ -27120,30 +27182,26 @@ var SqliteFindingsRepository = class {
27120
27182
  );
27121
27183
  }
27122
27184
  /**
27123
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
27124
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
27125
- * attributes bag, rule_id/category/severity from the definition), scoped to
27126
- * the four capture kinds (audit_events also holds structural/reconciler/scan
27127
- * rows this list must never surface), groups by ruleId, computes
27128
- * per-filter-excluded facets, applies the requested filters, and sorts by
27129
- * severity then recency. Filtering
27130
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
27131
- * reflect the full filtered set; `items` is the requested
27132
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
27133
- * filter, `totals.findings` counts only instances whose derived status was
27134
- * requested, and each item's instance preview is narrowed the same way.
27185
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
27186
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
27187
+ * list must never surface), with per-filter-excluded facets, the requested
27188
+ * filters applied, and sorted by severity then recency. Filtering and faceting
27189
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
27190
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
27191
+ * Under a `status` filter, `totals.findings` counts only findings whose
27192
+ * derived status was requested.
27193
+ *
27194
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
27195
+ * folding EVERY finding into the numbers a type row and the filters need
27196
+ * (count, severity, category, providers, actions, statuses, latest, search
27197
+ * text). The findings OF a type come from listFindingInstances scoped to
27198
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
27135
27199
  *
27136
- * Two reads, neither of which materializes a row per finding:
27137
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
27138
- * the group and the filters need (count, providers, actions, statuses,
27139
- * latest, search text);
27140
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
27141
- * populate `instances` for the table's expanded rows.
27142
27200
  * The aggregates carry raw DB values and are translated by the same
27143
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
27144
- * rule is ever restated in SQL.
27201
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
27202
+ * status rule is ever restated in SQL.
27145
27203
  */
27146
- listGroupedFindings(query) {
27204
+ listFindingTypes(query) {
27147
27205
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
27148
27206
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
27149
27207
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -27156,12 +27214,7 @@ var SqliteFindingsRepository = class {
27156
27214
  predicate,
27157
27215
  params: sessionParams
27158
27216
  });
27159
- const rows = this.previewRows(aggregates, {
27160
- sessionId: query.sessionId,
27161
- from: query.from
27162
- });
27163
- const groupable = rows.map(toFlatFindingRow);
27164
- const allGroups = buildFindingGroups(groupable, { aggregates });
27217
+ const allTypes = buildFindingTypes(aggregates);
27165
27218
  const filterOpts = {
27166
27219
  severity: query.severity,
27167
27220
  providers: query.provider,
@@ -27170,30 +27223,25 @@ var SqliteFindingsRepository = class {
27170
27223
  subtype: query.subtype,
27171
27224
  q: query.q
27172
27225
  };
27173
- const facets = computeFindingFacets(allGroups, filterOpts);
27174
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
27226
+ const facets = computeFindingFacets(allTypes, filterOpts);
27227
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
27175
27228
  const statusFilter = query.status ?? [];
27176
27229
  const totals = {
27177
- findings: sorted.reduce((acc, g) => {
27178
- if (statusFilter.length === 0) return acc + g.instanceCount;
27179
- const agg = aggregates.get(g.id);
27180
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
27230
+ findings: sorted.reduce((acc, t) => {
27231
+ if (statusFilter.length === 0) return acc + t.instanceCount;
27232
+ const agg = aggregates.get(t.id);
27233
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
27181
27234
  }, 0),
27182
- groups: sorted.length
27235
+ types: sorted.length
27183
27236
  };
27184
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
27237
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
27185
27238
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
27186
27239
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
27187
27240
  const page = sorted.slice(start, start + limit);
27188
27241
  const lastOnPage = page.at(-1);
27189
27242
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
27190
27243
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
27191
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
27192
- const narrow = (g) => statusSet ? {
27193
- ...g,
27194
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
27195
- } : g;
27196
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
27244
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
27197
27245
  return Promise.resolve({
27198
27246
  totals,
27199
27247
  facets,
@@ -27204,7 +27252,7 @@ var SqliteFindingsRepository = class {
27204
27252
  }
27205
27253
  /**
27206
27254
  * One row per rule_id, folding EVERY instance of the group into the values
27207
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
27255
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
27208
27256
  * distinct rule_ids (the installed packs' rules), not by the store's size.
27209
27257
  *
27210
27258
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -27298,13 +27346,25 @@ var SqliteFindingsRepository = class {
27298
27346
  });
27299
27347
  }
27300
27348
  /**
27301
- * The same findings folded by location: repository, then file within it.
27349
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
27302
27350
  *
27303
27351
  * The grouping keys come from the capturing event's attributes, which is what
27304
- * the local store relates a finding to — there is no finding↔asset row to
27305
- * group by instead. A repo or file the event did not record folds into the
27306
- * empty-string bucket, which the view renders but does not link, since no
27307
- * filter can name it.
27352
+ * the local store relates a finding to; there is no finding↔asset row to group
27353
+ * by instead. A repo or file the event did not record folds into the
27354
+ * empty-string bucket, which is a real location like any other: it is listed,
27355
+ * it is selectable, and its `?loc=` token is as good as any other row's.
27356
+ *
27357
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
27358
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
27359
+ * list was rebuilt to remove — and two-level pagination inside an
27360
+ * expand/collapse table is what pushed that view to master/detail in the first
27361
+ * place.
27362
+ *
27363
+ * Every filter narrows the FINDINGS and the locations fall out of what
27364
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
27365
+ * reports for the same filters scoped to that pair. The view depends on it:
27366
+ * one toolbar sits over both panels precisely because a location owns none of
27367
+ * its fields.
27308
27368
  */
27309
27369
  listFindingLocations(query) {
27310
27370
  const opts = {
@@ -27316,13 +27376,16 @@ var SqliteFindingsRepository = class {
27316
27376
  tools: query.tool,
27317
27377
  q: query.q
27318
27378
  };
27319
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
27379
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
27380
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
27320
27381
  const byRepo = /* @__PURE__ */ new Map();
27382
+ const accumulator = createInstanceFacetAccumulator(opts);
27321
27383
  let total = 0;
27322
27384
  for (const row of this.scanFindingRows({
27323
27385
  sessionId: query.sessionId,
27324
27386
  from: query.from
27325
27387
  })) {
27388
+ accumulator.add(row);
27326
27389
  if (!matchesInstanceFilters(row, opts)) continue;
27327
27390
  total += 1;
27328
27391
  let files = byRepo.get(row.repo);
@@ -27337,103 +27400,35 @@ var SqliteFindingsRepository = class {
27337
27400
  }
27338
27401
  addToLocation(acc, row);
27339
27402
  }
27340
- let fileCount = 0;
27341
- const repos = [...byRepo.entries()].map(([repo, files]) => {
27342
- fileCount += files.size;
27343
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
27344
- file: file2,
27345
- instanceCount: acc.instanceCount,
27346
- maxSeverity: acc.maxSeverity,
27347
- latestDetectedAt: acc.latestDetectedAt,
27348
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
27349
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
27350
- })).sort(compareLocationOrder);
27351
- const rollup = fileRows.reduce(
27352
- (a, f) => ({
27353
- instanceCount: a.instanceCount + f.instanceCount,
27354
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
27355
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
27356
- }),
27357
- {
27358
- instanceCount: 0,
27359
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
27360
- latestDetectedAt: ""
27361
- }
27362
- );
27363
- const statuses = fileRows.map((f) => f.status);
27364
- const folded = foldGroupStatus(statuses);
27365
- return {
27366
- repo,
27367
- instanceCount: rollup.instanceCount,
27368
- maxSeverity: rollup.maxSeverity,
27369
- latestDetectedAt: rollup.latestDetectedAt,
27370
- ...folded === void 0 ? {} : { status: folded },
27371
- files: fileRows
27372
- };
27373
- });
27374
- repos.sort(compareLocationOrder);
27403
+ const sorted = [];
27404
+ for (const [repo, files] of byRepo) {
27405
+ for (const [file2, acc] of files) {
27406
+ const status = foldGroupStatus(acc.statuses);
27407
+ sorted.push({
27408
+ id: encodeLocationId(repo, file2),
27409
+ repo,
27410
+ file: file2,
27411
+ instanceCount: acc.instanceCount,
27412
+ maxSeverity: acc.maxSeverity,
27413
+ latestDetectedAt: acc.latestDetectedAt,
27414
+ ...status === void 0 ? {} : { status },
27415
+ ruleIds: [...acc.ruleIds]
27416
+ });
27417
+ }
27418
+ }
27419
+ sorted.sort(compareLocationOrder);
27420
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
27421
+ const page = sorted.slice(start, start + limit);
27422
+ const lastOnPage = page.at(-1);
27423
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
27424
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
27375
27425
  return Promise.resolve({
27376
- totals: { findings: total, repos: repos.length, files: fileCount },
27377
- items: repos.slice(0, limit),
27378
- hasMore: repos.length > limit
27426
+ totals: { findings: total, locations: sorted.length },
27427
+ facets: accumulator.facets(),
27428
+ items: [...page, ...deepLinked ? [deepLinked] : []],
27429
+ nextCursor
27379
27430
  });
27380
27431
  }
27381
- /**
27382
- * Each group's newest instances, for the table's expanded rows.
27383
- *
27384
- * ONE index-ordered scan with early termination, and the shape is the point.
27385
- * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27386
- * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27387
- * through a temp B-tree to keep a bounded preview of each group, and then
27388
- * sorts the survivors again for the page order. Both sorts grow with the
27389
- * store while the answer does not.
27390
- *
27391
- * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27392
- * (or the session or window index the scope names — see `findingScanSql`),
27393
- * which is already the order the page wants, and keeps rows per rule until
27394
- * each rule has as many as it can show. The aggregate the caller already holds
27395
- * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27396
- * per rule, summed, is the number of rows this scan has to find, and it stops
27397
- * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27398
- * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27399
- * store with many firing rules widens it. The bound that DOES hold
27400
- * unconditionally is the sorted form's floor: this scan visits at most as
27401
- * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27402
- * sorted, and stops the moment every rule has its cap, where the sorted form
27403
- * sorts the whole scope regardless. The true worst case — the rarest rule's
27404
- * wanted instances sitting at the tail of the scope — is one pass over
27405
- * everything in scope with a block sort of the id tie-break only, never a
27406
- * sort of the scope, which is still that floor.
27407
- *
27408
- * A row whose rule the aggregate did not see is skipped: the two statements
27409
- * run without a shared snapshot, so a capture landing between them can add a
27410
- * rule here that has no counts there, and the counts are what the group is
27411
- * built from.
27412
- */
27413
- previewRows(aggregates, scope) {
27414
- const wanted = /* @__PURE__ */ new Map();
27415
- let remaining = 0;
27416
- for (const [ruleId, agg] of aggregates) {
27417
- const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27418
- wanted.set(ruleId, n);
27419
- remaining += n;
27420
- }
27421
- const rows = [];
27422
- if (remaining === 0) return rows;
27423
- const { sql, params } = this.findingScanSql(scope);
27424
- const taken = /* @__PURE__ */ new Map();
27425
- for (const r of iterateRows(this.db.prepare(sql), params)) {
27426
- const want = wanted.get(r.rule_id);
27427
- if (want === void 0) continue;
27428
- const have = taken.get(r.rule_id) ?? 0;
27429
- if (have >= want) continue;
27430
- taken.set(r.rule_id, have + 1);
27431
- rows.push(r);
27432
- remaining -= 1;
27433
- if (remaining === 0) break;
27434
- }
27435
- return rows;
27436
- }
27437
27432
  /**
27438
27433
  * Every finding in scope as a FlatFindingRow, newest first, streamed.
27439
27434
  *
@@ -27460,6 +27455,33 @@ var SqliteFindingsRepository = class {
27460
27455
  yield toFlatFindingRow(r);
27461
27456
  }
27462
27457
  }
27458
+ /**
27459
+ * One finding by its own id, or null when no such row exists.
27460
+ *
27461
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
27462
+ * the store — and, unlike anything derived from a list page, it resolves a
27463
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
27464
+ * deep link needs: the id it carries may name a finding thousands of rows
27465
+ * older than anything a first page holds.
27466
+ *
27467
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
27468
+ * RESOLVES an id; whether that row would survive the list's current filters is
27469
+ * a different question, and hiding the target because a filter excludes it is
27470
+ * worse than showing it.
27471
+ *
27472
+ * `groupId` on the result IS the rule id, so this one read answers both "which
27473
+ * type should the list select?" and "what does the drawer show?".
27474
+ */
27475
+ findingInstance(id) {
27476
+ const row = this.db.prepare(
27477
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
27478
+ FROM inspection_findings f
27479
+ JOIN audit_events e ON e.id = f.audit_event_id
27480
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27481
+ WHERE f.id = ?`
27482
+ ).get(id);
27483
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
27484
+ }
27463
27485
  /**
27464
27486
  * The one statement both instance-level scans run: every finding in scope,
27465
27487
  * joined to its event and definition, newest first.
@@ -27493,17 +27515,7 @@ var SqliteFindingsRepository = class {
27493
27515
  conditions.push("e.started_at >= ?");
27494
27516
  params.push(isoToEpochMillis(scope.from));
27495
27517
  }
27496
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27497
- d.severity AS severity, f.masked_match AS masked_match,
27498
- f.action_taken AS action_taken, f.confidence AS confidence,
27499
- e.started_at AS occurred_at,
27500
- e.source_tool AS source_tool,
27501
- e.repo AS repo,
27502
- e.file_path AS file,
27503
- e.tool_name AS tool_name,
27504
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27505
- e.event_type AS kind, f.finding_key AS finding_key,
27506
- ${latestResolutionStatusSql("f")} AS latest_status
27518
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
27507
27519
  FROM audit_events e
27508
27520
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27509
27521
  CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -27517,6 +27529,26 @@ var SqliteFindingsRepository = class {
27517
27529
  group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27518
27530
  const rows = this.db.prepare(
27519
27531
  `SELECT rule_id,
27532
+ -- BARE columns beside max(latest_at), which is deliberate and
27533
+ -- is SQLite's documented behaviour: with a single min()/max()
27534
+ -- in an aggregate query, every bare column takes its value from
27535
+ -- the row that produced the extremum. So these are the severity
27536
+ -- and category of the definition whose finding is NEWEST, which
27537
+ -- is what the row-based build they replaced read off its first
27538
+ -- (newest-first) row.
27539
+ --
27540
+ -- min() is WRONG here and was the defect: inspection_definitions
27541
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
27542
+ -- mints a new row), so a rule whose severity moved between
27543
+ -- versions has several, and min() picks the ALPHABETICALLY
27544
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
27545
+ -- That is arbitrary in direction, and it feeds the badge, the
27546
+ -- filter, the facet counts and the primary sort key.
27547
+ --
27548
+ -- Adding a second min()/max() aggregate here would make these
27549
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
27550
+ severity,
27551
+ category,
27520
27552
  sum(tuple_count) AS instance_count,
27521
27553
  max(latest_at) AS latest_at,
27522
27554
  group_concat(source_tools) AS source_tools,
@@ -27527,6 +27559,14 @@ var SqliteFindingsRepository = class {
27527
27559
  group_concat(tool_names) AS tool_names
27528
27560
  FROM (
27529
27561
  SELECT d.rule_id AS rule_id,
27562
+ -- Severity and category are columns of the DEFINITION, and
27563
+ -- a rule can have SEVERAL definitions (one per version), so
27564
+ -- these are grouped on below and resolved to the newest
27565
+ -- firing version by the outer query's bare-column select.
27566
+ -- They ride the aggregate because the type build has no rows
27567
+ -- to read them off \u2014 see buildFindingTypes.
27568
+ d.severity AS severity,
27569
+ d.category AS category,
27530
27570
  e.event_type || '${TUPLE_SEP}' ||
27531
27571
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
27532
27572
  coalesce(latest.status, '') AS status_tuple,
@@ -27541,7 +27581,7 @@ var SqliteFindingsRepository = class {
27541
27581
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27542
27582
  ON latest.finding_key = f.finding_key
27543
27583
  ${scope.predicate}
27544
- GROUP BY d.rule_id, status_tuple
27584
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
27545
27585
  )
27546
27586
  GROUP BY rule_id`
27547
27587
  ).all(scope.params);
@@ -27550,6 +27590,8 @@ var SqliteFindingsRepository = class {
27550
27590
  r.rule_id,
27551
27591
  {
27552
27592
  instanceCount: r.instance_count,
27593
+ severity: r.severity,
27594
+ category: r.category,
27553
27595
  sourceTools: splitConcat(r.source_tools),
27554
27596
  actionsTaken: splitConcat(r.actions_taken),
27555
27597
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -27566,7 +27608,7 @@ var SqliteFindingsRepository = class {
27566
27608
  latestDetectedAt: epochMillisToIso(r.latest_at),
27567
27609
  // Free text only — joined and substring-matched, so group_concat's
27568
27610
  // commas need no unpicking (a repo/path containing one still matches).
27569
- // Left undefined (not '') when unfetched, so buildFindingGroups can
27611
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
27570
27612
  // tell "no q this request" from "a group with no repo/file at all"
27571
27613
  // and skip priming a haystack nothing will read.
27572
27614
  ...withSearchText ? {
@@ -27718,6 +27760,12 @@ var SqliteHistorySyncRepository = class {
27718
27760
  this.markOwedStmt = db.prepare(
27719
27761
  `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27720
27762
  );
27763
+ this.markCaptureBacklogOwedStmt = db.prepare(
27764
+ `UPDATE audit_events SET outbox_owed = 1
27765
+ WHERE synced_at IS NULL
27766
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27767
+ AND started_at < :before`
27768
+ );
27721
27769
  this.stampStmt = db.prepare(
27722
27770
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27723
27771
  );
@@ -27766,7 +27814,9 @@ var SqliteHistorySyncRepository = class {
27766
27814
  );
27767
27815
  this.disownCapturesStmt = db.prepare(
27768
27816
  `UPDATE audit_events SET outbox_owed = NULL
27769
- WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27817
+ WHERE outbox_owed IS NOT NULL
27818
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27819
+ AND started_at < :attachedAt`
27770
27820
  );
27771
27821
  this.rearmStmt = db.prepare(
27772
27822
  `UPDATE audit_events SET synced_at = NULL
@@ -27842,6 +27892,7 @@ var SqliteHistorySyncRepository = class {
27842
27892
  freezeBoundaryStmt;
27843
27893
  captureRowsStmt;
27844
27894
  markOwedStmt;
27895
+ markCaptureBacklogOwedStmt;
27845
27896
  captureSkipCountStmt;
27846
27897
  disownCapturesStmt;
27847
27898
  partitionStmt;
@@ -27905,6 +27956,23 @@ var SqliteHistorySyncRepository = class {
27905
27956
  markCaptureOwed(id) {
27906
27957
  this.markOwedStmt.run({ id });
27907
27958
  }
27959
+ /**
27960
+ * Mark every capture already on disk as owed, as of `before`.
27961
+ *
27962
+ * The consent-time backfill, called once from `aka attach` when a human
27963
+ * grants existing-history consent — never from an ongoing drain pass, and
27964
+ * never inferred from a boundary that could later move. `before` is the
27965
+ * caller's own "now" at the moment consent was granted, so what this marks
27966
+ * is exactly the backlog the consent prompt already counted, not whatever a
27967
+ * later re-attach or key rotation might widen it to.
27968
+ *
27969
+ * Returns how many rows matched, for the caller to log or test against. Not a
27970
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
27971
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
27972
+ */
27973
+ markCaptureBacklogOwed(before) {
27974
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
27975
+ }
27908
27976
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27909
27977
  markSynced(ids, atMs) {
27910
27978
  this.stampAll(ids, atMs);
@@ -28021,15 +28089,42 @@ var SqliteHistorySyncRepository = class {
28021
28089
  *
28022
28090
  * Delivery is a fact about ONE recipient: rows sent to the deployment a
28023
28091
  * machine has just left are undelivered as far as the new one is concerned.
28024
- * All three in one transaction, so a crash between them cannot leave stamps
28025
- * attributed to the wrong deployment, or a boundary that belongs to another.
28092
+ * All four in one transaction, so a crash between them cannot leave stamps
28093
+ * attributed to the wrong deployment, a boundary that belongs to another, or
28094
+ * a disown with no re-mark to follow it.
28026
28095
  *
28027
28096
  * The boundary is written HERE and only here, which is what freezes it: a
28028
28097
  * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
28029
28098
  * unchanged, so this never runs and the backlog does not widen back over rows
28030
28099
  * the live path has since delivered.
28100
+ *
28101
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
28102
+ * granted existing-history consent for the deployment this call is arming —
28103
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
28104
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
28105
+ * apart. Passed only when that grant is valid, since this method has no way
28106
+ * to check consent itself and must not mark a row owed for a machine that
28107
+ * never agreed to it. Applied AFTER the disown above, in the SAME
28108
+ * transaction: what the disown clears is every marker below `backlogBefore`,
28109
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
28110
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
28111
+ * on the cleared side of that bound — and the re-mark in the same
28112
+ * transaction is what puts those rows back. A crash between the two cannot
28113
+ * strand the ledger disowned with nothing re-marked — the transaction either
28114
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
28115
+ * committed re-enters this method on the very next pass. Omit it (the
28116
+ * structural-only tests do) to exercise the disown in isolation.
28117
+ *
28118
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
28119
+ * touching a marker the NEW deployment's OWN live path has already set: B's
28120
+ * live path can mark a capture owed from the moment `aka attach` writes the
28121
+ * descriptor, before the drain's first pass ever reaches this method, and
28122
+ * such a row sits at or after the bound rather than below it. What keeps the
28123
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
28124
+ * bound — disown runs first, re-mark second, both inside the one
28125
+ * transaction above.
28031
28126
  */
28032
- rearmFor(fingerprint, backlogBefore) {
28127
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
28033
28128
  this.ensureRowStmt.run();
28034
28129
  withTransaction(
28035
28130
  this.db,
@@ -28037,7 +28132,10 @@ var SqliteHistorySyncRepository = class {
28037
28132
  const previous = getRow(this.fingerprintStmt)?.fingerprint;
28038
28133
  this.rearmStmt.run();
28039
28134
  if (previous !== null && previous !== void 0 && previous !== fingerprint) {
28040
- this.disownCapturesStmt.run();
28135
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
28136
+ }
28137
+ if (backfillCapturesBefore !== void 0) {
28138
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
28041
28139
  }
28042
28140
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
28043
28141
  },
@@ -28317,7 +28415,8 @@ function managedSettingsPaths(platform2 = process.platform) {
28317
28415
  }
28318
28416
  return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28319
28417
  }
28320
- function readManagedSettings(paths = managedSettingsPaths()) {
28418
+ var testOnlyManagedPaths = null;
28419
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
28321
28420
  for (const path of paths) {
28322
28421
  let text;
28323
28422
  try {
@@ -30797,7 +30896,7 @@ function toUtcDateString(ms) {
30797
30896
  return new Date(ms).toISOString().slice(0, 10);
30798
30897
  }
30799
30898
  function isTimeseriesSeverity(s) {
30800
- return s === "critical" || s === "high" || s === "medium";
30899
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
30801
30900
  }
30802
30901
  var SqliteSecurityRepository = class {
30803
30902
  constructor(db, now = () => Date.now()) {
@@ -30926,12 +31025,16 @@ var SqliteSecurityRepository = class {
30926
31025
  const now = this.now();
30927
31026
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
30928
31027
  const rows = this.findingsInRange(windowStart, now);
30929
- const points = Array.from({ length: numBuckets }, (_, i) => ({
30930
- timestamp: toUtcDateString(windowStart + i * bucketMs),
30931
- critical: 0,
30932
- high: 0,
30933
- medium: 0
30934
- }));
31028
+ const points = Array.from(
31029
+ { length: numBuckets },
31030
+ (_, i) => ({
31031
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
31032
+ critical: 0,
31033
+ high: 0,
31034
+ medium: 0,
31035
+ low: 0
31036
+ })
31037
+ );
30935
31038
  for (const r of rows) {
30936
31039
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
30937
31040
  const bucket = points[idx];
@@ -31149,6 +31252,7 @@ var SqliteSecurityRepository = class {
31149
31252
  `SELECT f.finding_key AS finding_key,
31150
31253
  d.rule_id AS rule_id,
31151
31254
  d.severity AS severity,
31255
+ e.repo AS repo,
31152
31256
  e.file_path AS path,
31153
31257
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
31154
31258
  latest.resolved_at AS latest_resolved_at
@@ -31168,6 +31272,7 @@ var SqliteSecurityRepository = class {
31168
31272
  const items = rows.map((r) => ({
31169
31273
  findingKey: r.finding_key,
31170
31274
  ruleId: r.rule_id,
31275
+ repo: r.repo ?? "",
31171
31276
  severity: r.severity,
31172
31277
  path: r.path ?? "",
31173
31278
  resolvedAt: new Date(r.latest_resolved_at).toISOString(),
@@ -31177,13 +31282,66 @@ var SqliteSecurityRepository = class {
31177
31282
  }));
31178
31283
  return Promise.resolve({ items });
31179
31284
  }
31285
+ /**
31286
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
31287
+ *
31288
+ * Scoped by status rather than by time, because the card this feeds is a to-do
31289
+ * list: a secret committed three weeks ago and never rotated is still the most
31290
+ * important thing to fix, and any window hides it. It carried a "newest N
31291
+ * findings" cap and then a range; the first meant a different span on every
31292
+ * machine, and the second reported "no recommendations" over live exposure.
31293
+ *
31294
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
31295
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
31296
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
31297
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
31298
+ * The two answer different questions and only this one has to match a link.
31299
+ *
31300
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
31301
+ * whole-store scope costs a grouped scan rather than a row per finding.
31302
+ */
31303
+ recommendationInputs() {
31304
+ const rows = allRows(
31305
+ this.db.prepare(
31306
+ `SELECT d.rule_id AS rule_id,
31307
+ d.category AS category,
31308
+ d.severity AS severity,
31309
+ COUNT(*) AS count
31310
+ FROM inspection_findings f
31311
+ JOIN audit_events e ON e.id = f.audit_event_id
31312
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31313
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31314
+ ON latest.finding_key = f.finding_key
31315
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31316
+ AND e.event_type = 'code_change'
31317
+ AND (
31318
+ f.finding_key IS NULL
31319
+ OR latest.status IS NULL
31320
+ OR latest.status NOT IN ('resolved', 'dismissed')
31321
+ )
31322
+ GROUP BY d.rule_id, d.category, d.severity`
31323
+ )
31324
+ );
31325
+ return Promise.resolve(
31326
+ rows.map((r) => ({
31327
+ ruleId: r.rule_id,
31328
+ category: r.category,
31329
+ severity: r.severity,
31330
+ count: r.count
31331
+ }))
31332
+ );
31333
+ }
31180
31334
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
31181
31335
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
31182
31336
  // numeric and the JS aggregations bucket/split on ms directly.
31183
31337
  findingsInRange(fromMs, toMs) {
31184
31338
  const rows = allRows(
31185
31339
  this.db.prepare(
31186
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
31340
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
31341
+ // joined for `severity`, so they are two more columns off a row this read
31342
+ // already fetches. They feed the recommended-actions rollup.
31343
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31344
+ d.rule_id AS rule_id, d.category AS category
31187
31345
  FROM inspection_findings f
31188
31346
  JOIN audit_events e ON e.id = f.audit_event_id
31189
31347
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -31196,7 +31354,9 @@ var SqliteSecurityRepository = class {
31196
31354
  return rows.map((r) => ({
31197
31355
  occurredAt: r.occurred_at,
31198
31356
  severity: r.severity,
31199
- actionTaken: r.action_taken
31357
+ actionTaken: r.action_taken,
31358
+ ruleId: r.rule_id,
31359
+ category: r.category
31200
31360
  }));
31201
31361
  }
31202
31362
  };
@@ -32366,6 +32526,43 @@ function openLocalDatabase(dir) {
32366
32526
  };
32367
32527
  }
32368
32528
 
32529
+ // ../../packages/persistence/src/egress-wire.ts
32530
+ import { createHash as createHash3 } from "crypto";
32531
+ function hashProjectKey(projectKey) {
32532
+ return createHash3("sha256").update(projectKey, "utf8").digest("hex");
32533
+ }
32534
+ function toIngestHit(hit) {
32535
+ return {
32536
+ host: hit.host,
32537
+ kind: hit.kind,
32538
+ name: hit.name,
32539
+ category: hit.category,
32540
+ trust: hit.trust,
32541
+ network: hit.network,
32542
+ method: hit.method,
32543
+ transport: hit.transport,
32544
+ url: hit.url,
32545
+ template: hit.template,
32546
+ dataClass: hit.dataClass,
32547
+ site: {
32548
+ file: hit.site.file,
32549
+ line: hit.site.line,
32550
+ dynamic: hit.site.dynamic,
32551
+ vendored: hit.site.vendored
32552
+ }
32553
+ };
32554
+ }
32555
+ function toEgressIngestRequest(input2) {
32556
+ const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
32557
+ const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
32558
+ return {
32559
+ projectKey: hashProjectKey(input2.projectKey),
32560
+ project: input2.project,
32561
+ reconcile,
32562
+ hits: hits.map(toIngestHit)
32563
+ };
32564
+ }
32565
+
32369
32566
  // ../../packages/persistence/src/exception-policy.ts
32370
32567
  var UserGrantPolicyProvider = class {
32371
32568
  #exceptions;
@@ -32387,13 +32584,13 @@ var UserGrantPolicyProvider = class {
32387
32584
  };
32388
32585
 
32389
32586
  // ../../packages/persistence/src/finding-key.ts
32390
- import { createHash as createHash3 } from "crypto";
32587
+ import { createHash as createHash4 } from "crypto";
32391
32588
  function normalizeFilePath(filePath) {
32392
32589
  return filePath.replaceAll("\\", "/");
32393
32590
  }
32394
32591
  function computeFindingKey(input2) {
32395
32592
  const normalizedPath = normalizeFilePath(input2.filePath);
32396
- return createHash3("sha256").update(`${input2.ruleId}\0${normalizedPath}\0${input2.valueFingerprint}`).digest("hex");
32593
+ return createHash4("sha256").update(`${input2.ruleId}\0${normalizedPath}\0${input2.valueFingerprint}`).digest("hex");
32397
32594
  }
32398
32595
 
32399
32596
  // ../../packages/persistence/src/fingerprint.ts
@@ -32519,14 +32716,18 @@ function fingerprintValue(key, raw) {
32519
32716
  return createHmac("sha256", key.material).update(raw, "utf8").digest("hex");
32520
32717
  }
32521
32718
 
32522
- // ../../packages/persistence/src/history-preview.ts
32719
+ // ../../packages/persistence/src/history-backfill.ts
32523
32720
  import { existsSync as existsSync4 } from "fs";
32524
32721
  import { join as join9 } from "path";
32722
+
32723
+ // ../../packages/persistence/src/history-preview.ts
32724
+ import { existsSync as existsSync5 } from "fs";
32725
+ import { join as join10 } from "path";
32525
32726
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32526
32727
 
32527
32728
  // ../../packages/persistence/src/store-symlinks.ts
32528
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32529
- import { dirname as dirname3, join as join10, resolve } from "path";
32729
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32730
+ import { dirname as dirname3, join as join11, resolve } from "path";
32530
32731
 
32531
32732
  // ../../packages/persistence/src/vault/crypto.ts
32532
32733
  import {
@@ -32640,7 +32841,7 @@ function formatPointer(category, keyVersion, pointerId, tag) {
32640
32841
  import { execFileSync } from "child_process";
32641
32842
  import { randomBytes as randomBytes2 } from "crypto";
32642
32843
  import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32643
- import { join as join11 } from "path";
32844
+ import { join as join12 } from "path";
32644
32845
  var VAULT_OCCUPANT_REASON = {
32645
32846
  symlink: "the path is a symlink; remove it so a keyring can be created",
32646
32847
  gone: "the path was occupied but holds no keyring (removed while it was being created)",
@@ -32739,7 +32940,7 @@ function claimRotationLock(lock, owner) {
32739
32940
  throw asError(err);
32740
32941
  }
32741
32942
  try {
32742
- writeFileSync3(join11(lock, LOCK_OWNER_FILE), `${owner}
32943
+ writeFileSync3(join12(lock, LOCK_OWNER_FILE), `${owner}
32743
32944
  `, { mode: DATA_FILE_MODE });
32744
32945
  return true;
32745
32946
  } catch (err) {
@@ -32748,7 +32949,7 @@ function claimRotationLock(lock, owner) {
32748
32949
  }
32749
32950
  }
32750
32951
  function acquireRotationLock(keysDir2) {
32751
- const lock = join11(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32952
+ const lock = join12(keysDir2, `${VAULT_KEY_FILENAME}.lock`);
32752
32953
  const owner = randomBytes2(16).toString("hex");
32753
32954
  if (claimRotationLock(lock, owner)) return { lock, owner };
32754
32955
  let held;
@@ -32775,7 +32976,7 @@ function acquireRotationLock(keysDir2) {
32775
32976
  }
32776
32977
  function releaseRotationLock(lease) {
32777
32978
  try {
32778
- if (readFileSync7(join11(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32979
+ if (readFileSync7(join12(lease.lock, LOCK_OWNER_FILE), "utf8").trim() !== lease.owner) return;
32779
32980
  } catch {
32780
32981
  return;
32781
32982
  }
@@ -32796,7 +32997,7 @@ var FileKeyProvider = class {
32796
32997
  this.#keysDir = keysDir2;
32797
32998
  }
32798
32999
  get filePath() {
32799
- return join11(this.#keysDir, VAULT_KEY_FILENAME);
33000
+ return join12(this.#keysDir, VAULT_KEY_FILENAME);
32800
33001
  }
32801
33002
  loadOrCreate() {
32802
33003
  return asAsync(() => {
@@ -33461,55 +33662,19 @@ var SecretVault = class {
33461
33662
  };
33462
33663
 
33463
33664
  // ../../packages/persistence/src/warn-era-cap.ts
33464
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
33465
- import { join as join12 } from "path";
33665
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
33666
+ import { join as join13 } from "path";
33466
33667
  var MARKER = "warn-era-capped";
33467
33668
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
33468
33669
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
33469
- const marker = join12(dataDir2, MARKER);
33470
- if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
33670
+ const marker = join13(dataDir2, MARKER);
33671
+ if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
33471
33672
  const capped = db.policies.capCategoryActions();
33472
33673
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
33473
33674
  `, { mode: DATA_FILE_MODE });
33474
33675
  return { capped };
33475
33676
  }
33476
33677
 
33477
- // ../../packages/plugin-runtime/src/attached/egress-wire.ts
33478
- function hashProjectKey(projectKey) {
33479
- return createHash4("sha256").update(projectKey, "utf8").digest("hex");
33480
- }
33481
- function toIngestHit(hit) {
33482
- return {
33483
- host: hit.host,
33484
- kind: hit.kind,
33485
- name: hit.name,
33486
- category: hit.category,
33487
- trust: hit.trust,
33488
- network: hit.network,
33489
- method: hit.method,
33490
- transport: hit.transport,
33491
- url: hit.url,
33492
- template: hit.template,
33493
- dataClass: hit.dataClass,
33494
- site: {
33495
- file: hit.site.file,
33496
- line: hit.site.line,
33497
- dynamic: hit.site.dynamic,
33498
- vendored: hit.site.vendored
33499
- }
33500
- };
33501
- }
33502
- function toEgressIngestRequest(input2) {
33503
- const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
33504
- const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
33505
- return {
33506
- projectKey: hashProjectKey(input2.projectKey),
33507
- project: input2.project,
33508
- reconcile,
33509
- hits: hits.map(toIngestHit)
33510
- };
33511
- }
33512
-
33513
33678
  // ../../packages/remote/src/http.ts
33514
33679
  import { request as httpRequest } from "http";
33515
33680
  import { request as httpsRequest } from "https";
@@ -33789,6 +33954,7 @@ function createRemoteClient(options) {
33789
33954
  url: url2(ROUTES.shares),
33790
33955
  body: JSON.stringify(validated.data)
33791
33956
  });
33957
+ if (response.status === 404) throw new RemoteRouteAbsent(ROUTES.shares);
33792
33958
  okBody(response);
33793
33959
  },
33794
33960
  async pollCommand() {
@@ -33811,19 +33977,51 @@ function createRemoteClient(options) {
33811
33977
  };
33812
33978
  }
33813
33979
 
33814
- // ../../packages/plugin-runtime/src/attached/failure.ts
33980
+ // ../../packages/remote/src/failure-kind.ts
33815
33981
  function statusOf(err) {
33816
33982
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
33817
33983
  const { status } = err;
33818
33984
  if (typeof status !== "number" || !Number.isInteger(status)) return null;
33819
33985
  return status >= 100 && status <= 599 ? status : null;
33820
33986
  }
33821
- function classifyFailure(err) {
33822
- switch (statusOf(err)) {
33987
+ function nameOf(err) {
33988
+ if (typeof err !== "object" || err === null || !("name" in err)) return null;
33989
+ return typeof err.name === "string" ? err.name : null;
33990
+ }
33991
+ function classifyRemoteFailure(err) {
33992
+ switch (nameOf(err)) {
33993
+ case "RemoteRouteAbsent":
33994
+ return "route-absent";
33995
+ case "RemoteRequestInvalid":
33996
+ return "invalid-request";
33997
+ case "RemoteResponseInvalid":
33998
+ return "rejected";
33999
+ default:
34000
+ break;
34001
+ }
34002
+ const status = statusOf(err);
34003
+ if (status === null) return "unreachable";
34004
+ switch (status) {
33823
34005
  case 401:
33824
34006
  return "unauthorized";
33825
34007
  case 403:
33826
34008
  return "forbidden";
34009
+ case 429:
34010
+ return "unreachable";
34011
+ case 404:
34012
+ return "unreachable";
34013
+ default:
34014
+ return status >= 400 && status <= 499 ? "rejected" : "unreachable";
34015
+ }
34016
+ }
34017
+
34018
+ // ../../packages/plugin-runtime/src/attached/failure.ts
34019
+ function classifyFailure(err) {
34020
+ switch (classifyRemoteFailure(err)) {
34021
+ case "unauthorized":
34022
+ return "unauthorized";
34023
+ case "forbidden":
34024
+ return "forbidden";
33827
34025
  default:
33828
34026
  return "unreachable";
33829
34027
  }
@@ -33846,10 +34044,10 @@ function withTimeout(promise2, ms) {
33846
34044
 
33847
34045
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
33848
34046
  import { readFileSync as readFileSync8 } from "fs";
33849
- import { join as join13 } from "path";
34047
+ import { join as join14 } from "path";
33850
34048
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
33851
34049
  function forwardDropsPath(dataDir2) {
33852
- return join13(dataDir2, FORWARD_DROPS_FILENAME);
34050
+ return join14(dataDir2, FORWARD_DROPS_FILENAME);
33853
34051
  }
33854
34052
  function recordForwardDrops(dataDir2, count, nowMs) {
33855
34053
  if (count <= 0) return;
@@ -33885,13 +34083,13 @@ function readForwardDrops(dataDir2) {
33885
34083
 
33886
34084
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
33887
34085
  import { randomUUID as randomUUID15 } from "crypto";
33888
- import { readFileSync as readFileSync14 } from "fs";
34086
+ import { readFileSync as readFileSync15 } from "fs";
33889
34087
  import { readFile, rename, writeFile } from "fs/promises";
33890
- import { join as join22 } from "path";
34088
+ import { join as join24 } from "path";
33891
34089
 
33892
34090
  // ../../packages/plugin-sdk/src/config.ts
33893
- import { existsSync as existsSync7 } from "fs";
33894
- import { join as join14 } from "path";
34091
+ import { existsSync as existsSync8 } from "fs";
34092
+ import { join as join15 } from "path";
33895
34093
 
33896
34094
  // ../../packages/plugin-sdk/src/provider-env.ts
33897
34095
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -33955,8 +34153,8 @@ function providerFromModelId(modelId) {
33955
34153
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
33956
34154
  try {
33957
34155
  ensureLayoutDirSync(base);
33958
- const settingsFile = join14(settingsDir(base), "settings.json");
33959
- if (existsSync7(settingsFile)) tightenFile(settingsFile);
34156
+ const settingsFile = join15(settingsDir(base), "settings.json");
34157
+ if (existsSync8(settingsFile)) tightenFile(settingsFile);
33960
34158
  } catch {
33961
34159
  }
33962
34160
  migrateLegacyLayout(base);
@@ -33981,7 +34179,7 @@ function resolveProviderSafe(resolveProviderFn) {
33981
34179
  // ../../packages/plugin-sdk/src/config-inventory.ts
33982
34180
  import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
33983
34181
  import { homedir as homedir2 } from "os";
33984
- import { basename as basename3, join as join16 } from "path";
34182
+ import { basename as basename3, join as join17 } from "path";
33985
34183
 
33986
34184
  // ../../packages/detections/src/egress/registry.ts
33987
34185
  var EXTRACTOR_VERSION = "1";
@@ -37112,8 +37310,8 @@ function scanText(text, ruleVersions) {
37112
37310
  }
37113
37311
 
37114
37312
  // ../../packages/plugin-sdk/src/repo.ts
37115
- import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
37116
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
37313
+ import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
37314
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join16, sep as sep2 } from "path";
37117
37315
  function resolveRepoIdentity(cwd) {
37118
37316
  try {
37119
37317
  const root = findGitRoot(cwd);
@@ -37146,32 +37344,32 @@ function resolveRepoNwo(cwd) {
37146
37344
  function findGitRoot(start) {
37147
37345
  let dir = start;
37148
37346
  for (; ; ) {
37149
- if (existsSync8(join15(dir, ".git"))) return dir;
37347
+ if (existsSync9(join16(dir, ".git"))) return dir;
37150
37348
  const parent = dirname4(dir);
37151
37349
  if (parent === dir) return void 0;
37152
37350
  dir = parent;
37153
37351
  }
37154
37352
  }
37155
37353
  function resolveGitContext(root) {
37156
- const dotGit = join15(root, ".git");
37354
+ const dotGit = join16(root, ".git");
37157
37355
  try {
37158
37356
  if (statSync6(dotGit).isDirectory()) {
37159
- return { configPath: join15(dotGit, "config"), headRoot: root };
37357
+ return { configPath: join16(dotGit, "config"), headRoot: root };
37160
37358
  }
37161
37359
  } catch {
37162
37360
  return void 0;
37163
37361
  }
37164
37362
  const target = /^gitdir:\s*(.+?)\s*$/m.exec(safeRead(dotGit) ?? "")?.[1];
37165
37363
  if (!target) return void 0;
37166
- const gitdir = isAbsolute(target) ? target : join15(root, target);
37167
- if (existsSync8(join15(gitdir, "config"))) {
37168
- return { configPath: join15(gitdir, "config"), headRoot: root };
37364
+ const gitdir = isAbsolute(target) ? target : join16(root, target);
37365
+ if (existsSync9(join16(gitdir, "config"))) {
37366
+ return { configPath: join16(gitdir, "config"), headRoot: root };
37169
37367
  }
37170
- const commonRaw = safeRead(join15(gitdir, "commondir"))?.trim();
37368
+ const commonRaw = safeRead(join16(gitdir, "commondir"))?.trim();
37171
37369
  if (!commonRaw) return void 0;
37172
- const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join15(gitdir, commonRaw);
37370
+ const commonGitDir = isAbsolute(commonRaw) ? commonRaw : join16(gitdir, commonRaw);
37173
37371
  const headRoot = basename2(commonGitDir) === ".git" ? dirname4(commonGitDir) : root;
37174
- return { configPath: join15(commonGitDir, "config"), headRoot };
37372
+ return { configPath: join16(commonGitDir, "config"), headRoot };
37175
37373
  }
37176
37374
  function safeRead(path) {
37177
37375
  try {
@@ -37254,7 +37452,7 @@ function buildIngestEvent(input2) {
37254
37452
  }
37255
37453
 
37256
37454
  // ../../packages/plugin-sdk/src/isolated-scan.ts
37257
- import { existsSync as existsSync9 } from "fs";
37455
+ import { existsSync as existsSync10 } from "fs";
37258
37456
  import { fileURLToPath } from "url";
37259
37457
  import { Worker } from "worker_threads";
37260
37458
  var ISOLATED_SCAN_BUDGET_MS = 2e3;
@@ -37268,7 +37466,7 @@ function resolveWorkerUrl() {
37268
37466
  for (const name of ["scan-worker.js", "scan-worker.ts"]) {
37269
37467
  const candidate = new URL(name, import.meta.url);
37270
37468
  try {
37271
- if (existsSync9(fileURLToPath(candidate))) {
37469
+ if (existsSync10(fileURLToPath(candidate))) {
37272
37470
  resolvedWorkerUrl = candidate;
37273
37471
  return candidate;
37274
37472
  }
@@ -37731,10 +37929,45 @@ function createGuardedScanner(partition, gateway, opts) {
37731
37929
  };
37732
37930
  }
37733
37931
 
37932
+ // ../../packages/plugin-sdk/src/host-floor.ts
37933
+ import { readFileSync as readFileSync12 } from "fs";
37934
+ import { join as join19 } from "path";
37935
+
37936
+ // ../../packages/plugin-sdk/src/model-governance.ts
37937
+ import {
37938
+ closeSync as closeSync2,
37939
+ fstatSync,
37940
+ mkdirSync as mkdirSync2,
37941
+ openSync as openSync2,
37942
+ readFileSync as readFileSync11,
37943
+ readSync,
37944
+ writeFileSync as writeFileSync5
37945
+ } from "fs";
37946
+ import { join as join18 } from "path";
37947
+ var TAIL_BYTES = 256 * 1024;
37948
+
37949
+ // ../../packages/plugin-sdk/src/host-floor.ts
37950
+ var HOST_FEATURE = {
37951
+ ModelSwitch: "model-switch",
37952
+ VaultPointerDisplay: "vault-pointer-display"
37953
+ };
37954
+ var HOST_FLOORS = {
37955
+ [HOST_FEATURE.ModelSwitch]: {
37956
+ label: "model-switch protection",
37957
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
37958
+ since: "2.1.251"
37959
+ },
37960
+ [HOST_FEATURE.VaultPointerDisplay]: {
37961
+ label: "vault pointer display",
37962
+ hookEvents: ["MessageDisplay"],
37963
+ since: "2.1.152"
37964
+ }
37965
+ };
37966
+
37734
37967
  // ../../packages/plugin-sdk/src/ignore-layers.ts
37735
37968
  var import_ignore = __toESM(require_ignore(), 1);
37736
- import { readFileSync as readFileSync11 } from "fs";
37737
- import { join as join17 } from "path";
37969
+ import { readFileSync as readFileSync13 } from "fs";
37970
+ import { join as join20 } from "path";
37738
37971
 
37739
37972
  // ../../packages/plugin-sdk/src/inventory-resolver.ts
37740
37973
  import { arch, hostname as hostname4, platform, release } from "os";
@@ -37765,22 +37998,9 @@ function resolveInventoryContext(input2) {
37765
37998
  return ctx;
37766
37999
  }
37767
38000
 
37768
- // ../../packages/plugin-sdk/src/model-governance.ts
37769
- import {
37770
- closeSync as closeSync2,
37771
- fstatSync,
37772
- mkdirSync as mkdirSync2,
37773
- openSync as openSync2,
37774
- readFileSync as readFileSync12,
37775
- readSync,
37776
- writeFileSync as writeFileSync5
37777
- } from "fs";
37778
- import { join as join18 } from "path";
37779
- var TAIL_BYTES = 256 * 1024;
37780
-
37781
38001
  // ../../packages/plugin-sdk/src/nudge.ts
37782
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
37783
- import { join as join19 } from "path";
38002
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
38003
+ import { join as join21 } from "path";
37784
38004
 
37785
38005
  // ../../packages/plugin-sdk/src/paths.ts
37786
38006
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
@@ -37822,8 +38042,8 @@ function createPolicyResolver(bundle) {
37822
38042
  }
37823
38043
 
37824
38044
  // ../../packages/plugin-sdk/src/project-files.ts
37825
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
37826
- import { basename as basename5, join as join20 } from "path";
38045
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
38046
+ import { basename as basename5, join as join22 } from "path";
37827
38047
 
37828
38048
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
37829
38049
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -37894,6 +38114,14 @@ function safeMaskedMatch(rawMatch) {
37894
38114
  // ../../packages/plugin-sdk/src/runtime.ts
37895
38115
  import { randomUUID as randomUUID14 } from "crypto";
37896
38116
  var ENFORCEMENT_CEILING_ENABLED = false;
38117
+ function applyEnforcementCeiling(action, policyMode, enabled) {
38118
+ if (!enabled || policyMode !== "warn") return action;
38119
+ return action === "block" || action === "redact" ? "warn" : action;
38120
+ }
38121
+ function resolveEnforcedAction(action, opts) {
38122
+ const degraded = !opts.rewritable && action === "redact" ? builtinPolicyToAction(opts.redactFallback) : action;
38123
+ return applyEnforcementCeiling(degraded, opts.policyMode, opts.ceilingEnabled);
38124
+ }
37897
38125
  function startTiming() {
37898
38126
  try {
37899
38127
  return performance.now();
@@ -38002,12 +38230,12 @@ function createPluginRuntime(gateway, settings, opts) {
38002
38230
  }
38003
38231
  function actionForFinding(finding, excepted, rewritable = true) {
38004
38232
  if (excepted?.has(finding)) return "allow";
38005
- const action = resolveAction(finding.ruleId, finding.category);
38006
- if (!rewritable && action === "redact") return builtinPolicyToAction(redactFallback);
38007
- if (ENFORCEMENT_CEILING_ENABLED && policyMode === "warn" && (action === "block" || action === "redact")) {
38008
- return "warn";
38009
- }
38010
- return action;
38233
+ return resolveEnforcedAction(resolveAction(finding.ruleId, finding.category), {
38234
+ policyMode,
38235
+ redactFallback,
38236
+ rewritable,
38237
+ ceilingEnabled: ENFORCEMENT_CEILING_ENABLED
38238
+ });
38011
38239
  }
38012
38240
  function decide(findings, text, excepted, rewritable = true) {
38013
38241
  if (findings.length === 0) return { action: "log", text, findings: [] };
@@ -38256,7 +38484,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
38256
38484
 
38257
38485
  // ../../packages/plugin-sdk/src/throttle.ts
38258
38486
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
38259
- import { join as join21 } from "path";
38487
+ import { join as join23 } from "path";
38260
38488
 
38261
38489
  // ../../packages/plugin-sdk/src/tokenize.ts
38262
38490
  function redactedPlaceholder(category) {
@@ -38609,7 +38837,7 @@ function parseBreakerState(raw, nowMs) {
38609
38837
  }
38610
38838
  function createForwardPolicy(deps) {
38611
38839
  const now = deps.now ?? (() => Date.now());
38612
- const file2 = join22(deps.dir, STATE_FILENAME);
38840
+ const file2 = join24(deps.dir, STATE_FILENAME);
38613
38841
  let state = null;
38614
38842
  let loading = null;
38615
38843
  async function readState() {
@@ -39331,8 +39559,8 @@ function toolAuditEvent(input2) {
39331
39559
  }
39332
39560
 
39333
39561
  // ../../packages/plugin-runtime/src/attached/history-state.ts
39334
- import { readFileSync as readFileSync15 } from "fs";
39335
- import { join as join23 } from "path";
39562
+ import { readFileSync as readFileSync16 } from "fs";
39563
+ import { join as join25 } from "path";
39336
39564
 
39337
39565
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
39338
39566
  import { createHash as createHash6 } from "crypto";
@@ -39349,14 +39577,14 @@ import { fileURLToPath as fileURLToPath2 } from "url";
39349
39577
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
39350
39578
 
39351
39579
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
39352
- import { readFileSync as readFileSync16 } from "fs";
39580
+ import { readFileSync as readFileSync17 } from "fs";
39353
39581
  var manifestBuildCache = /* @__PURE__ */ new Map();
39354
39582
  function readManifestBuild(manifestUrl, packageName) {
39355
39583
  const key = manifestUrl.href;
39356
39584
  if (!manifestBuildCache.has(key)) {
39357
39585
  let build;
39358
39586
  try {
39359
- const manifest = JSON.parse(readFileSync16(manifestUrl, "utf8"));
39587
+ const manifest = JSON.parse(readFileSync17(manifestUrl, "utf8"));
39360
39588
  build = typeof manifest.version === "string" && manifest.version.length > 0 ? { package: packageName, version: manifest.version } : void 0;
39361
39589
  } catch {
39362
39590
  build = void 0;
@@ -39383,7 +39611,7 @@ function createPluginBlock(build, policyStore) {
39383
39611
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
39384
39612
  import { randomUUID as randomUUID16 } from "crypto";
39385
39613
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
39386
- import { join as join24 } from "path";
39614
+ import { join as join26 } from "path";
39387
39615
 
39388
39616
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
39389
39617
  import { rename as rename2 } from "fs/promises";
@@ -39407,7 +39635,7 @@ async function publishByRename(tmp, file2, move = rename2) {
39407
39635
 
39408
39636
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
39409
39637
  function createPolicyStore(dir = dataDir()) {
39410
- const file2 = join24(dir, "policy-cache.json");
39638
+ const file2 = join26(dir, "policy-cache.json");
39411
39639
  async function read() {
39412
39640
  try {
39413
39641
  const raw = await readFile2(file2, "utf8");
@@ -39638,11 +39866,11 @@ function readStorePosture(dbPath2) {
39638
39866
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
39639
39867
  import { randomUUID as randomUUID17 } from "crypto";
39640
39868
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
39641
- import { join as join25 } from "path";
39869
+ import { join as join27 } from "path";
39642
39870
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
39643
39871
  function createPostureStore(dir = settingsDir(), legacyDir) {
39644
- const file2 = join25(dir, "posture-state.json");
39645
- const legacyFile = legacyDir === void 0 ? null : join25(legacyDir, "posture-state.json");
39872
+ const file2 = join27(dir, "posture-state.json");
39873
+ const legacyFile = legacyDir === void 0 ? null : join27(legacyDir, "posture-state.json");
39646
39874
  async function persist(state) {
39647
39875
  await ensureDataDir(dir);
39648
39876
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -39710,8 +39938,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
39710
39938
  }
39711
39939
 
39712
39940
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
39713
- import { readFileSync as readFileSync17 } from "fs";
39714
- import { join as join26 } from "path";
39941
+ import { readFileSync as readFileSync18 } from "fs";
39942
+ import { join as join28 } from "path";
39715
39943
 
39716
39944
  // ../../packages/plugin-runtime/src/attached/status.ts
39717
39945
  var REFUSAL_LINES = {
@@ -40183,7 +40411,7 @@ var EXCEPTION_RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
40183
40411
 
40184
40412
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
40185
40413
  import { writeFileSync as writeFileSync8 } from "fs";
40186
- import { join as join27 } from "path";
40414
+ import { join as join29 } from "path";
40187
40415
 
40188
40416
  // ../../packages/setup-wizard/src/triage/merge.ts
40189
40417
  var RANK = Object.fromEntries(
@@ -40191,9 +40419,9 @@ var RANK = Object.fromEntries(
40191
40419
  );
40192
40420
 
40193
40421
  // ../../packages/setup-wizard/src/triage/plan-file.ts
40194
- import { mkdtempSync, readFileSync as readFileSync18, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
40422
+ import { mkdtempSync, readFileSync as readFileSync19, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
40195
40423
  import { tmpdir } from "os";
40196
- import { basename as basename6, dirname as dirname6, join as join28 } from "path";
40424
+ import { basename as basename6, dirname as dirname6, join as join30 } from "path";
40197
40425
  var SuppressionEntrySchema = external_exports.object({
40198
40426
  ruleId: external_exports.string(),
40199
40427
  category: DetectionCategory,
@@ -40238,11 +40466,11 @@ var PersistedPlanSchema = external_exports.object({
40238
40466
  var TRIAGE_STATUSES = ["complete", "complete:no-history", "skipped:no-consent"];
40239
40467
 
40240
40468
  // src/history/transcripts.ts
40241
- import { readdirSync as readdirSync5, readFileSync as readFileSync19 } from "fs";
40469
+ import { readdirSync as readdirSync5, readFileSync as readFileSync20 } from "fs";
40242
40470
  import { homedir as homedir3 } from "os";
40243
- import { join as join29 } from "path";
40471
+ import { join as join31 } from "path";
40244
40472
  function transcriptsDir(home) {
40245
- return join29(home ?? homedir3(), ".claude", "projects");
40473
+ return join31(home ?? homedir3(), ".claude", "projects");
40246
40474
  }
40247
40475
  function isRecord(value) {
40248
40476
  return typeof value === "object" && value !== null;
@@ -40490,7 +40718,7 @@ function* iterateFileContents(dir, excludeSessionId) {
40490
40718
  return;
40491
40719
  }
40492
40720
  for (const project of projects) {
40493
- const projectDir = join29(dir, project);
40721
+ const projectDir = join31(dir, project);
40494
40722
  let files;
40495
40723
  try {
40496
40724
  files = readdirSync5(projectDir).filter((name) => name.endsWith(".jsonl"));
@@ -40500,10 +40728,10 @@ function* iterateFileContents(dir, excludeSessionId) {
40500
40728
  for (const file2 of files) {
40501
40729
  if (excludeSessionId !== void 0 && file2.slice(0, -".jsonl".length) === excludeSessionId)
40502
40730
  continue;
40503
- const filePath = join29(projectDir, file2);
40731
+ const filePath = join31(projectDir, file2);
40504
40732
  let content;
40505
40733
  try {
40506
- content = readFileSync19(filePath, "utf8");
40734
+ content = readFileSync20(filePath, "utf8");
40507
40735
  } catch {
40508
40736
  continue;
40509
40737
  }
@@ -40635,20 +40863,20 @@ async function scanHistory(config2, opts = {}, onHit) {
40635
40863
  }
40636
40864
 
40637
40865
  // src/history/tail-scrub.ts
40638
- import { readFileSync as readFileSync21, renameSync as renameSync6, rmSync as rmSync9, statSync as statSync11, writeFileSync as writeFileSync11 } from "fs";
40866
+ import { readFileSync as readFileSync22, renameSync as renameSync6, rmSync as rmSync9, statSync as statSync11, writeFileSync as writeFileSync11 } from "fs";
40639
40867
 
40640
40868
  // src/remediation/redact.ts
40641
40869
  import {
40642
40870
  lstatSync as lstatSync4,
40643
40871
  readdirSync as readdirSync6,
40644
- readFileSync as readFileSync20,
40872
+ readFileSync as readFileSync21,
40645
40873
  realpathSync as realpathSync4,
40646
40874
  renameSync as renameSync5,
40647
40875
  rmSync as rmSync8,
40648
40876
  statSync as statSync10,
40649
40877
  writeFileSync as writeFileSync10
40650
40878
  } from "fs";
40651
- import { basename as basename7, dirname as dirname7, isAbsolute as isAbsolute2, join as join30, relative, resolve as resolve2 } from "path";
40879
+ import { basename as basename7, dirname as dirname7, isAbsolute as isAbsolute2, join as join32, relative, resolve as resolve2 } from "path";
40652
40880
  function platformRedactionScope(home) {
40653
40881
  return { artifactRoots: [transcriptsDir(home)] };
40654
40882
  }
@@ -40679,7 +40907,7 @@ async function scrubTranscriptTail(filePath, deps) {
40679
40907
  if (realPath === null) return null;
40680
40908
  const statBefore = statSync11(realPath);
40681
40909
  if (statBefore.size > (deps.maxBytes ?? DEFAULT_MAX_SCRUB_BYTES)) return null;
40682
- const content = readFileSync21(realPath, "utf8");
40910
+ const content = readFileSync22(realPath, "utf8");
40683
40911
  const lines = content.split("\n");
40684
40912
  let rewritten = 0;
40685
40913
  for (const [i, line] of lines.entries()) {
@@ -40729,11 +40957,11 @@ import {
40729
40957
  fstatSync as fstatSync2,
40730
40958
  mkdirSync as mkdirSync5,
40731
40959
  openSync as openSync3,
40732
- readFileSync as readFileSync22,
40960
+ readFileSync as readFileSync23,
40733
40961
  readSync as readSync2,
40734
40962
  writeFileSync as writeFileSync12
40735
40963
  } from "fs";
40736
- import { join as join31 } from "path";
40964
+ import { join as join33 } from "path";
40737
40965
 
40738
40966
  // src/history/usage.ts
40739
40967
  var NO_PROJECT_CWD = "/nonexistent/aka-reconciler/no-project";