@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.
@@ -491,9 +491,6 @@ var require_ignore = __commonJS({
491
491
  }
492
492
  });
493
493
 
494
- // ../../packages/plugin-runtime/src/attached/egress-wire.ts
495
- import { createHash as createHash4 } from "crypto";
496
-
497
494
  // ../../packages/persistence/src/attached-derived.ts
498
495
  import { rmSync } from "fs";
499
496
  import { join } from "path";
@@ -20423,6 +20420,20 @@ function epochMillisToIso(ms) {
20423
20420
  return new Date(ms).toISOString();
20424
20421
  }
20425
20422
 
20423
+ // ../../packages/schema/src/security/recommendations.ts
20424
+ function healthScore(summary) {
20425
+ const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
20426
+ const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
20427
+ return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
20428
+ }
20429
+ function findingStatus(summary) {
20430
+ return {
20431
+ score: healthScore(summary),
20432
+ unreviewed: { ...summary.bySeverity },
20433
+ openFindings: summary.findings
20434
+ };
20435
+ }
20436
+
20426
20437
  // ../../packages/schema/src/token/cost-model.ts
20427
20438
  var PROVIDER_PLATFORM = /* @__PURE__ */ new Map([
20428
20439
  ["anthropic", "anthropic"],
@@ -20742,13 +20753,11 @@ var FindingGroup = external_exports.object({
20742
20753
  latestDetectedAt: external_exports.iso.datetime(),
20743
20754
  instances: external_exports.array(FindingInstance),
20744
20755
  // Derived from instances' statuses with open-dominates precedence (see
20745
- // buildFindingGroups). Undefined only when no instance carries a status.
20756
+ // foldGroupStatus). Undefined only when no instance carries a status.
20746
20757
  status: FindingStatus.optional(),
20747
- // The distinct people across the WHOLE group, not just the `instances`
20748
- // preview — from the store's whole-group aggregate when it supplies one,
20749
- // else folded from the rows (see buildFindingGroups). Undefined when no
20750
- // instance carries a user, or when the store supplied whole-group folds
20751
- // without one.
20758
+ // The distinct people across the WHOLE group, not just the instances
20759
+ // carried here. Undefined when no instance carries a user, or when the
20760
+ // store supplied whole-group folds without one.
20752
20761
  users: external_exports.array(FindingUser).optional()
20753
20762
  }).meta({ id: "FindingGroup" });
20754
20763
  var FindingStats = external_exports.object({
@@ -20777,21 +20786,31 @@ var FindingFacets = external_exports.object({
20777
20786
  // counted under no value.
20778
20787
  status: external_exports.array(FindingFacetItem),
20779
20788
  // Host tool (attributes.tool_name). Present only on the instance-level
20780
- // reads, which can filter by it; the grouped read omits the dimension
20789
+ // reads, which can filter by it; the type-level read omits the dimension
20781
20790
  // because a group spans tools.
20782
20791
  tool: external_exports.array(FindingFacetItem).optional()
20783
20792
  }).meta({ id: "FindingFacets" });
20784
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
20785
- var ListGroupedFindingsQuery = external_exports.object({
20793
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20794
+ id: "FindingTypeSummary"
20795
+ });
20796
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
20797
+ var MAX_FINDING_TYPES_LIMIT = 100;
20798
+ var ListFindingTypesQuery = external_exports.object({
20786
20799
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
20787
- // FindingAction.
20800
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
20801
+ // firing version carries, and this list pages types.
20802
+ //
20803
+ // That is NOT a claim the findings of a type share it. A rule can hold several
20804
+ // definition versions at different severities, so a type kept by this filter
20805
+ // can hold findings that individually do not match — see totals.findings on
20806
+ // ListFindingTypesResponse, which counts them all.
20788
20807
  severity: external_exports.array(Severity).optional(),
20789
20808
  subtype: external_exports.array(external_exports.string()).optional(),
20790
20809
  provider: external_exports.array(FindingProvider).optional(),
20791
20810
  action: external_exports.array(FindingAction).optional(),
20792
- // Matches a group's DERIVED status (see FindingGroup.status), not its
20793
- // individual instances' — so a filtered group's Status column always reads
20794
- // one of the requested values.
20811
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
20812
+ // individual findings' — so a filtered row's status always reads one of the
20813
+ // requested values.
20795
20814
  status: external_exports.array(FindingStatus).optional(),
20796
20815
  q: external_exports.string().optional(),
20797
20816
  // Scope to findings whose event carries this session id (the Activity page's
@@ -20801,23 +20820,37 @@ var ListGroupedFindingsQuery = external_exports.object({
20801
20820
  // from a time-scoped page (Activity's range) can carry that scope. Absent
20802
20821
  // means all time — this list has no default window.
20803
20822
  from: external_exports.iso.datetime().optional(),
20804
- // A group or instance id that must appear in the page even when the cursor
20805
- // has already advanced past its sort position. This is what keeps the
20806
- // Findings page's one-shot ?finding= deep link resolving once the list
20807
- // paginates: the target group is appended out of sort order rather than
20808
- // scanning forward for it. Never affects totals, facets or the cursor.
20823
+ // A RULE id that must appear in the page even when the cursor has already
20824
+ // advanced past its sort position. This is what keeps the selected type
20825
+ // visible in the list once it paginates: the target is appended out of sort
20826
+ // order rather than scanned forward for. Never affects totals, facets or the
20827
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
20828
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
20829
+ // and so is not bounded by what any page happens to hold.
20809
20830
  includeId: external_exports.string().optional(),
20810
- groupBy: external_exports.literal("type").optional(),
20811
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
20831
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
20812
20832
  cursor: external_exports.string().optional()
20813
20833
  });
20814
- var ListGroupedFindingsResponse = external_exports.object({
20834
+ var ListFindingTypesResponse = external_exports.object({
20815
20835
  totals: external_exports.object({
20836
+ // Findings belonging to the matching TYPES — not findings that each match
20837
+ // the filters. The filters here select types, so a type that survives
20838
+ // contributes its whole instanceCount.
20839
+ //
20840
+ // `status` is the one exception, narrowed per finding via
20841
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
20842
+ // this can exceed what the instance read reports for the same filters: a
20843
+ // rule whose severity moved between versions is kept on its newest and
20844
+ // still counts its older findings. Narrowing the other three needs
20845
+ // per-dimension counts the aggregate does not carry today.
20816
20846
  findings: external_exports.number().int().nonnegative(),
20817
- groups: external_exports.number().int().nonnegative()
20847
+ // Counts TYPES, which is the unit this read pages. The instance read's
20848
+ // own totals count findings; the two deliberately answer different
20849
+ // questions and are never summed.
20850
+ types: external_exports.number().int().nonnegative()
20818
20851
  }),
20819
20852
  facets: FindingFacets,
20820
- items: external_exports.array(FindingGroup),
20853
+ items: external_exports.array(FindingTypeSummary),
20821
20854
  nextCursor: external_exports.string().nullable(),
20822
20855
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
20823
20856
  // many times that rule fired in the session's persisted transcript. Findings
@@ -20825,7 +20858,7 @@ var ListGroupedFindingsResponse = external_exports.object({
20825
20858
  // every firing, so the two numbers legitimately differ — this map lets a
20826
20859
  // session-scoped view show both.
20827
20860
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
20828
- }).meta({ id: "ListGroupedFindingsResponse" });
20861
+ }).meta({ id: "ListFindingTypesResponse" });
20829
20862
  var ApplyFindingActionRequest = external_exports.object({
20830
20863
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
20831
20864
  // it, so it is excluded from the request contract. The mapping helper
@@ -20855,12 +20888,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
20855
20888
  var MAX_FLAT_FINDINGS_LIMIT = 200;
20856
20889
  var ListFindingInstancesQuery = external_exports.object({
20857
20890
  severity: external_exports.array(Severity).optional(),
20858
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
20891
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
20892
+ // ONE of them is how the master/detail view scopes its right-hand panel.
20859
20893
  subtype: external_exports.array(external_exports.string()).optional(),
20860
20894
  provider: external_exports.array(FindingProvider).optional(),
20861
20895
  action: external_exports.array(FindingAction).optional(),
20862
20896
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
20863
- // the grouped query's group-level fold.
20897
+ // the types query's type-level fold.
20864
20898
  status: external_exports.array(FindingStatus).optional(),
20865
20899
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20866
20900
  // where the free-text `q` can only match the rendered "via Bash" label.
@@ -20877,37 +20911,47 @@ var ListFindingInstancesQuery = external_exports.object({
20877
20911
  });
20878
20912
  var ListFindingInstancesResponse = external_exports.object({
20879
20913
  // Instances matching the filters across the whole scope, not just this
20880
- // page — cursor-independent, like the grouped list's totals.
20914
+ // page — cursor-independent, like the types list's totals.
20881
20915
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
20882
- // Counts in INSTANCES here, where the grouped response counts groups. Each
20916
+ // Counts in INSTANCES here, where the types response counts types. Each
20883
20917
  // dimension still excludes its own filter.
20884
20918
  facets: FindingFacets,
20885
20919
  items: external_exports.array(FindingInstanceDetail),
20886
20920
  nextCursor: external_exports.string().nullable()
20887
20921
  }).meta({ id: "ListFindingInstancesResponse" });
20888
- var FindingLocationFile = external_exports.object({
20889
- // Empty when the instances carried no file path (a prompt or a tool call
20890
- // with no file attribution).
20891
- file: external_exports.string(),
20892
- instanceCount: external_exports.number().int().nonnegative(),
20893
- maxSeverity: Severity,
20894
- latestDetectedAt: external_exports.iso.datetime(),
20895
- // Folded from the instances' derived statuses with the same
20896
- // open-dominates precedence a group uses.
20897
- status: FindingStatus.optional(),
20898
- // Distinct rules seen at this location, capped — the row shows them as
20899
- // chips, and the count is what conveys scale.
20900
- ruleIds: external_exports.array(external_exports.string())
20901
- }).meta({ id: "FindingLocationFile" });
20902
- var FindingLocationRepo = external_exports.object({
20922
+ var FindingLocationSummary = external_exports.object({
20923
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
20924
+ // because a location's identity is two values and a URL param carries one:
20925
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
20926
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
20927
+ // client's page dedupe — never decoded, and never a sort key.
20928
+ id: external_exports.string(),
20903
20929
  /** Empty when the instances carried no repo attribute. */
20904
20930
  repo: external_exports.string(),
20931
+ // Empty when the instances carried no file path (a prompt, or a tool call
20932
+ // with no file attribution). Both halves empty is a real location — usually
20933
+ // the largest one in a store — and is selectable like any other.
20934
+ file: external_exports.string(),
20905
20935
  instanceCount: external_exports.number().int().nonnegative(),
20936
+ // The WORST severity present, not the first row's. It is this list's primary
20937
+ // sort key, so it is also what explains why a row is where it is, and it is
20938
+ // how a reader decides what to open without opening everything.
20906
20939
  maxSeverity: Severity,
20907
20940
  latestDetectedAt: external_exports.iso.datetime(),
20941
+ // Folded from the instances' derived statuses with the same open-dominates
20942
+ // precedence a group uses, so it answers "is anything left to do here" and
20943
+ // not much more: a location holding 1 open among 40 resolved reads like one
20944
+ // holding 40 open. That loss is accepted — the panel beside this list
20945
+ // carries each finding's own status, and instanceCount sits next to the
20946
+ // badge.
20908
20947
  status: FindingStatus.optional(),
20909
- files: external_exports.array(FindingLocationFile)
20910
- }).meta({ id: "FindingLocationRepo" });
20948
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
20949
+ // tally rather than a sample and a row can say how many there are. Bounded
20950
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
20951
+ ruleIds: external_exports.array(external_exports.string())
20952
+ }).meta({ id: "FindingLocationSummary" });
20953
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
20954
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
20911
20955
  var ListFindingLocationsQuery = external_exports.object({
20912
20956
  severity: external_exports.array(Severity).optional(),
20913
20957
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -20920,18 +20964,42 @@ var ListFindingLocationsQuery = external_exports.object({
20920
20964
  q: external_exports.string().optional(),
20921
20965
  sessionId: external_exports.string().optional(),
20922
20966
  from: external_exports.iso.datetime().optional(),
20923
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
20967
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
20968
+ // even when the cursor has already advanced past its sort position — the
20969
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
20970
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
20971
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
20972
+ // into the thousands, a selection sitting off page 0 is the ordinary case
20973
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
20974
+ includeId: external_exports.string().optional(),
20975
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
20976
+ cursor: external_exports.string().optional()
20924
20977
  });
20925
20978
  var ListFindingLocationsResponse = external_exports.object({
20926
20979
  totals: external_exports.object({
20980
+ // Findings matching the filters across the whole scope. Unlike the types
20981
+ // read's same-named field this needs no caveat: the filters here narrow
20982
+ // per finding, so this is the sum of every row's instanceCount.
20927
20983
  findings: external_exports.number().int().nonnegative(),
20928
- repos: external_exports.number().int().nonnegative(),
20929
- files: external_exports.number().int().nonnegative()
20984
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
20985
+ // states. The facets beside it count FINDINGS (see below); a surface
20986
+ // showing both says which is which.
20987
+ locations: external_exports.number().int().nonnegative()
20930
20988
  }),
20931
- /** Sorted by max severity, then most recent. */
20932
- items: external_exports.array(FindingLocationRepo),
20933
- /** Whether `limit` truncated the repo list. */
20934
- hasMore: external_exports.boolean()
20989
+ // Counts in FINDINGS, where the types response counts types, each dimension
20990
+ // still excluding its own filter. Deliberately not locations: counting those
20991
+ // needs a set of location keys per dimension per value — memory tracking the
20992
+ // store times the vocabulary, in a read whose scan promises flat memory —
20993
+ // and the cheap per-location version is not an approximation but WRONG. A
20994
+ // location holding {claudecode, block} and {codex, warn} would survive
20995
+ // provider=claudecode AND action=warn, under which no single finding
20996
+ // matches, so the facet would contradict the instanceCount this whole view
20997
+ // rests on. Findings also keep the toolbar in the same unit as the page
20998
+ // tally and the panel it sits above.
20999
+ facets: FindingFacets,
21000
+ /** Sorted by max severity, then most recent, then (repo, file). */
21001
+ items: external_exports.array(FindingLocationSummary),
21002
+ nextCursor: external_exports.string().nullable()
20935
21003
  }).meta({ id: "ListFindingLocationsResponse" });
20936
21004
 
20937
21005
  // ../../packages/schema/src/zod/meta.ts
@@ -22100,6 +22168,14 @@ var ControlPlaneErrorBody = external_exports.object({
22100
22168
  message: external_exports.string().optional()
22101
22169
  }).optional()
22102
22170
  });
22171
+ var RemoteFailureKind = external_exports.enum([
22172
+ "unauthorized",
22173
+ "forbidden",
22174
+ "route-absent",
22175
+ "invalid-request",
22176
+ "rejected",
22177
+ "unreachable"
22178
+ ]);
22103
22179
  var AttachDeviceRequest = external_exports.object({
22104
22180
  // This machine's own continuity id, so re-attaching ROTATES the credential
22105
22181
  // on one machine record instead of producing a second one. Client-minted
@@ -22793,139 +22869,62 @@ function deriveFindingStatus(row) {
22793
22869
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
22794
22870
  return "open";
22795
22871
  }
22796
- function distinctUsers(instances) {
22797
- const seen = /* @__PURE__ */ new Set();
22798
- const users = [];
22799
- for (const i of instances) {
22800
- if (i.user === void 0 || seen.has(i.user.id)) continue;
22801
- seen.add(i.user.id);
22802
- users.push(i.user);
22803
- }
22804
- return users;
22805
- }
22806
22872
  function sortUsers(users) {
22807
22873
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
22808
22874
  }
22809
- function buildFindingGroups(rows, opts = {}) {
22810
- const overrides = opts.overrides;
22875
+ function buildFindingTypes(aggregates, opts = {}) {
22811
22876
  const packNames = opts.packNames;
22812
- const aggregates = opts.aggregates;
22813
- const byRuleId = /* @__PURE__ */ new Map();
22814
- for (const row of rows) {
22815
- const existing = byRuleId.get(row.ruleId);
22816
- if (existing) existing.push(row);
22817
- else byRuleId.set(row.ruleId, [row]);
22818
- }
22819
- const groups = [];
22820
- for (const [ruleId, ruleRows] of byRuleId) {
22821
- const instances = ruleRows.map((r) => {
22822
- const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
22823
- return {
22824
- id: r.id,
22825
- provider: toApiProvider(r.sourceTool),
22826
- repo: r.repo,
22827
- file: r.file,
22828
- ...r.toolName === void 0 ? {} : { toolName: r.toolName },
22829
- ...r.eventId === void 0 ? {} : { eventId: r.eventId },
22830
- ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
22831
- ...r.user === void 0 ? {} : { user: r.user },
22832
- action: toApiAction(effectiveDbAction),
22833
- detectedAt: r.occurredAt,
22834
- confidence: r.confidence,
22835
- status: r.status
22836
- };
22837
- });
22838
- const agg = aggregates?.get(ruleId);
22839
- const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
22840
- const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
22841
- (max, r) => r.occurredAt > max ? r.occurredAt : max,
22842
- ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
22843
- );
22844
- const seenProviders = /* @__PURE__ */ new Set();
22845
- const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
22846
- if (seenProviders.has(p)) return false;
22847
- seenProviders.add(p);
22848
- return true;
22849
- });
22850
- const actionSet = new Set(
22851
- agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
22852
- );
22877
+ const types = [];
22878
+ for (const [ruleId, agg] of aggregates) {
22879
+ const users = sortUsers(agg.users ?? []);
22880
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
22881
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
22853
22882
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
22854
- const severity = ruleRows[0]?.severity ?? "low";
22855
- const detection = {
22856
- id: ruleId,
22857
- name: packNames?.get(ruleId) ?? null
22858
- };
22859
- const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
22860
- const policy = { id: `category:${apiCategory}`, name: apiCategory };
22861
- const match = {
22862
- maskedValue: ruleRows[0]?.maskedMatch ?? "",
22863
- contextPrefix: ""
22864
- // empty (pending privacy review)
22865
- };
22866
- const status = foldGroupStatus(
22867
- agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
22868
- );
22869
- const group = {
22883
+ const apiCategory = toApiCategory(agg.category ?? "custom");
22884
+ const type = {
22870
22885
  id: ruleId,
22871
22886
  category: apiCategory,
22872
22887
  subtype: ruleId,
22873
22888
  // human label comes with pack metadata later
22874
- severity,
22875
- match,
22876
- detection,
22877
- policy,
22878
- instanceCount: agg?.instanceCount ?? instances.length,
22889
+ severity: agg.severity ?? "low",
22890
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
22891
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
22892
+ instanceCount: agg.instanceCount,
22879
22893
  providers,
22880
22894
  aggregateAction,
22881
- latestDetectedAt,
22882
- instances,
22883
- status,
22895
+ latestDetectedAt: agg.latestDetectedAt,
22896
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
22884
22897
  ...users.length > 0 ? { users } : {}
22885
22898
  };
22886
- if (agg) {
22887
- actionsCache.set(group, [...actionSet]);
22888
- if (agg.searchText !== void 0) {
22889
- haystackCache.set(group, buildHaystack(group, agg.searchText));
22890
- }
22899
+ actionsCache.set(type, [...actionSet]);
22900
+ if (agg.searchText !== void 0) {
22901
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
22891
22902
  }
22892
- groups.push(group);
22903
+ types.push(type);
22893
22904
  }
22894
- return groups;
22905
+ return types;
22895
22906
  }
22896
22907
  var haystackCache = /* @__PURE__ */ new WeakMap();
22897
- function buildHaystack(g, extra) {
22908
+ function buildHaystack(t, extra) {
22898
22909
  return [
22899
- g.subtype,
22900
- g.category,
22901
- g.match.maskedValue,
22902
- g.policy.name,
22903
- g.id,
22904
- ...g.instances.map((i) => i.repo),
22905
- ...g.instances.map((i) => i.file),
22906
- ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
22907
- ...g.instances.map((i) => i.id),
22908
- // The people: the whole group's list when the store folded one, plus the
22909
- // preview's own — the two overlap, and a haystack does not mind.
22910
- ...(g.users ?? []).map((u) => u.name),
22911
- ...g.instances.map((i) => i.user?.name ?? ""),
22910
+ t.subtype,
22911
+ t.category,
22912
+ t.policy.name,
22913
+ t.id,
22914
+ ...(t.users ?? []).map((u) => u.name),
22912
22915
  ...extra === void 0 ? [] : [extra]
22913
22916
  ].join(" ").toLowerCase();
22914
22917
  }
22915
- function groupHaystack(g) {
22916
- const cached2 = haystackCache.get(g);
22918
+ function typeHaystack(t) {
22919
+ const cached2 = haystackCache.get(t);
22917
22920
  if (cached2 !== void 0) return cached2;
22918
- const haystack = buildHaystack(g);
22919
- haystackCache.set(g, haystack);
22921
+ const haystack = buildHaystack(t);
22922
+ haystackCache.set(t, haystack);
22920
22923
  return haystack;
22921
22924
  }
22922
22925
  var actionsCache = /* @__PURE__ */ new WeakMap();
22923
- function groupActions(g) {
22924
- const cached2 = actionsCache.get(g);
22925
- if (cached2 !== void 0) return cached2;
22926
- const actions = [...new Set(g.instances.map((i) => i.action))];
22927
- actionsCache.set(g, actions);
22928
- return actions;
22926
+ function typeActions(t) {
22927
+ return actionsCache.get(t) ?? [];
22929
22928
  }
22930
22929
  function countInstancesByStatus(statusInputs, statuses) {
22931
22930
  const statusSet = new Set(statuses);
@@ -22936,8 +22935,8 @@ function countInstancesByStatus(statusInputs, statuses) {
22936
22935
  }
22937
22936
  return sum;
22938
22937
  }
22939
- function applyFindingFilters(groups, opts) {
22940
- let filtered = groups;
22938
+ function applyFindingFilters(types, opts) {
22939
+ let filtered = types;
22941
22940
  if (opts.severity && opts.severity.length > 0) {
22942
22941
  const sevSet = new Set(opts.severity);
22943
22942
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -22948,7 +22947,7 @@ function applyFindingFilters(groups, opts) {
22948
22947
  }
22949
22948
  if (opts.actions && opts.actions.length > 0) {
22950
22949
  const actionSet = new Set(opts.actions);
22951
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
22950
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
22952
22951
  }
22953
22952
  if (opts.subtype && opts.subtype.length > 0) {
22954
22953
  const subtypeSet = new Set(opts.subtype);
@@ -22960,7 +22959,7 @@ function applyFindingFilters(groups, opts) {
22960
22959
  }
22961
22960
  if (opts.q) {
22962
22961
  const q = opts.q.toLowerCase();
22963
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
22962
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
22964
22963
  }
22965
22964
  return filtered;
22966
22965
  }
@@ -22975,11 +22974,11 @@ function compareFindingGroupOrder(a, b) {
22975
22974
  if (recencyDiff !== 0) return recencyDiff;
22976
22975
  return a.id.localeCompare(b.id);
22977
22976
  }
22978
- function sortFindingGroups(groups) {
22979
- return [...groups].sort(compareFindingGroupOrder);
22977
+ function sortFindingTypes(types) {
22978
+ return [...types].sort(compareFindingGroupOrder);
22980
22979
  }
22981
- function computeFindingFacets(allGroups, opts) {
22982
- const forSeverity = applyFindingFilters(allGroups, {
22980
+ function computeFindingFacets(allTypes, opts) {
22981
+ const forSeverity = applyFindingFilters(allTypes, {
22983
22982
  providers: opts.providers,
22984
22983
  actions: opts.actions,
22985
22984
  statuses: opts.statuses,
@@ -22990,7 +22989,7 @@ function computeFindingFacets(allGroups, opts) {
22990
22989
  for (const g of forSeverity) {
22991
22990
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
22992
22991
  }
22993
- const forProvider = applyFindingFilters(allGroups, {
22992
+ const forProvider = applyFindingFilters(allTypes, {
22994
22993
  actions: opts.actions,
22995
22994
  statuses: opts.statuses,
22996
22995
  q: opts.q,
@@ -23001,7 +23000,7 @@ function computeFindingFacets(allGroups, opts) {
23001
23000
  for (const g of forProvider) {
23002
23001
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
23003
23002
  }
23004
- const forAction = applyFindingFilters(allGroups, {
23003
+ const forAction = applyFindingFilters(allTypes, {
23005
23004
  providers: opts.providers,
23006
23005
  statuses: opts.statuses,
23007
23006
  q: opts.q,
@@ -23010,9 +23009,9 @@ function computeFindingFacets(allGroups, opts) {
23010
23009
  });
23011
23010
  const actionMap = /* @__PURE__ */ new Map();
23012
23011
  for (const g of forAction) {
23013
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23012
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23014
23013
  }
23015
- const forSubtype = applyFindingFilters(allGroups, {
23014
+ const forSubtype = applyFindingFilters(allTypes, {
23016
23015
  providers: opts.providers,
23017
23016
  actions: opts.actions,
23018
23017
  statuses: opts.statuses,
@@ -23021,7 +23020,7 @@ function computeFindingFacets(allGroups, opts) {
23021
23020
  });
23022
23021
  const subtypeMap = /* @__PURE__ */ new Map();
23023
23022
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
23024
- const forStatus = applyFindingFilters(allGroups, {
23023
+ const forStatus = applyFindingFilters(allTypes, {
23025
23024
  providers: opts.providers,
23026
23025
  actions: opts.actions,
23027
23026
  q: opts.q,
@@ -23069,10 +23068,20 @@ function matchesDimension(row, opts, dimension) {
23069
23068
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23070
23069
  case "tools":
23071
23070
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23071
+ // An EMPTY value is a real filter here, not an absent one. The location
23072
+ // list buckets a finding whose event recorded no repo — or no file — under
23073
+ // the empty string, and selecting that bucket has to narrow the panel to
23074
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23075
+ // row omits the key, which every call site already does.
23076
+ //
23077
+ // Reading '' as unset is what this replaced, and it failed in the one place
23078
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23079
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23080
+ // — a row reading 3 findings beside a panel listing every finding there is.
23072
23081
  case "repo":
23073
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23082
+ return opts.repo === void 0 || row.repo === opts.repo;
23074
23083
  case "file":
23075
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23084
+ return opts.file === void 0 || row.file === opts.file;
23076
23085
  case "q":
23077
23086
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23078
23087
  }
@@ -23186,6 +23195,23 @@ function addToLocation(acc, row) {
23186
23195
  acc.statuses.push(row.status);
23187
23196
  acc.ruleIds.add(row.ruleId);
23188
23197
  }
23198
+ function compareLocationOrder(a, b) {
23199
+ const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23200
+ const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23201
+ if (rankA !== rankB) return rankA - rankB;
23202
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23203
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23204
+ }
23205
+ if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23206
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23207
+ return 0;
23208
+ }
23209
+ function encodeLocationId(repo, file2) {
23210
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23211
+ }
23212
+ function encodePart(value) {
23213
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23214
+ }
23189
23215
 
23190
23216
  // ../../packages/schema/src/zod/installed-pack.ts
23191
23217
  var InstalledPack = external_exports.object({
@@ -23689,11 +23715,11 @@ var WorkspaceSettings = external_exports.object({
23689
23715
  // covers the current payload and must be re-granted.
23690
23716
  modelJudgeConsent: ModelJudgeConsent.optional(),
23691
23717
  // Records that the user consented to the DEFERRED send — the outbox — along
23692
- // with the payload shape and the endpoint they agreed to. Since payload v2
23693
- // that covers both the pre-attach backlog and undelivered captures (which
23694
- // carry prompt/reply text in `content`); the key name predates the widening.
23695
- // Absent until granted, and a grant for a different endpoint or an older
23696
- // payload no longer counts.
23718
+ // with the payload shape and the endpoint they agreed to. Since payload v3
23719
+ // that covers the pre-attach backlog AND undelivered captures alike, and both
23720
+ // carry prompt/reply/tool-output text in `content`; the key name predates
23721
+ // both widenings. Absent until granted, and a grant for a different endpoint
23722
+ // or an older payload no longer counts.
23697
23723
  historySyncConsent: HistorySyncConsent.optional()
23698
23724
  });
23699
23725
  function defaultWorkspaceSettings() {
@@ -23824,6 +23850,9 @@ var ManagedSettingKey = external_exports.enum([
23824
23850
  "dataSharesInPlace",
23825
23851
  "redactFallback"
23826
23852
  ]).meta({ id: "ManagedSettingKey" });
23853
+ function isManagedSettingKey(value) {
23854
+ return ManagedSettingKey.safeParse(value).success;
23855
+ }
23827
23856
  var ManagedSettingsValues = external_exports.object({
23828
23857
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
23829
23858
  controlPlane: external_exports.object({
@@ -23848,7 +23877,27 @@ var ManagedSettings = external_exports.object({
23848
23877
  // Which of those the user may not change. A key here with no matching value
23849
23878
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23850
23879
  // the user may still override. The two are separable on purpose.
23851
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23880
+ //
23881
+ // Parsed as NAMES rather than as the enum, and split below: a name this
23882
+ // build does not know is dropped from the locked set and reported, never a
23883
+ // reason to refuse the file. The same shape reaches an older build whenever
23884
+ // an administrator locks a key a newer build added, and refusing it there
23885
+ // ran that build entirely unmanaged — every pin and lock gone — on exactly
23886
+ // the fleets most likely to carry a version skew. A name outside the enum
23887
+ // is still never HONOURED: the lockable set stays explicit above.
23888
+ lockedFields: external_exports.array(external_exports.string()).default([])
23889
+ }).transform(({ lockedFields, ...rest }) => {
23890
+ const known = [];
23891
+ const unknown2 = [];
23892
+ for (const name of lockedFields) {
23893
+ if (isManagedSettingKey(name)) known.push(name);
23894
+ else unknown2.push(name);
23895
+ }
23896
+ return {
23897
+ ...rest,
23898
+ lockedFields: known,
23899
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
23900
+ };
23852
23901
  }).meta({ id: "ManagedSettings" });
23853
23902
 
23854
23903
  // ../../packages/schema/src/zod/project-files.ts
@@ -23972,7 +24021,11 @@ var FindingsTimeseriesPoint = external_exports.object({
23972
24021
  timestamp: external_exports.iso.date(),
23973
24022
  critical: external_exports.number().int().nonnegative(),
23974
24023
  high: external_exports.number().int().nonnegative(),
23975
- medium: external_exports.number().int().nonnegative()
24024
+ medium: external_exports.number().int().nonnegative(),
24025
+ // Optional and additive, so a producer written against the earlier
24026
+ // three-series contract keeps validating. A consumer plotting it resolves the
24027
+ // absent case itself — the chart point requires a number.
24028
+ low: external_exports.number().int().nonnegative().optional()
23976
24029
  }).meta({ id: "FindingsTimeseriesPoint" });
23977
24030
  var FindingsTimeseriesResponse = external_exports.object({
23978
24031
  range: TimeRange,
@@ -23998,6 +24051,10 @@ var ResolvedFeedItem = external_exports.object({
23998
24051
  findingKey: external_exports.string(),
23999
24052
  ruleId: external_exports.string(),
24000
24053
  severity: Severity,
24054
+ // Repository slug, and the file path RELATIVE to it. The pair is what
24055
+ // identifies the file: a bare path matches the same name in every repo.
24056
+ // Optional and additive; empty when the event carried no repo.
24057
+ repo: external_exports.string().optional(),
24001
24058
  path: external_exports.string(),
24002
24059
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
24003
24060
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -25541,7 +25598,8 @@ var SqliteActivityRepository = class {
25541
25598
  SELECT 1 FROM audit_events d
25542
25599
  WHERE d.root_session_id = audit_events.id
25543
25600
  AND (d.content LIKE ? ESCAPE '\\'
25544
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
25601
+ OR coalesce(json_extract(d.attributes, '$.detail'),
25602
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
25545
25603
  );
25546
25604
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
25547
25605
  }
@@ -26879,23 +26937,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26879
26937
  )`;
26880
26938
 
26881
26939
  // ../../packages/persistence/src/repositories/findings.ts
26882
- var PREVIEW_INSTANCES_PER_GROUP = 200;
26883
- var DEFAULT_LOCATIONS_LIMIT = 100;
26884
- var LOCATION_RULE_IDS_CAP = 20;
26885
- function compareLocationOrder(a, b) {
26886
- return compareFindingGroupOrder(
26887
- {
26888
- severity: a.maxSeverity,
26889
- latestDetectedAt: a.latestDetectedAt,
26890
- id: ""
26891
- },
26892
- {
26893
- severity: b.maxSeverity,
26894
- latestDetectedAt: b.latestDetectedAt,
26895
- id: ""
26896
- }
26897
- );
26898
- }
26899
26940
  var CONCAT_SEP = ",";
26900
26941
  var TUPLE_SEP = "|";
26901
26942
  function splitConcat(value) {
@@ -26947,13 +26988,48 @@ function decodeGroupCursor(cursor) {
26947
26988
  return null;
26948
26989
  }
26949
26990
  function firstAfter(sorted, cursor) {
26950
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
26991
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
26951
26992
  return index === -1 ? sorted.length : index;
26952
26993
  }
26953
26994
  function findDeepLinked(sorted, page, id) {
26954
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
26955
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
26995
+ if (page.some((t) => t.id === id)) return void 0;
26996
+ return sorted.find((t) => t.id === id);
26997
+ }
26998
+ function encodeLocationCursor(location) {
26999
+ const payload = {
27000
+ sev: location.maxSeverity,
27001
+ t: location.latestDetectedAt,
27002
+ r: location.repo,
27003
+ f: location.file
27004
+ };
27005
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
27006
+ }
27007
+ function decodeLocationCursor(cursor) {
27008
+ const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
27009
+ if (parsed2 !== void 0 && typeof parsed2.sev === "string" && typeof parsed2.t === "string" && typeof parsed2.r === "string" && typeof parsed2.f === "string") {
27010
+ return { maxSeverity: parsed2.sev, latestDetectedAt: parsed2.t, repo: parsed2.r, file: parsed2.f };
27011
+ }
27012
+ return null;
27013
+ }
27014
+ function firstLocationAfter(sorted, cursor) {
27015
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
27016
+ return index === -1 ? sorted.length : index;
26956
27017
  }
27018
+ function findDeepLinkedLocation(sorted, page, id) {
27019
+ if (page.some((l) => l.id === id)) return void 0;
27020
+ return sorted.find((l) => l.id === id);
27021
+ }
27022
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
27023
+ d.severity AS severity, f.masked_match AS masked_match,
27024
+ f.action_taken AS action_taken, f.confidence AS confidence,
27025
+ e.started_at AS occurred_at,
27026
+ e.source_tool AS source_tool,
27027
+ e.repo AS repo,
27028
+ e.file_path AS file,
27029
+ e.tool_name AS tool_name,
27030
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
27031
+ e.event_type AS kind, f.finding_key AS finding_key,
27032
+ ${latestResolutionStatusSql("f")} AS latest_status`;
26957
27033
  var DAY_MS3 = 864e5;
26958
27034
  var SqliteFindingsRepository = class {
26959
27035
  constructor(db) {
@@ -27074,30 +27150,26 @@ var SqliteFindingsRepository = class {
27074
27150
  );
27075
27151
  }
27076
27152
  /**
27077
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
27078
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
27079
- * attributes bag, rule_id/category/severity from the definition), scoped to
27080
- * the four capture kinds (audit_events also holds structural/reconciler/scan
27081
- * rows this list must never surface), groups by ruleId, computes
27082
- * per-filter-excluded facets, applies the requested filters, and sorts by
27083
- * severity then recency. Filtering
27084
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
27085
- * reflect the full filtered set; `items` is the requested
27086
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
27087
- * filter, `totals.findings` counts only instances whose derived status was
27088
- * requested, and each item's instance preview is narrowed the same way.
27153
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
27154
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
27155
+ * list must never surface), with per-filter-excluded facets, the requested
27156
+ * filters applied, and sorted by severity then recency. Filtering and faceting
27157
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
27158
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
27159
+ * Under a `status` filter, `totals.findings` counts only findings whose
27160
+ * derived status was requested.
27161
+ *
27162
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
27163
+ * folding EVERY finding into the numbers a type row and the filters need
27164
+ * (count, severity, category, providers, actions, statuses, latest, search
27165
+ * text). The findings OF a type come from listFindingInstances scoped to
27166
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
27089
27167
  *
27090
- * Two reads, neither of which materializes a row per finding:
27091
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
27092
- * the group and the filters need (count, providers, actions, statuses,
27093
- * latest, search text);
27094
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
27095
- * populate `instances` for the table's expanded rows.
27096
27168
  * The aggregates carry raw DB values and are translated by the same
27097
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
27098
- * rule is ever restated in SQL.
27169
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
27170
+ * status rule is ever restated in SQL.
27099
27171
  */
27100
- listGroupedFindings(query) {
27172
+ listFindingTypes(query) {
27101
27173
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
27102
27174
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
27103
27175
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -27110,12 +27182,7 @@ var SqliteFindingsRepository = class {
27110
27182
  predicate,
27111
27183
  params: sessionParams
27112
27184
  });
27113
- const rows = this.previewRows(aggregates, {
27114
- sessionId: query.sessionId,
27115
- from: query.from
27116
- });
27117
- const groupable = rows.map(toFlatFindingRow);
27118
- const allGroups = buildFindingGroups(groupable, { aggregates });
27185
+ const allTypes = buildFindingTypes(aggregates);
27119
27186
  const filterOpts = {
27120
27187
  severity: query.severity,
27121
27188
  providers: query.provider,
@@ -27124,30 +27191,25 @@ var SqliteFindingsRepository = class {
27124
27191
  subtype: query.subtype,
27125
27192
  q: query.q
27126
27193
  };
27127
- const facets = computeFindingFacets(allGroups, filterOpts);
27128
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
27194
+ const facets = computeFindingFacets(allTypes, filterOpts);
27195
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
27129
27196
  const statusFilter = query.status ?? [];
27130
27197
  const totals = {
27131
- findings: sorted.reduce((acc, g) => {
27132
- if (statusFilter.length === 0) return acc + g.instanceCount;
27133
- const agg = aggregates.get(g.id);
27134
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
27198
+ findings: sorted.reduce((acc, t) => {
27199
+ if (statusFilter.length === 0) return acc + t.instanceCount;
27200
+ const agg = aggregates.get(t.id);
27201
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
27135
27202
  }, 0),
27136
- groups: sorted.length
27203
+ types: sorted.length
27137
27204
  };
27138
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
27205
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
27139
27206
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
27140
27207
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
27141
27208
  const page = sorted.slice(start, start + limit);
27142
27209
  const lastOnPage = page.at(-1);
27143
27210
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
27144
27211
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
27145
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
27146
- const narrow = (g) => statusSet ? {
27147
- ...g,
27148
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
27149
- } : g;
27150
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
27212
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
27151
27213
  return Promise.resolve({
27152
27214
  totals,
27153
27215
  facets,
@@ -27158,7 +27220,7 @@ var SqliteFindingsRepository = class {
27158
27220
  }
27159
27221
  /**
27160
27222
  * One row per rule_id, folding EVERY instance of the group into the values
27161
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
27223
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
27162
27224
  * distinct rule_ids (the installed packs' rules), not by the store's size.
27163
27225
  *
27164
27226
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -27252,13 +27314,25 @@ var SqliteFindingsRepository = class {
27252
27314
  });
27253
27315
  }
27254
27316
  /**
27255
- * The same findings folded by location: repository, then file within it.
27317
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
27256
27318
  *
27257
27319
  * The grouping keys come from the capturing event's attributes, which is what
27258
- * the local store relates a finding to — there is no finding↔asset row to
27259
- * group by instead. A repo or file the event did not record folds into the
27260
- * empty-string bucket, which the view renders but does not link, since no
27261
- * filter can name it.
27320
+ * the local store relates a finding to; there is no finding↔asset row to group
27321
+ * by instead. A repo or file the event did not record folds into the
27322
+ * empty-string bucket, which is a real location like any other: it is listed,
27323
+ * it is selectable, and its `?loc=` token is as good as any other row's.
27324
+ *
27325
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
27326
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
27327
+ * list was rebuilt to remove — and two-level pagination inside an
27328
+ * expand/collapse table is what pushed that view to master/detail in the first
27329
+ * place.
27330
+ *
27331
+ * Every filter narrows the FINDINGS and the locations fall out of what
27332
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
27333
+ * reports for the same filters scoped to that pair. The view depends on it:
27334
+ * one toolbar sits over both panels precisely because a location owns none of
27335
+ * its fields.
27262
27336
  */
27263
27337
  listFindingLocations(query) {
27264
27338
  const opts = {
@@ -27270,13 +27344,16 @@ var SqliteFindingsRepository = class {
27270
27344
  tools: query.tool,
27271
27345
  q: query.q
27272
27346
  };
27273
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
27347
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
27348
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
27274
27349
  const byRepo = /* @__PURE__ */ new Map();
27350
+ const accumulator = createInstanceFacetAccumulator(opts);
27275
27351
  let total = 0;
27276
27352
  for (const row of this.scanFindingRows({
27277
27353
  sessionId: query.sessionId,
27278
27354
  from: query.from
27279
27355
  })) {
27356
+ accumulator.add(row);
27280
27357
  if (!matchesInstanceFilters(row, opts)) continue;
27281
27358
  total += 1;
27282
27359
  let files = byRepo.get(row.repo);
@@ -27291,103 +27368,35 @@ var SqliteFindingsRepository = class {
27291
27368
  }
27292
27369
  addToLocation(acc, row);
27293
27370
  }
27294
- let fileCount = 0;
27295
- const repos = [...byRepo.entries()].map(([repo, files]) => {
27296
- fileCount += files.size;
27297
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
27298
- file: file2,
27299
- instanceCount: acc.instanceCount,
27300
- maxSeverity: acc.maxSeverity,
27301
- latestDetectedAt: acc.latestDetectedAt,
27302
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
27303
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
27304
- })).sort(compareLocationOrder);
27305
- const rollup = fileRows.reduce(
27306
- (a, f) => ({
27307
- instanceCount: a.instanceCount + f.instanceCount,
27308
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
27309
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
27310
- }),
27311
- {
27312
- instanceCount: 0,
27313
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
27314
- latestDetectedAt: ""
27315
- }
27316
- );
27317
- const statuses = fileRows.map((f) => f.status);
27318
- const folded = foldGroupStatus(statuses);
27319
- return {
27320
- repo,
27321
- instanceCount: rollup.instanceCount,
27322
- maxSeverity: rollup.maxSeverity,
27323
- latestDetectedAt: rollup.latestDetectedAt,
27324
- ...folded === void 0 ? {} : { status: folded },
27325
- files: fileRows
27326
- };
27327
- });
27328
- repos.sort(compareLocationOrder);
27371
+ const sorted = [];
27372
+ for (const [repo, files] of byRepo) {
27373
+ for (const [file2, acc] of files) {
27374
+ const status = foldGroupStatus(acc.statuses);
27375
+ sorted.push({
27376
+ id: encodeLocationId(repo, file2),
27377
+ repo,
27378
+ file: file2,
27379
+ instanceCount: acc.instanceCount,
27380
+ maxSeverity: acc.maxSeverity,
27381
+ latestDetectedAt: acc.latestDetectedAt,
27382
+ ...status === void 0 ? {} : { status },
27383
+ ruleIds: [...acc.ruleIds]
27384
+ });
27385
+ }
27386
+ }
27387
+ sorted.sort(compareLocationOrder);
27388
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
27389
+ const page = sorted.slice(start, start + limit);
27390
+ const lastOnPage = page.at(-1);
27391
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
27392
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
27329
27393
  return Promise.resolve({
27330
- totals: { findings: total, repos: repos.length, files: fileCount },
27331
- items: repos.slice(0, limit),
27332
- hasMore: repos.length > limit
27394
+ totals: { findings: total, locations: sorted.length },
27395
+ facets: accumulator.facets(),
27396
+ items: [...page, ...deepLinked ? [deepLinked] : []],
27397
+ nextCursor
27333
27398
  });
27334
27399
  }
27335
- /**
27336
- * Each group's newest instances, for the table's expanded rows.
27337
- *
27338
- * ONE index-ordered scan with early termination, and the shape is the point.
27339
- * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27340
- * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27341
- * through a temp B-tree to keep a bounded preview of each group, and then
27342
- * sorts the survivors again for the page order. Both sorts grow with the
27343
- * store while the answer does not.
27344
- *
27345
- * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27346
- * (or the session or window index the scope names — see `findingScanSql`),
27347
- * which is already the order the page wants, and keeps rows per rule until
27348
- * each rule has as many as it can show. The aggregate the caller already holds
27349
- * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27350
- * per rule, summed, is the number of rows this scan has to find, and it stops
27351
- * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27352
- * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27353
- * store with many firing rules widens it. The bound that DOES hold
27354
- * unconditionally is the sorted form's floor: this scan visits at most as
27355
- * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27356
- * sorted, and stops the moment every rule has its cap, where the sorted form
27357
- * sorts the whole scope regardless. The true worst case — the rarest rule's
27358
- * wanted instances sitting at the tail of the scope — is one pass over
27359
- * everything in scope with a block sort of the id tie-break only, never a
27360
- * sort of the scope, which is still that floor.
27361
- *
27362
- * A row whose rule the aggregate did not see is skipped: the two statements
27363
- * run without a shared snapshot, so a capture landing between them can add a
27364
- * rule here that has no counts there, and the counts are what the group is
27365
- * built from.
27366
- */
27367
- previewRows(aggregates, scope) {
27368
- const wanted = /* @__PURE__ */ new Map();
27369
- let remaining = 0;
27370
- for (const [ruleId, agg] of aggregates) {
27371
- const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27372
- wanted.set(ruleId, n);
27373
- remaining += n;
27374
- }
27375
- const rows = [];
27376
- if (remaining === 0) return rows;
27377
- const { sql, params } = this.findingScanSql(scope);
27378
- const taken = /* @__PURE__ */ new Map();
27379
- for (const r of iterateRows(this.db.prepare(sql), params)) {
27380
- const want = wanted.get(r.rule_id);
27381
- if (want === void 0) continue;
27382
- const have = taken.get(r.rule_id) ?? 0;
27383
- if (have >= want) continue;
27384
- taken.set(r.rule_id, have + 1);
27385
- rows.push(r);
27386
- remaining -= 1;
27387
- if (remaining === 0) break;
27388
- }
27389
- return rows;
27390
- }
27391
27400
  /**
27392
27401
  * Every finding in scope as a FlatFindingRow, newest first, streamed.
27393
27402
  *
@@ -27414,6 +27423,33 @@ var SqliteFindingsRepository = class {
27414
27423
  yield toFlatFindingRow(r);
27415
27424
  }
27416
27425
  }
27426
+ /**
27427
+ * One finding by its own id, or null when no such row exists.
27428
+ *
27429
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
27430
+ * the store — and, unlike anything derived from a list page, it resolves a
27431
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
27432
+ * deep link needs: the id it carries may name a finding thousands of rows
27433
+ * older than anything a first page holds.
27434
+ *
27435
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
27436
+ * RESOLVES an id; whether that row would survive the list's current filters is
27437
+ * a different question, and hiding the target because a filter excludes it is
27438
+ * worse than showing it.
27439
+ *
27440
+ * `groupId` on the result IS the rule id, so this one read answers both "which
27441
+ * type should the list select?" and "what does the drawer show?".
27442
+ */
27443
+ findingInstance(id) {
27444
+ const row = this.db.prepare(
27445
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
27446
+ FROM inspection_findings f
27447
+ JOIN audit_events e ON e.id = f.audit_event_id
27448
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27449
+ WHERE f.id = ?`
27450
+ ).get(id);
27451
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
27452
+ }
27417
27453
  /**
27418
27454
  * The one statement both instance-level scans run: every finding in scope,
27419
27455
  * joined to its event and definition, newest first.
@@ -27447,17 +27483,7 @@ var SqliteFindingsRepository = class {
27447
27483
  conditions.push("e.started_at >= ?");
27448
27484
  params.push(isoToEpochMillis(scope.from));
27449
27485
  }
27450
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27451
- d.severity AS severity, f.masked_match AS masked_match,
27452
- f.action_taken AS action_taken, f.confidence AS confidence,
27453
- e.started_at AS occurred_at,
27454
- e.source_tool AS source_tool,
27455
- e.repo AS repo,
27456
- e.file_path AS file,
27457
- e.tool_name AS tool_name,
27458
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27459
- e.event_type AS kind, f.finding_key AS finding_key,
27460
- ${latestResolutionStatusSql("f")} AS latest_status
27486
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
27461
27487
  FROM audit_events e
27462
27488
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27463
27489
  CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -27471,6 +27497,26 @@ var SqliteFindingsRepository = class {
27471
27497
  group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27472
27498
  const rows = this.db.prepare(
27473
27499
  `SELECT rule_id,
27500
+ -- BARE columns beside max(latest_at), which is deliberate and
27501
+ -- is SQLite's documented behaviour: with a single min()/max()
27502
+ -- in an aggregate query, every bare column takes its value from
27503
+ -- the row that produced the extremum. So these are the severity
27504
+ -- and category of the definition whose finding is NEWEST, which
27505
+ -- is what the row-based build they replaced read off its first
27506
+ -- (newest-first) row.
27507
+ --
27508
+ -- min() is WRONG here and was the defect: inspection_definitions
27509
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
27510
+ -- mints a new row), so a rule whose severity moved between
27511
+ -- versions has several, and min() picks the ALPHABETICALLY
27512
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
27513
+ -- That is arbitrary in direction, and it feeds the badge, the
27514
+ -- filter, the facet counts and the primary sort key.
27515
+ --
27516
+ -- Adding a second min()/max() aggregate here would make these
27517
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
27518
+ severity,
27519
+ category,
27474
27520
  sum(tuple_count) AS instance_count,
27475
27521
  max(latest_at) AS latest_at,
27476
27522
  group_concat(source_tools) AS source_tools,
@@ -27481,6 +27527,14 @@ var SqliteFindingsRepository = class {
27481
27527
  group_concat(tool_names) AS tool_names
27482
27528
  FROM (
27483
27529
  SELECT d.rule_id AS rule_id,
27530
+ -- Severity and category are columns of the DEFINITION, and
27531
+ -- a rule can have SEVERAL definitions (one per version), so
27532
+ -- these are grouped on below and resolved to the newest
27533
+ -- firing version by the outer query's bare-column select.
27534
+ -- They ride the aggregate because the type build has no rows
27535
+ -- to read them off \u2014 see buildFindingTypes.
27536
+ d.severity AS severity,
27537
+ d.category AS category,
27484
27538
  e.event_type || '${TUPLE_SEP}' ||
27485
27539
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
27486
27540
  coalesce(latest.status, '') AS status_tuple,
@@ -27495,7 +27549,7 @@ var SqliteFindingsRepository = class {
27495
27549
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27496
27550
  ON latest.finding_key = f.finding_key
27497
27551
  ${scope.predicate}
27498
- GROUP BY d.rule_id, status_tuple
27552
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
27499
27553
  )
27500
27554
  GROUP BY rule_id`
27501
27555
  ).all(scope.params);
@@ -27504,6 +27558,8 @@ var SqliteFindingsRepository = class {
27504
27558
  r.rule_id,
27505
27559
  {
27506
27560
  instanceCount: r.instance_count,
27561
+ severity: r.severity,
27562
+ category: r.category,
27507
27563
  sourceTools: splitConcat(r.source_tools),
27508
27564
  actionsTaken: splitConcat(r.actions_taken),
27509
27565
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -27520,7 +27576,7 @@ var SqliteFindingsRepository = class {
27520
27576
  latestDetectedAt: epochMillisToIso(r.latest_at),
27521
27577
  // Free text only — joined and substring-matched, so group_concat's
27522
27578
  // commas need no unpicking (a repo/path containing one still matches).
27523
- // Left undefined (not '') when unfetched, so buildFindingGroups can
27579
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
27524
27580
  // tell "no q this request" from "a group with no repo/file at all"
27525
27581
  // and skip priming a haystack nothing will read.
27526
27582
  ...withSearchText ? {
@@ -27672,6 +27728,12 @@ var SqliteHistorySyncRepository = class {
27672
27728
  this.markOwedStmt = db.prepare(
27673
27729
  `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27674
27730
  );
27731
+ this.markCaptureBacklogOwedStmt = db.prepare(
27732
+ `UPDATE audit_events SET outbox_owed = 1
27733
+ WHERE synced_at IS NULL
27734
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27735
+ AND started_at < :before`
27736
+ );
27675
27737
  this.stampStmt = db.prepare(
27676
27738
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27677
27739
  );
@@ -27720,7 +27782,9 @@ var SqliteHistorySyncRepository = class {
27720
27782
  );
27721
27783
  this.disownCapturesStmt = db.prepare(
27722
27784
  `UPDATE audit_events SET outbox_owed = NULL
27723
- WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27785
+ WHERE outbox_owed IS NOT NULL
27786
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27787
+ AND started_at < :attachedAt`
27724
27788
  );
27725
27789
  this.rearmStmt = db.prepare(
27726
27790
  `UPDATE audit_events SET synced_at = NULL
@@ -27796,6 +27860,7 @@ var SqliteHistorySyncRepository = class {
27796
27860
  freezeBoundaryStmt;
27797
27861
  captureRowsStmt;
27798
27862
  markOwedStmt;
27863
+ markCaptureBacklogOwedStmt;
27799
27864
  captureSkipCountStmt;
27800
27865
  disownCapturesStmt;
27801
27866
  partitionStmt;
@@ -27859,6 +27924,23 @@ var SqliteHistorySyncRepository = class {
27859
27924
  markCaptureOwed(id) {
27860
27925
  this.markOwedStmt.run({ id });
27861
27926
  }
27927
+ /**
27928
+ * Mark every capture already on disk as owed, as of `before`.
27929
+ *
27930
+ * The consent-time backfill, called once from `aka attach` when a human
27931
+ * grants existing-history consent — never from an ongoing drain pass, and
27932
+ * never inferred from a boundary that could later move. `before` is the
27933
+ * caller's own "now" at the moment consent was granted, so what this marks
27934
+ * is exactly the backlog the consent prompt already counted, not whatever a
27935
+ * later re-attach or key rotation might widen it to.
27936
+ *
27937
+ * Returns how many rows matched, for the caller to log or test against. Not a
27938
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
27939
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
27940
+ */
27941
+ markCaptureBacklogOwed(before) {
27942
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
27943
+ }
27862
27944
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27863
27945
  markSynced(ids, atMs) {
27864
27946
  this.stampAll(ids, atMs);
@@ -27975,15 +28057,42 @@ var SqliteHistorySyncRepository = class {
27975
28057
  *
27976
28058
  * Delivery is a fact about ONE recipient: rows sent to the deployment a
27977
28059
  * machine has just left are undelivered as far as the new one is concerned.
27978
- * All three in one transaction, so a crash between them cannot leave stamps
27979
- * attributed to the wrong deployment, or a boundary that belongs to another.
28060
+ * All four in one transaction, so a crash between them cannot leave stamps
28061
+ * attributed to the wrong deployment, a boundary that belongs to another, or
28062
+ * a disown with no re-mark to follow it.
27980
28063
  *
27981
28064
  * The boundary is written HERE and only here, which is what freezes it: a
27982
28065
  * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
27983
28066
  * unchanged, so this never runs and the backlog does not widen back over rows
27984
28067
  * the live path has since delivered.
28068
+ *
28069
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
28070
+ * granted existing-history consent for the deployment this call is arming —
28071
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
28072
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
28073
+ * apart. Passed only when that grant is valid, since this method has no way
28074
+ * to check consent itself and must not mark a row owed for a machine that
28075
+ * never agreed to it. Applied AFTER the disown above, in the SAME
28076
+ * transaction: what the disown clears is every marker below `backlogBefore`,
28077
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
28078
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
28079
+ * on the cleared side of that bound — and the re-mark in the same
28080
+ * transaction is what puts those rows back. A crash between the two cannot
28081
+ * strand the ledger disowned with nothing re-marked — the transaction either
28082
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
28083
+ * committed re-enters this method on the very next pass. Omit it (the
28084
+ * structural-only tests do) to exercise the disown in isolation.
28085
+ *
28086
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
28087
+ * touching a marker the NEW deployment's OWN live path has already set: B's
28088
+ * live path can mark a capture owed from the moment `aka attach` writes the
28089
+ * descriptor, before the drain's first pass ever reaches this method, and
28090
+ * such a row sits at or after the bound rather than below it. What keeps the
28091
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
28092
+ * bound — disown runs first, re-mark second, both inside the one
28093
+ * transaction above.
27985
28094
  */
27986
- rearmFor(fingerprint, backlogBefore) {
28095
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
27987
28096
  this.ensureRowStmt.run();
27988
28097
  withTransaction(
27989
28098
  this.db,
@@ -27991,7 +28100,10 @@ var SqliteHistorySyncRepository = class {
27991
28100
  const previous = getRow(this.fingerprintStmt)?.fingerprint;
27992
28101
  this.rearmStmt.run();
27993
28102
  if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27994
- this.disownCapturesStmt.run();
28103
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
28104
+ }
28105
+ if (backfillCapturesBefore !== void 0) {
28106
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
27995
28107
  }
27996
28108
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27997
28109
  },
@@ -28268,7 +28380,8 @@ function managedSettingsPaths(platform2 = process.platform) {
28268
28380
  }
28269
28381
  return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28270
28382
  }
28271
- function readManagedSettings(paths = managedSettingsPaths()) {
28383
+ var testOnlyManagedPaths = null;
28384
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
28272
28385
  for (const path of paths) {
28273
28386
  let text;
28274
28387
  try {
@@ -30748,7 +30861,7 @@ function toUtcDateString(ms) {
30748
30861
  return new Date(ms).toISOString().slice(0, 10);
30749
30862
  }
30750
30863
  function isTimeseriesSeverity(s) {
30751
- return s === "critical" || s === "high" || s === "medium";
30864
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
30752
30865
  }
30753
30866
  var SqliteSecurityRepository = class {
30754
30867
  constructor(db, now = () => Date.now()) {
@@ -30877,12 +30990,16 @@ var SqliteSecurityRepository = class {
30877
30990
  const now = this.now();
30878
30991
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
30879
30992
  const rows = this.findingsInRange(windowStart, now);
30880
- const points = Array.from({ length: numBuckets }, (_, i) => ({
30881
- timestamp: toUtcDateString(windowStart + i * bucketMs),
30882
- critical: 0,
30883
- high: 0,
30884
- medium: 0
30885
- }));
30993
+ const points = Array.from(
30994
+ { length: numBuckets },
30995
+ (_, i) => ({
30996
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
30997
+ critical: 0,
30998
+ high: 0,
30999
+ medium: 0,
31000
+ low: 0
31001
+ })
31002
+ );
30886
31003
  for (const r of rows) {
30887
31004
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
30888
31005
  const bucket = points[idx];
@@ -31100,6 +31217,7 @@ var SqliteSecurityRepository = class {
31100
31217
  `SELECT f.finding_key AS finding_key,
31101
31218
  d.rule_id AS rule_id,
31102
31219
  d.severity AS severity,
31220
+ e.repo AS repo,
31103
31221
  e.file_path AS path,
31104
31222
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
31105
31223
  latest.resolved_at AS latest_resolved_at
@@ -31119,6 +31237,7 @@ var SqliteSecurityRepository = class {
31119
31237
  const items = rows.map((r) => ({
31120
31238
  findingKey: r.finding_key,
31121
31239
  ruleId: r.rule_id,
31240
+ repo: r.repo ?? "",
31122
31241
  severity: r.severity,
31123
31242
  path: r.path ?? "",
31124
31243
  resolvedAt: new Date(r.latest_resolved_at).toISOString(),
@@ -31128,13 +31247,66 @@ var SqliteSecurityRepository = class {
31128
31247
  }));
31129
31248
  return Promise.resolve({ items });
31130
31249
  }
31250
+ /**
31251
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
31252
+ *
31253
+ * Scoped by status rather than by time, because the card this feeds is a to-do
31254
+ * list: a secret committed three weeks ago and never rotated is still the most
31255
+ * important thing to fix, and any window hides it. It carried a "newest N
31256
+ * findings" cap and then a range; the first meant a different span on every
31257
+ * machine, and the second reported "no recommendations" over live exposure.
31258
+ *
31259
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
31260
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
31261
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
31262
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
31263
+ * The two answer different questions and only this one has to match a link.
31264
+ *
31265
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
31266
+ * whole-store scope costs a grouped scan rather than a row per finding.
31267
+ */
31268
+ recommendationInputs() {
31269
+ const rows = allRows(
31270
+ this.db.prepare(
31271
+ `SELECT d.rule_id AS rule_id,
31272
+ d.category AS category,
31273
+ d.severity AS severity,
31274
+ COUNT(*) AS count
31275
+ FROM inspection_findings f
31276
+ JOIN audit_events e ON e.id = f.audit_event_id
31277
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31278
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31279
+ ON latest.finding_key = f.finding_key
31280
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31281
+ AND e.event_type = 'code_change'
31282
+ AND (
31283
+ f.finding_key IS NULL
31284
+ OR latest.status IS NULL
31285
+ OR latest.status NOT IN ('resolved', 'dismissed')
31286
+ )
31287
+ GROUP BY d.rule_id, d.category, d.severity`
31288
+ )
31289
+ );
31290
+ return Promise.resolve(
31291
+ rows.map((r) => ({
31292
+ ruleId: r.rule_id,
31293
+ category: r.category,
31294
+ severity: r.severity,
31295
+ count: r.count
31296
+ }))
31297
+ );
31298
+ }
31131
31299
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
31132
31300
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
31133
31301
  // numeric and the JS aggregations bucket/split on ms directly.
31134
31302
  findingsInRange(fromMs, toMs) {
31135
31303
  const rows = allRows(
31136
31304
  this.db.prepare(
31137
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
31305
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
31306
+ // joined for `severity`, so they are two more columns off a row this read
31307
+ // already fetches. They feed the recommended-actions rollup.
31308
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31309
+ d.rule_id AS rule_id, d.category AS category
31138
31310
  FROM inspection_findings f
31139
31311
  JOIN audit_events e ON e.id = f.audit_event_id
31140
31312
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -31147,7 +31319,9 @@ var SqliteSecurityRepository = class {
31147
31319
  return rows.map((r) => ({
31148
31320
  occurredAt: r.occurred_at,
31149
31321
  severity: r.severity,
31150
- actionTaken: r.action_taken
31322
+ actionTaken: r.action_taken,
31323
+ ruleId: r.rule_id,
31324
+ category: r.category
31151
31325
  }));
31152
31326
  }
31153
31327
  };
@@ -32317,8 +32491,45 @@ function openLocalDatabase(dir) {
32317
32491
  };
32318
32492
  }
32319
32493
 
32320
- // ../../packages/persistence/src/finding-key.ts
32494
+ // ../../packages/persistence/src/egress-wire.ts
32321
32495
  import { createHash as createHash3 } from "crypto";
32496
+ function hashProjectKey(projectKey) {
32497
+ return createHash3("sha256").update(projectKey, "utf8").digest("hex");
32498
+ }
32499
+ function toIngestHit(hit) {
32500
+ return {
32501
+ host: hit.host,
32502
+ kind: hit.kind,
32503
+ name: hit.name,
32504
+ category: hit.category,
32505
+ trust: hit.trust,
32506
+ network: hit.network,
32507
+ method: hit.method,
32508
+ transport: hit.transport,
32509
+ url: hit.url,
32510
+ template: hit.template,
32511
+ dataClass: hit.dataClass,
32512
+ site: {
32513
+ file: hit.site.file,
32514
+ line: hit.site.line,
32515
+ dynamic: hit.site.dynamic,
32516
+ vendored: hit.site.vendored
32517
+ }
32518
+ };
32519
+ }
32520
+ function toEgressIngestRequest(input2) {
32521
+ const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
32522
+ const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
32523
+ return {
32524
+ projectKey: hashProjectKey(input2.projectKey),
32525
+ project: input2.project,
32526
+ reconcile,
32527
+ hits: hits.map(toIngestHit)
32528
+ };
32529
+ }
32530
+
32531
+ // ../../packages/persistence/src/finding-key.ts
32532
+ import { createHash as createHash4 } from "crypto";
32322
32533
 
32323
32534
  // ../../packages/persistence/src/fingerprint.ts
32324
32535
  import { createHmac, randomBytes } from "crypto";
@@ -32359,14 +32570,18 @@ function readFingerprintKey(dataDir2) {
32359
32570
  return parseKeyFile(raw);
32360
32571
  }
32361
32572
 
32362
- // ../../packages/persistence/src/history-preview.ts
32573
+ // ../../packages/persistence/src/history-backfill.ts
32363
32574
  import { existsSync as existsSync4 } from "fs";
32364
32575
  import { join as join9 } from "path";
32576
+
32577
+ // ../../packages/persistence/src/history-preview.ts
32578
+ import { existsSync as existsSync5 } from "fs";
32579
+ import { join as join10 } from "path";
32365
32580
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32366
32581
 
32367
32582
  // ../../packages/persistence/src/store-symlinks.ts
32368
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32369
- import { dirname as dirname3, join as join10, resolve } from "path";
32583
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32584
+ import { dirname as dirname3, join as join11, resolve } from "path";
32370
32585
 
32371
32586
  // ../../packages/persistence/src/vault/crypto.ts
32372
32587
  import {
@@ -32381,61 +32596,25 @@ import {
32381
32596
  import { execFileSync } from "child_process";
32382
32597
  import { randomBytes as randomBytes2 } from "crypto";
32383
32598
  import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32384
- import { join as join11 } from "path";
32599
+ import { join as join12 } from "path";
32385
32600
 
32386
32601
  // ../../packages/persistence/src/vault/vault.ts
32387
32602
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32388
32603
 
32389
32604
  // ../../packages/persistence/src/warn-era-cap.ts
32390
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32391
- import { join as join12 } from "path";
32605
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
32606
+ import { join as join13 } from "path";
32392
32607
  var MARKER = "warn-era-capped";
32393
32608
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32394
32609
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32395
- const marker = join12(dataDir2, MARKER);
32396
- if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
32610
+ const marker = join13(dataDir2, MARKER);
32611
+ if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
32397
32612
  const capped = db.policies.capCategoryActions();
32398
32613
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
32399
32614
  `, { mode: DATA_FILE_MODE });
32400
32615
  return { capped };
32401
32616
  }
32402
32617
 
32403
- // ../../packages/plugin-runtime/src/attached/egress-wire.ts
32404
- function hashProjectKey(projectKey) {
32405
- return createHash4("sha256").update(projectKey, "utf8").digest("hex");
32406
- }
32407
- function toIngestHit(hit) {
32408
- return {
32409
- host: hit.host,
32410
- kind: hit.kind,
32411
- name: hit.name,
32412
- category: hit.category,
32413
- trust: hit.trust,
32414
- network: hit.network,
32415
- method: hit.method,
32416
- transport: hit.transport,
32417
- url: hit.url,
32418
- template: hit.template,
32419
- dataClass: hit.dataClass,
32420
- site: {
32421
- file: hit.site.file,
32422
- line: hit.site.line,
32423
- dynamic: hit.site.dynamic,
32424
- vendored: hit.site.vendored
32425
- }
32426
- };
32427
- }
32428
- function toEgressIngestRequest(input2) {
32429
- const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
32430
- const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
32431
- return {
32432
- projectKey: hashProjectKey(input2.projectKey),
32433
- project: input2.project,
32434
- reconcile,
32435
- hits: hits.map(toIngestHit)
32436
- };
32437
- }
32438
-
32439
32618
  // ../../packages/remote/src/http.ts
32440
32619
  import { request as httpRequest } from "http";
32441
32620
  import { request as httpsRequest } from "https";
@@ -32715,6 +32894,7 @@ function createRemoteClient(options) {
32715
32894
  url: url2(ROUTES.shares),
32716
32895
  body: JSON.stringify(validated.data)
32717
32896
  });
32897
+ if (response.status === 404) throw new RemoteRouteAbsent(ROUTES.shares);
32718
32898
  okBody(response);
32719
32899
  },
32720
32900
  async pollCommand() {
@@ -32737,19 +32917,51 @@ function createRemoteClient(options) {
32737
32917
  };
32738
32918
  }
32739
32919
 
32740
- // ../../packages/plugin-runtime/src/attached/failure.ts
32920
+ // ../../packages/remote/src/failure-kind.ts
32741
32921
  function statusOf(err) {
32742
32922
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
32743
32923
  const { status } = err;
32744
32924
  if (typeof status !== "number" || !Number.isInteger(status)) return null;
32745
32925
  return status >= 100 && status <= 599 ? status : null;
32746
32926
  }
32747
- function classifyFailure(err) {
32748
- switch (statusOf(err)) {
32927
+ function nameOf(err) {
32928
+ if (typeof err !== "object" || err === null || !("name" in err)) return null;
32929
+ return typeof err.name === "string" ? err.name : null;
32930
+ }
32931
+ function classifyRemoteFailure(err) {
32932
+ switch (nameOf(err)) {
32933
+ case "RemoteRouteAbsent":
32934
+ return "route-absent";
32935
+ case "RemoteRequestInvalid":
32936
+ return "invalid-request";
32937
+ case "RemoteResponseInvalid":
32938
+ return "rejected";
32939
+ default:
32940
+ break;
32941
+ }
32942
+ const status = statusOf(err);
32943
+ if (status === null) return "unreachable";
32944
+ switch (status) {
32749
32945
  case 401:
32750
32946
  return "unauthorized";
32751
32947
  case 403:
32752
32948
  return "forbidden";
32949
+ case 429:
32950
+ return "unreachable";
32951
+ case 404:
32952
+ return "unreachable";
32953
+ default:
32954
+ return status >= 400 && status <= 499 ? "rejected" : "unreachable";
32955
+ }
32956
+ }
32957
+
32958
+ // ../../packages/plugin-runtime/src/attached/failure.ts
32959
+ function classifyFailure(err) {
32960
+ switch (classifyRemoteFailure(err)) {
32961
+ case "unauthorized":
32962
+ return "unauthorized";
32963
+ case "forbidden":
32964
+ return "forbidden";
32753
32965
  default:
32754
32966
  return "unreachable";
32755
32967
  }
@@ -32772,10 +32984,10 @@ function withTimeout(promise2, ms) {
32772
32984
 
32773
32985
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
32774
32986
  import { readFileSync as readFileSync8 } from "fs";
32775
- import { join as join13 } from "path";
32987
+ import { join as join14 } from "path";
32776
32988
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
32777
32989
  function forwardDropsPath(dataDir2) {
32778
- return join13(dataDir2, FORWARD_DROPS_FILENAME);
32990
+ return join14(dataDir2, FORWARD_DROPS_FILENAME);
32779
32991
  }
32780
32992
  function recordForwardDrops(dataDir2, count, nowMs) {
32781
32993
  if (count <= 0) return;
@@ -32811,13 +33023,13 @@ function readForwardDrops(dataDir2) {
32811
33023
 
32812
33024
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
32813
33025
  import { randomUUID as randomUUID15 } from "crypto";
32814
- import { readFileSync as readFileSync14 } from "fs";
33026
+ import { readFileSync as readFileSync15 } from "fs";
32815
33027
  import { readFile, rename, writeFile } from "fs/promises";
32816
- import { join as join22 } from "path";
33028
+ import { join as join24 } from "path";
32817
33029
 
32818
33030
  // ../../packages/plugin-sdk/src/config.ts
32819
- import { existsSync as existsSync7 } from "fs";
32820
- import { join as join14 } from "path";
33031
+ import { existsSync as existsSync8 } from "fs";
33032
+ import { join as join15 } from "path";
32821
33033
 
32822
33034
  // ../../packages/plugin-sdk/src/provider-env.ts
32823
33035
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32871,8 +33083,8 @@ function resolveProvider() {
32871
33083
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32872
33084
  try {
32873
33085
  ensureLayoutDirSync(base);
32874
- const settingsFile = join14(settingsDir(base), "settings.json");
32875
- if (existsSync7(settingsFile)) tightenFile(settingsFile);
33086
+ const settingsFile = join15(settingsDir(base), "settings.json");
33087
+ if (existsSync8(settingsFile)) tightenFile(settingsFile);
32876
33088
  } catch {
32877
33089
  }
32878
33090
  migrateLegacyLayout(base);
@@ -32897,7 +33109,7 @@ function resolveProviderSafe(resolveProviderFn) {
32897
33109
  // ../../packages/plugin-sdk/src/config-inventory.ts
32898
33110
  import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32899
33111
  import { homedir as homedir2 } from "os";
32900
- import { basename as basename3, join as join16 } from "path";
33112
+ import { basename as basename3, join as join17 } from "path";
32901
33113
 
32902
33114
  // ../../packages/detections/src/egress/registry.ts
32903
33115
  var EXTRACTOR_VERSION = "1";
@@ -35680,24 +35892,20 @@ function bundledDetections() {
35680
35892
  }
35681
35893
 
35682
35894
  // ../../packages/plugin-sdk/src/repo.ts
35683
- import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
35684
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
35895
+ import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
35896
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join16, sep as sep2 } from "path";
35685
35897
 
35686
35898
  // ../../packages/plugin-sdk/src/events.ts
35687
35899
  import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
35688
35900
 
35689
35901
  // ../../packages/plugin-sdk/src/isolated-scan.ts
35690
- import { existsSync as existsSync9 } from "fs";
35902
+ import { existsSync as existsSync10 } from "fs";
35691
35903
  import { fileURLToPath } from "url";
35692
35904
  import { Worker } from "worker_threads";
35693
35905
 
35694
- // ../../packages/plugin-sdk/src/ignore-layers.ts
35695
- var import_ignore = __toESM(require_ignore(), 1);
35696
- import { readFileSync as readFileSync11 } from "fs";
35697
- import { join as join17 } from "path";
35698
-
35699
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
35700
- import { arch, hostname as hostname4, platform, release } from "os";
35906
+ // ../../packages/plugin-sdk/src/host-floor.ts
35907
+ import { readFileSync as readFileSync12 } from "fs";
35908
+ import { join as join19 } from "path";
35701
35909
 
35702
35910
  // ../../packages/plugin-sdk/src/model-governance.ts
35703
35911
  import {
@@ -35705,24 +35913,50 @@ import {
35705
35913
  fstatSync,
35706
35914
  mkdirSync as mkdirSync2,
35707
35915
  openSync as openSync2,
35708
- readFileSync as readFileSync12,
35916
+ readFileSync as readFileSync11,
35709
35917
  readSync,
35710
35918
  writeFileSync as writeFileSync5
35711
35919
  } from "fs";
35712
35920
  import { join as join18 } from "path";
35713
35921
  var TAIL_BYTES = 256 * 1024;
35714
35922
 
35923
+ // ../../packages/plugin-sdk/src/host-floor.ts
35924
+ var HOST_FEATURE = {
35925
+ ModelSwitch: "model-switch",
35926
+ VaultPointerDisplay: "vault-pointer-display"
35927
+ };
35928
+ var HOST_FLOORS = {
35929
+ [HOST_FEATURE.ModelSwitch]: {
35930
+ label: "model-switch protection",
35931
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
35932
+ since: "2.1.251"
35933
+ },
35934
+ [HOST_FEATURE.VaultPointerDisplay]: {
35935
+ label: "vault pointer display",
35936
+ hookEvents: ["MessageDisplay"],
35937
+ since: "2.1.152"
35938
+ }
35939
+ };
35940
+
35941
+ // ../../packages/plugin-sdk/src/ignore-layers.ts
35942
+ var import_ignore = __toESM(require_ignore(), 1);
35943
+ import { readFileSync as readFileSync13 } from "fs";
35944
+ import { join as join20 } from "path";
35945
+
35946
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
35947
+ import { arch, hostname as hostname4, platform, release } from "os";
35948
+
35715
35949
  // ../../packages/plugin-sdk/src/nudge.ts
35716
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
35717
- import { join as join19 } from "path";
35950
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
35951
+ import { join as join21 } from "path";
35718
35952
 
35719
35953
  // ../../packages/plugin-sdk/src/paths.ts
35720
35954
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
35721
35955
  import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
35722
35956
 
35723
35957
  // ../../packages/plugin-sdk/src/project-files.ts
35724
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
35725
- import { basename as basename5, join as join20 } from "path";
35958
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
35959
+ import { basename as basename5, join as join22 } from "path";
35726
35960
 
35727
35961
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
35728
35962
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -35758,7 +35992,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
35758
35992
 
35759
35993
  // ../../packages/plugin-sdk/src/throttle.ts
35760
35994
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
35761
- import { join as join21 } from "path";
35995
+ import { join as join23 } from "path";
35762
35996
 
35763
35997
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
35764
35998
  function isInvalidRequest(err) {
@@ -35798,7 +36032,7 @@ function parseBreakerState(raw, nowMs) {
35798
36032
  }
35799
36033
  function createForwardPolicy(deps) {
35800
36034
  const now = deps.now ?? (() => Date.now());
35801
- const file2 = join22(deps.dir, STATE_FILENAME);
36035
+ const file2 = join24(deps.dir, STATE_FILENAME);
35802
36036
  let state = null;
35803
36037
  let loading = null;
35804
36038
  async function readState() {
@@ -36520,8 +36754,8 @@ function toolAuditEvent(input2) {
36520
36754
  }
36521
36755
 
36522
36756
  // ../../packages/plugin-runtime/src/attached/history-state.ts
36523
- import { readFileSync as readFileSync15 } from "fs";
36524
- import { join as join23 } from "path";
36757
+ import { readFileSync as readFileSync16 } from "fs";
36758
+ import { join as join25 } from "path";
36525
36759
 
36526
36760
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
36527
36761
  import { createHash as createHash6 } from "crypto";
@@ -36538,7 +36772,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
36538
36772
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
36539
36773
 
36540
36774
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
36541
- import { readFileSync as readFileSync16 } from "fs";
36775
+ import { readFileSync as readFileSync17 } from "fs";
36542
36776
  function createPluginBlock(build, policyStore) {
36543
36777
  return async () => {
36544
36778
  const cached2 = await policyStore.read();
@@ -36557,7 +36791,7 @@ function createPluginBlock(build, policyStore) {
36557
36791
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
36558
36792
  import { randomUUID as randomUUID16 } from "crypto";
36559
36793
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
36560
- import { join as join24 } from "path";
36794
+ import { join as join26 } from "path";
36561
36795
 
36562
36796
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
36563
36797
  import { rename as rename2 } from "fs/promises";
@@ -36581,7 +36815,7 @@ async function publishByRename(tmp, file2, move = rename2) {
36581
36815
 
36582
36816
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
36583
36817
  function createPolicyStore(dir = dataDir()) {
36584
- const file2 = join24(dir, "policy-cache.json");
36818
+ const file2 = join26(dir, "policy-cache.json");
36585
36819
  async function read() {
36586
36820
  try {
36587
36821
  const raw = await readFile2(file2, "utf8");
@@ -36812,11 +37046,11 @@ function readStorePosture(dbPath2) {
36812
37046
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
36813
37047
  import { randomUUID as randomUUID17 } from "crypto";
36814
37048
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
36815
- import { join as join25 } from "path";
37049
+ import { join as join27 } from "path";
36816
37050
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
36817
37051
  function createPostureStore(dir = settingsDir(), legacyDir) {
36818
- const file2 = join25(dir, "posture-state.json");
36819
- const legacyFile = legacyDir === void 0 ? null : join25(legacyDir, "posture-state.json");
37052
+ const file2 = join27(dir, "posture-state.json");
37053
+ const legacyFile = legacyDir === void 0 ? null : join27(legacyDir, "posture-state.json");
36820
37054
  async function persist(state) {
36821
37055
  await ensureDataDir(dir);
36822
37056
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -36884,8 +37118,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
36884
37118
  }
36885
37119
 
36886
37120
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
36887
- import { readFileSync as readFileSync17 } from "fs";
36888
- import { join as join26 } from "path";
37121
+ import { readFileSync as readFileSync18 } from "fs";
37122
+ import { join as join28 } from "path";
36889
37123
 
36890
37124
  // ../../packages/plugin-runtime/src/attached/status.ts
36891
37125
  var REFUSAL_LINES = {
@@ -37381,7 +37615,7 @@ async function readStdin() {
37381
37615
 
37382
37616
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
37383
37617
  import { writeFileSync as writeFileSync8 } from "fs";
37384
- import { join as join27 } from "path";
37618
+ import { join as join29 } from "path";
37385
37619
 
37386
37620
  // ../../packages/setup-wizard/src/triage/merge.ts
37387
37621
  var RANK = Object.fromEntries(
@@ -37389,9 +37623,9 @@ var RANK = Object.fromEntries(
37389
37623
  );
37390
37624
 
37391
37625
  // ../../packages/setup-wizard/src/triage/plan-file.ts
37392
- import { mkdtempSync, readFileSync as readFileSync18, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
37626
+ import { mkdtempSync, readFileSync as readFileSync19, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
37393
37627
  import { tmpdir } from "os";
37394
- import { basename as basename6, dirname as dirname6, join as join28 } from "path";
37628
+ import { basename as basename6, dirname as dirname6, join as join30 } from "path";
37395
37629
  var SuppressionEntrySchema = external_exports.object({
37396
37630
  ruleId: external_exports.string(),
37397
37631
  category: DetectionCategory,
@@ -37477,11 +37711,6 @@ var SEVERITY_GLYPH = {
37477
37711
  low: SHADE.light
37478
37712
  };
37479
37713
  var CATEGORY_ORDER2 = DetectionCategory.options;
37480
- function healthScore(summary) {
37481
- const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
37482
- const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
37483
- return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
37484
- }
37485
37714
  function renderStatusBar(s, opts = {}) {
37486
37715
  const u = s.unreviewed;
37487
37716
  if (opts.color !== true) {
@@ -37500,13 +37729,6 @@ function renderStatusBar(s, opts = {}) {
37500
37729
  function renderStatusLine(summary) {
37501
37730
  return renderStatusBar(findingStatus(summary), { color: true });
37502
37731
  }
37503
- function findingStatus(summary) {
37504
- return {
37505
- score: healthScore(summary),
37506
- unreviewed: { ...summary.bySeverity },
37507
- openFindings: summary.findings
37508
- };
37509
- }
37510
37732
 
37511
37733
  // src/statusline.ts
37512
37734
  try {