@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.
@@ -492,9 +492,9 @@ var require_ignore = __commonJS({
492
492
  });
493
493
 
494
494
  // src/apply-suppressions.ts
495
- import { existsSync as existsSync11, readFileSync as readFileSync15 } from "fs";
495
+ import { existsSync as existsSync12, readFileSync as readFileSync16 } from "fs";
496
496
  import { userInfo } from "os";
497
- import { dirname as dirname8, join as join24 } from "path";
497
+ import { dirname as dirname8, join as join26 } from "path";
498
498
  import { fileURLToPath as fileURLToPath4 } from "url";
499
499
 
500
500
  // ../../packages/persistence/src/attached-derived.ts
@@ -20743,13 +20743,11 @@ var FindingGroup = external_exports.object({
20743
20743
  latestDetectedAt: external_exports.iso.datetime(),
20744
20744
  instances: external_exports.array(FindingInstance),
20745
20745
  // Derived from instances' statuses with open-dominates precedence (see
20746
- // buildFindingGroups). Undefined only when no instance carries a status.
20746
+ // foldGroupStatus). Undefined only when no instance carries a status.
20747
20747
  status: FindingStatus.optional(),
20748
- // The distinct people across the WHOLE group, not just the `instances`
20749
- // preview — from the store's whole-group aggregate when it supplies one,
20750
- // else folded from the rows (see buildFindingGroups). Undefined when no
20751
- // instance carries a user, or when the store supplied whole-group folds
20752
- // without one.
20748
+ // The distinct people across the WHOLE group, not just the instances
20749
+ // carried here. Undefined when no instance carries a user, or when the
20750
+ // store supplied whole-group folds without one.
20753
20751
  users: external_exports.array(FindingUser).optional()
20754
20752
  }).meta({ id: "FindingGroup" });
20755
20753
  var FindingStats = external_exports.object({
@@ -20778,21 +20776,31 @@ var FindingFacets = external_exports.object({
20778
20776
  // counted under no value.
20779
20777
  status: external_exports.array(FindingFacetItem),
20780
20778
  // Host tool (attributes.tool_name). Present only on the instance-level
20781
- // reads, which can filter by it; the grouped read omits the dimension
20779
+ // reads, which can filter by it; the type-level read omits the dimension
20782
20780
  // because a group spans tools.
20783
20781
  tool: external_exports.array(FindingFacetItem).optional()
20784
20782
  }).meta({ id: "FindingFacets" });
20785
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
20786
- var ListGroupedFindingsQuery = external_exports.object({
20783
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20784
+ id: "FindingTypeSummary"
20785
+ });
20786
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
20787
+ var MAX_FINDING_TYPES_LIMIT = 100;
20788
+ var ListFindingTypesQuery = external_exports.object({
20787
20789
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
20788
- // FindingAction.
20790
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
20791
+ // firing version carries, and this list pages types.
20792
+ //
20793
+ // That is NOT a claim the findings of a type share it. A rule can hold several
20794
+ // definition versions at different severities, so a type kept by this filter
20795
+ // can hold findings that individually do not match — see totals.findings on
20796
+ // ListFindingTypesResponse, which counts them all.
20789
20797
  severity: external_exports.array(Severity).optional(),
20790
20798
  subtype: external_exports.array(external_exports.string()).optional(),
20791
20799
  provider: external_exports.array(FindingProvider).optional(),
20792
20800
  action: external_exports.array(FindingAction).optional(),
20793
- // Matches a group's DERIVED status (see FindingGroup.status), not its
20794
- // individual instances' — so a filtered group's Status column always reads
20795
- // one of the requested values.
20801
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
20802
+ // individual findings' — so a filtered row's status always reads one of the
20803
+ // requested values.
20796
20804
  status: external_exports.array(FindingStatus).optional(),
20797
20805
  q: external_exports.string().optional(),
20798
20806
  // Scope to findings whose event carries this session id (the Activity page's
@@ -20802,23 +20810,37 @@ var ListGroupedFindingsQuery = external_exports.object({
20802
20810
  // from a time-scoped page (Activity's range) can carry that scope. Absent
20803
20811
  // means all time — this list has no default window.
20804
20812
  from: external_exports.iso.datetime().optional(),
20805
- // A group or instance id that must appear in the page even when the cursor
20806
- // has already advanced past its sort position. This is what keeps the
20807
- // Findings page's one-shot ?finding= deep link resolving once the list
20808
- // paginates: the target group is appended out of sort order rather than
20809
- // scanning forward for it. Never affects totals, facets or the cursor.
20813
+ // A RULE id that must appear in the page even when the cursor has already
20814
+ // advanced past its sort position. This is what keeps the selected type
20815
+ // visible in the list once it paginates: the target is appended out of sort
20816
+ // order rather than scanned forward for. Never affects totals, facets or the
20817
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
20818
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
20819
+ // and so is not bounded by what any page happens to hold.
20810
20820
  includeId: external_exports.string().optional(),
20811
- groupBy: external_exports.literal("type").optional(),
20812
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
20821
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
20813
20822
  cursor: external_exports.string().optional()
20814
20823
  });
20815
- var ListGroupedFindingsResponse = external_exports.object({
20824
+ var ListFindingTypesResponse = external_exports.object({
20816
20825
  totals: external_exports.object({
20826
+ // Findings belonging to the matching TYPES — not findings that each match
20827
+ // the filters. The filters here select types, so a type that survives
20828
+ // contributes its whole instanceCount.
20829
+ //
20830
+ // `status` is the one exception, narrowed per finding via
20831
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
20832
+ // this can exceed what the instance read reports for the same filters: a
20833
+ // rule whose severity moved between versions is kept on its newest and
20834
+ // still counts its older findings. Narrowing the other three needs
20835
+ // per-dimension counts the aggregate does not carry today.
20817
20836
  findings: external_exports.number().int().nonnegative(),
20818
- groups: external_exports.number().int().nonnegative()
20837
+ // Counts TYPES, which is the unit this read pages. The instance read's
20838
+ // own totals count findings; the two deliberately answer different
20839
+ // questions and are never summed.
20840
+ types: external_exports.number().int().nonnegative()
20819
20841
  }),
20820
20842
  facets: FindingFacets,
20821
- items: external_exports.array(FindingGroup),
20843
+ items: external_exports.array(FindingTypeSummary),
20822
20844
  nextCursor: external_exports.string().nullable(),
20823
20845
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
20824
20846
  // many times that rule fired in the session's persisted transcript. Findings
@@ -20826,7 +20848,7 @@ var ListGroupedFindingsResponse = external_exports.object({
20826
20848
  // every firing, so the two numbers legitimately differ — this map lets a
20827
20849
  // session-scoped view show both.
20828
20850
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
20829
- }).meta({ id: "ListGroupedFindingsResponse" });
20851
+ }).meta({ id: "ListFindingTypesResponse" });
20830
20852
  var ApplyFindingActionRequest = external_exports.object({
20831
20853
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
20832
20854
  // it, so it is excluded from the request contract. The mapping helper
@@ -20856,12 +20878,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
20856
20878
  var MAX_FLAT_FINDINGS_LIMIT = 200;
20857
20879
  var ListFindingInstancesQuery = external_exports.object({
20858
20880
  severity: external_exports.array(Severity).optional(),
20859
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
20881
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
20882
+ // ONE of them is how the master/detail view scopes its right-hand panel.
20860
20883
  subtype: external_exports.array(external_exports.string()).optional(),
20861
20884
  provider: external_exports.array(FindingProvider).optional(),
20862
20885
  action: external_exports.array(FindingAction).optional(),
20863
20886
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
20864
- // the grouped query's group-level fold.
20887
+ // the types query's type-level fold.
20865
20888
  status: external_exports.array(FindingStatus).optional(),
20866
20889
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20867
20890
  // where the free-text `q` can only match the rendered "via Bash" label.
@@ -20878,37 +20901,47 @@ var ListFindingInstancesQuery = external_exports.object({
20878
20901
  });
20879
20902
  var ListFindingInstancesResponse = external_exports.object({
20880
20903
  // Instances matching the filters across the whole scope, not just this
20881
- // page — cursor-independent, like the grouped list's totals.
20904
+ // page — cursor-independent, like the types list's totals.
20882
20905
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
20883
- // Counts in INSTANCES here, where the grouped response counts groups. Each
20906
+ // Counts in INSTANCES here, where the types response counts types. Each
20884
20907
  // dimension still excludes its own filter.
20885
20908
  facets: FindingFacets,
20886
20909
  items: external_exports.array(FindingInstanceDetail),
20887
20910
  nextCursor: external_exports.string().nullable()
20888
20911
  }).meta({ id: "ListFindingInstancesResponse" });
20889
- var FindingLocationFile = external_exports.object({
20890
- // Empty when the instances carried no file path (a prompt or a tool call
20891
- // with no file attribution).
20892
- file: external_exports.string(),
20893
- instanceCount: external_exports.number().int().nonnegative(),
20894
- maxSeverity: Severity,
20895
- latestDetectedAt: external_exports.iso.datetime(),
20896
- // Folded from the instances' derived statuses with the same
20897
- // open-dominates precedence a group uses.
20898
- status: FindingStatus.optional(),
20899
- // Distinct rules seen at this location, capped — the row shows them as
20900
- // chips, and the count is what conveys scale.
20901
- ruleIds: external_exports.array(external_exports.string())
20902
- }).meta({ id: "FindingLocationFile" });
20903
- var FindingLocationRepo = external_exports.object({
20912
+ var FindingLocationSummary = external_exports.object({
20913
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
20914
+ // because a location's identity is two values and a URL param carries one:
20915
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
20916
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
20917
+ // client's page dedupe — never decoded, and never a sort key.
20918
+ id: external_exports.string(),
20904
20919
  /** Empty when the instances carried no repo attribute. */
20905
20920
  repo: external_exports.string(),
20921
+ // Empty when the instances carried no file path (a prompt, or a tool call
20922
+ // with no file attribution). Both halves empty is a real location — usually
20923
+ // the largest one in a store — and is selectable like any other.
20924
+ file: external_exports.string(),
20906
20925
  instanceCount: external_exports.number().int().nonnegative(),
20926
+ // The WORST severity present, not the first row's. It is this list's primary
20927
+ // sort key, so it is also what explains why a row is where it is, and it is
20928
+ // how a reader decides what to open without opening everything.
20907
20929
  maxSeverity: Severity,
20908
20930
  latestDetectedAt: external_exports.iso.datetime(),
20931
+ // Folded from the instances' derived statuses with the same open-dominates
20932
+ // precedence a group uses, so it answers "is anything left to do here" and
20933
+ // not much more: a location holding 1 open among 40 resolved reads like one
20934
+ // holding 40 open. That loss is accepted — the panel beside this list
20935
+ // carries each finding's own status, and instanceCount sits next to the
20936
+ // badge.
20909
20937
  status: FindingStatus.optional(),
20910
- files: external_exports.array(FindingLocationFile)
20911
- }).meta({ id: "FindingLocationRepo" });
20938
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
20939
+ // tally rather than a sample and a row can say how many there are. Bounded
20940
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
20941
+ ruleIds: external_exports.array(external_exports.string())
20942
+ }).meta({ id: "FindingLocationSummary" });
20943
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
20944
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
20912
20945
  var ListFindingLocationsQuery = external_exports.object({
20913
20946
  severity: external_exports.array(Severity).optional(),
20914
20947
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -20921,18 +20954,42 @@ var ListFindingLocationsQuery = external_exports.object({
20921
20954
  q: external_exports.string().optional(),
20922
20955
  sessionId: external_exports.string().optional(),
20923
20956
  from: external_exports.iso.datetime().optional(),
20924
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
20957
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
20958
+ // even when the cursor has already advanced past its sort position — the
20959
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
20960
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
20961
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
20962
+ // into the thousands, a selection sitting off page 0 is the ordinary case
20963
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
20964
+ includeId: external_exports.string().optional(),
20965
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
20966
+ cursor: external_exports.string().optional()
20925
20967
  });
20926
20968
  var ListFindingLocationsResponse = external_exports.object({
20927
20969
  totals: external_exports.object({
20970
+ // Findings matching the filters across the whole scope. Unlike the types
20971
+ // read's same-named field this needs no caveat: the filters here narrow
20972
+ // per finding, so this is the sum of every row's instanceCount.
20928
20973
  findings: external_exports.number().int().nonnegative(),
20929
- repos: external_exports.number().int().nonnegative(),
20930
- files: external_exports.number().int().nonnegative()
20974
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
20975
+ // states. The facets beside it count FINDINGS (see below); a surface
20976
+ // showing both says which is which.
20977
+ locations: external_exports.number().int().nonnegative()
20931
20978
  }),
20932
- /** Sorted by max severity, then most recent. */
20933
- items: external_exports.array(FindingLocationRepo),
20934
- /** Whether `limit` truncated the repo list. */
20935
- hasMore: external_exports.boolean()
20979
+ // Counts in FINDINGS, where the types response counts types, each dimension
20980
+ // still excluding its own filter. Deliberately not locations: counting those
20981
+ // needs a set of location keys per dimension per value — memory tracking the
20982
+ // store times the vocabulary, in a read whose scan promises flat memory —
20983
+ // and the cheap per-location version is not an approximation but WRONG. A
20984
+ // location holding {claudecode, block} and {codex, warn} would survive
20985
+ // provider=claudecode AND action=warn, under which no single finding
20986
+ // matches, so the facet would contradict the instanceCount this whole view
20987
+ // rests on. Findings also keep the toolbar in the same unit as the page
20988
+ // tally and the panel it sits above.
20989
+ facets: FindingFacets,
20990
+ /** Sorted by max severity, then most recent, then (repo, file). */
20991
+ items: external_exports.array(FindingLocationSummary),
20992
+ nextCursor: external_exports.string().nullable()
20936
20993
  }).meta({ id: "ListFindingLocationsResponse" });
20937
20994
 
20938
20995
  // ../../packages/schema/src/zod/meta.ts
@@ -22100,6 +22157,14 @@ var ControlPlaneErrorBody = external_exports.object({
22100
22157
  message: external_exports.string().optional()
22101
22158
  }).optional()
22102
22159
  });
22160
+ var RemoteFailureKind = external_exports.enum([
22161
+ "unauthorized",
22162
+ "forbidden",
22163
+ "route-absent",
22164
+ "invalid-request",
22165
+ "rejected",
22166
+ "unreachable"
22167
+ ]);
22103
22168
  var AttachDeviceRequest = external_exports.object({
22104
22169
  // This machine's own continuity id, so re-attaching ROTATES the credential
22105
22170
  // on one machine record instead of producing a second one. Client-minted
@@ -22793,139 +22858,62 @@ function deriveFindingStatus(row) {
22793
22858
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
22794
22859
  return "open";
22795
22860
  }
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
22861
  function sortUsers(users) {
22807
22862
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
22808
22863
  }
22809
- function buildFindingGroups(rows, opts = {}) {
22810
- const overrides = opts.overrides;
22864
+ function buildFindingTypes(aggregates, opts = {}) {
22811
22865
  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
- );
22866
+ const types = [];
22867
+ for (const [ruleId, agg] of aggregates) {
22868
+ const users = sortUsers(agg.users ?? []);
22869
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
22870
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
22853
22871
  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 = {
22872
+ const apiCategory = toApiCategory(agg.category ?? "custom");
22873
+ const type = {
22870
22874
  id: ruleId,
22871
22875
  category: apiCategory,
22872
22876
  subtype: ruleId,
22873
22877
  // human label comes with pack metadata later
22874
- severity,
22875
- match,
22876
- detection,
22877
- policy,
22878
- instanceCount: agg?.instanceCount ?? instances.length,
22878
+ severity: agg.severity ?? "low",
22879
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
22880
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
22881
+ instanceCount: agg.instanceCount,
22879
22882
  providers,
22880
22883
  aggregateAction,
22881
- latestDetectedAt,
22882
- instances,
22883
- status,
22884
+ latestDetectedAt: agg.latestDetectedAt,
22885
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
22884
22886
  ...users.length > 0 ? { users } : {}
22885
22887
  };
22886
- if (agg) {
22887
- actionsCache.set(group, [...actionSet]);
22888
- if (agg.searchText !== void 0) {
22889
- haystackCache.set(group, buildHaystack(group, agg.searchText));
22890
- }
22888
+ actionsCache.set(type, [...actionSet]);
22889
+ if (agg.searchText !== void 0) {
22890
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
22891
22891
  }
22892
- groups.push(group);
22892
+ types.push(type);
22893
22893
  }
22894
- return groups;
22894
+ return types;
22895
22895
  }
22896
22896
  var haystackCache = /* @__PURE__ */ new WeakMap();
22897
- function buildHaystack(g, extra) {
22897
+ function buildHaystack(t, extra) {
22898
22898
  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 ?? ""),
22899
+ t.subtype,
22900
+ t.category,
22901
+ t.policy.name,
22902
+ t.id,
22903
+ ...(t.users ?? []).map((u) => u.name),
22912
22904
  ...extra === void 0 ? [] : [extra]
22913
22905
  ].join(" ").toLowerCase();
22914
22906
  }
22915
- function groupHaystack(g) {
22916
- const cached2 = haystackCache.get(g);
22907
+ function typeHaystack(t) {
22908
+ const cached2 = haystackCache.get(t);
22917
22909
  if (cached2 !== void 0) return cached2;
22918
- const haystack = buildHaystack(g);
22919
- haystackCache.set(g, haystack);
22910
+ const haystack = buildHaystack(t);
22911
+ haystackCache.set(t, haystack);
22920
22912
  return haystack;
22921
22913
  }
22922
22914
  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;
22915
+ function typeActions(t) {
22916
+ return actionsCache.get(t) ?? [];
22929
22917
  }
22930
22918
  function countInstancesByStatus(statusInputs, statuses) {
22931
22919
  const statusSet = new Set(statuses);
@@ -22936,8 +22924,8 @@ function countInstancesByStatus(statusInputs, statuses) {
22936
22924
  }
22937
22925
  return sum;
22938
22926
  }
22939
- function applyFindingFilters(groups, opts) {
22940
- let filtered = groups;
22927
+ function applyFindingFilters(types, opts) {
22928
+ let filtered = types;
22941
22929
  if (opts.severity && opts.severity.length > 0) {
22942
22930
  const sevSet = new Set(opts.severity);
22943
22931
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -22948,7 +22936,7 @@ function applyFindingFilters(groups, opts) {
22948
22936
  }
22949
22937
  if (opts.actions && opts.actions.length > 0) {
22950
22938
  const actionSet = new Set(opts.actions);
22951
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
22939
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
22952
22940
  }
22953
22941
  if (opts.subtype && opts.subtype.length > 0) {
22954
22942
  const subtypeSet = new Set(opts.subtype);
@@ -22960,7 +22948,7 @@ function applyFindingFilters(groups, opts) {
22960
22948
  }
22961
22949
  if (opts.q) {
22962
22950
  const q = opts.q.toLowerCase();
22963
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
22951
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
22964
22952
  }
22965
22953
  return filtered;
22966
22954
  }
@@ -22975,11 +22963,11 @@ function compareFindingGroupOrder(a, b) {
22975
22963
  if (recencyDiff !== 0) return recencyDiff;
22976
22964
  return a.id.localeCompare(b.id);
22977
22965
  }
22978
- function sortFindingGroups(groups) {
22979
- return [...groups].sort(compareFindingGroupOrder);
22966
+ function sortFindingTypes(types) {
22967
+ return [...types].sort(compareFindingGroupOrder);
22980
22968
  }
22981
- function computeFindingFacets(allGroups, opts) {
22982
- const forSeverity = applyFindingFilters(allGroups, {
22969
+ function computeFindingFacets(allTypes, opts) {
22970
+ const forSeverity = applyFindingFilters(allTypes, {
22983
22971
  providers: opts.providers,
22984
22972
  actions: opts.actions,
22985
22973
  statuses: opts.statuses,
@@ -22990,7 +22978,7 @@ function computeFindingFacets(allGroups, opts) {
22990
22978
  for (const g of forSeverity) {
22991
22979
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
22992
22980
  }
22993
- const forProvider = applyFindingFilters(allGroups, {
22981
+ const forProvider = applyFindingFilters(allTypes, {
22994
22982
  actions: opts.actions,
22995
22983
  statuses: opts.statuses,
22996
22984
  q: opts.q,
@@ -23001,7 +22989,7 @@ function computeFindingFacets(allGroups, opts) {
23001
22989
  for (const g of forProvider) {
23002
22990
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
23003
22991
  }
23004
- const forAction = applyFindingFilters(allGroups, {
22992
+ const forAction = applyFindingFilters(allTypes, {
23005
22993
  providers: opts.providers,
23006
22994
  statuses: opts.statuses,
23007
22995
  q: opts.q,
@@ -23010,9 +22998,9 @@ function computeFindingFacets(allGroups, opts) {
23010
22998
  });
23011
22999
  const actionMap = /* @__PURE__ */ new Map();
23012
23000
  for (const g of forAction) {
23013
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23001
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23014
23002
  }
23015
- const forSubtype = applyFindingFilters(allGroups, {
23003
+ const forSubtype = applyFindingFilters(allTypes, {
23016
23004
  providers: opts.providers,
23017
23005
  actions: opts.actions,
23018
23006
  statuses: opts.statuses,
@@ -23021,7 +23009,7 @@ function computeFindingFacets(allGroups, opts) {
23021
23009
  });
23022
23010
  const subtypeMap = /* @__PURE__ */ new Map();
23023
23011
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
23024
- const forStatus = applyFindingFilters(allGroups, {
23012
+ const forStatus = applyFindingFilters(allTypes, {
23025
23013
  providers: opts.providers,
23026
23014
  actions: opts.actions,
23027
23015
  q: opts.q,
@@ -23069,10 +23057,20 @@ function matchesDimension(row, opts, dimension) {
23069
23057
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23070
23058
  case "tools":
23071
23059
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23060
+ // An EMPTY value is a real filter here, not an absent one. The location
23061
+ // list buckets a finding whose event recorded no repo — or no file — under
23062
+ // the empty string, and selecting that bucket has to narrow the panel to
23063
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23064
+ // row omits the key, which every call site already does.
23065
+ //
23066
+ // Reading '' as unset is what this replaced, and it failed in the one place
23067
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23068
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23069
+ // — a row reading 3 findings beside a panel listing every finding there is.
23072
23070
  case "repo":
23073
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23071
+ return opts.repo === void 0 || row.repo === opts.repo;
23074
23072
  case "file":
23075
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23073
+ return opts.file === void 0 || row.file === opts.file;
23076
23074
  case "q":
23077
23075
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23078
23076
  }
@@ -23186,6 +23184,23 @@ function addToLocation(acc, row) {
23186
23184
  acc.statuses.push(row.status);
23187
23185
  acc.ruleIds.add(row.ruleId);
23188
23186
  }
23187
+ function compareLocationOrder(a, b) {
23188
+ const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23189
+ const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23190
+ if (rankA !== rankB) return rankA - rankB;
23191
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23192
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23193
+ }
23194
+ if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23195
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23196
+ return 0;
23197
+ }
23198
+ function encodeLocationId(repo, file2) {
23199
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23200
+ }
23201
+ function encodePart(value) {
23202
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23203
+ }
23189
23204
 
23190
23205
  // ../../packages/schema/src/zod/installed-pack.ts
23191
23206
  var InstalledPack = external_exports.object({
@@ -23700,11 +23715,11 @@ var WorkspaceSettings = external_exports.object({
23700
23715
  // covers the current payload and must be re-granted.
23701
23716
  modelJudgeConsent: ModelJudgeConsent.optional(),
23702
23717
  // Records that the user consented to the DEFERRED send — the outbox — along
23703
- // with the payload shape and the endpoint they agreed to. Since payload v2
23704
- // that covers both the pre-attach backlog and undelivered captures (which
23705
- // carry prompt/reply text in `content`); the key name predates the widening.
23706
- // Absent until granted, and a grant for a different endpoint or an older
23707
- // 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.
23708
23723
  historySyncConsent: HistorySyncConsent.optional()
23709
23724
  });
23710
23725
  function defaultWorkspaceSettings() {
@@ -23835,6 +23850,9 @@ var ManagedSettingKey = external_exports.enum([
23835
23850
  "dataSharesInPlace",
23836
23851
  "redactFallback"
23837
23852
  ]).meta({ id: "ManagedSettingKey" });
23853
+ function isManagedSettingKey(value) {
23854
+ return ManagedSettingKey.safeParse(value).success;
23855
+ }
23838
23856
  var ManagedSettingsValues = external_exports.object({
23839
23857
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
23840
23858
  controlPlane: external_exports.object({
@@ -23859,7 +23877,27 @@ var ManagedSettings = external_exports.object({
23859
23877
  // Which of those the user may not change. A key here with no matching value
23860
23878
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23861
23879
  // the user may still override. The two are separable on purpose.
23862
- 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
+ };
23863
23901
  }).meta({ id: "ManagedSettings" });
23864
23902
 
23865
23903
  // ../../packages/schema/src/zod/project-files.ts
@@ -23983,7 +24021,11 @@ var FindingsTimeseriesPoint = external_exports.object({
23983
24021
  timestamp: external_exports.iso.date(),
23984
24022
  critical: external_exports.number().int().nonnegative(),
23985
24023
  high: external_exports.number().int().nonnegative(),
23986
- 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()
23987
24029
  }).meta({ id: "FindingsTimeseriesPoint" });
23988
24030
  var FindingsTimeseriesResponse = external_exports.object({
23989
24031
  range: TimeRange,
@@ -24009,6 +24051,10 @@ var ResolvedFeedItem = external_exports.object({
24009
24051
  findingKey: external_exports.string(),
24010
24052
  ruleId: external_exports.string(),
24011
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(),
24012
24058
  path: external_exports.string(),
24013
24059
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
24014
24060
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -25414,7 +25460,8 @@ var SqliteActivityRepository = class {
25414
25460
  SELECT 1 FROM audit_events d
25415
25461
  WHERE d.root_session_id = audit_events.id
25416
25462
  AND (d.content LIKE ? ESCAPE '\\'
25417
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
25463
+ OR coalesce(json_extract(d.attributes, '$.detail'),
25464
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
25418
25465
  );
25419
25466
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
25420
25467
  }
@@ -26752,23 +26799,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26752
26799
  )`;
26753
26800
 
26754
26801
  // ../../packages/persistence/src/repositories/findings.ts
26755
- var PREVIEW_INSTANCES_PER_GROUP = 200;
26756
- var DEFAULT_LOCATIONS_LIMIT = 100;
26757
- var LOCATION_RULE_IDS_CAP = 20;
26758
- function compareLocationOrder(a, b) {
26759
- return compareFindingGroupOrder(
26760
- {
26761
- severity: a.maxSeverity,
26762
- latestDetectedAt: a.latestDetectedAt,
26763
- id: ""
26764
- },
26765
- {
26766
- severity: b.maxSeverity,
26767
- latestDetectedAt: b.latestDetectedAt,
26768
- id: ""
26769
- }
26770
- );
26771
- }
26772
26802
  var CONCAT_SEP = ",";
26773
26803
  var TUPLE_SEP = "|";
26774
26804
  function splitConcat(value) {
@@ -26820,13 +26850,48 @@ function decodeGroupCursor(cursor) {
26820
26850
  return null;
26821
26851
  }
26822
26852
  function firstAfter(sorted, cursor) {
26823
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
26853
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
26824
26854
  return index === -1 ? sorted.length : index;
26825
26855
  }
26826
26856
  function findDeepLinked(sorted, page, id) {
26827
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
26828
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
26857
+ if (page.some((t) => t.id === id)) return void 0;
26858
+ return sorted.find((t) => t.id === id);
26829
26859
  }
26860
+ function encodeLocationCursor(location) {
26861
+ const payload = {
26862
+ sev: location.maxSeverity,
26863
+ t: location.latestDetectedAt,
26864
+ r: location.repo,
26865
+ f: location.file
26866
+ };
26867
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
26868
+ }
26869
+ function decodeLocationCursor(cursor) {
26870
+ const parsed = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
26871
+ if (parsed !== void 0 && typeof parsed.sev === "string" && typeof parsed.t === "string" && typeof parsed.r === "string" && typeof parsed.f === "string") {
26872
+ return { maxSeverity: parsed.sev, latestDetectedAt: parsed.t, repo: parsed.r, file: parsed.f };
26873
+ }
26874
+ return null;
26875
+ }
26876
+ function firstLocationAfter(sorted, cursor) {
26877
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
26878
+ return index === -1 ? sorted.length : index;
26879
+ }
26880
+ function findDeepLinkedLocation(sorted, page, id) {
26881
+ if (page.some((l) => l.id === id)) return void 0;
26882
+ return sorted.find((l) => l.id === id);
26883
+ }
26884
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
26885
+ d.severity AS severity, f.masked_match AS masked_match,
26886
+ f.action_taken AS action_taken, f.confidence AS confidence,
26887
+ e.started_at AS occurred_at,
26888
+ e.source_tool AS source_tool,
26889
+ e.repo AS repo,
26890
+ e.file_path AS file,
26891
+ e.tool_name AS tool_name,
26892
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
26893
+ e.event_type AS kind, f.finding_key AS finding_key,
26894
+ ${latestResolutionStatusSql("f")} AS latest_status`;
26830
26895
  var DAY_MS3 = 864e5;
26831
26896
  var SqliteFindingsRepository = class {
26832
26897
  constructor(db) {
@@ -26947,30 +27012,26 @@ var SqliteFindingsRepository = class {
26947
27012
  );
26948
27013
  }
26949
27014
  /**
26950
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
26951
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
26952
- * attributes bag, rule_id/category/severity from the definition), scoped to
26953
- * the four capture kinds (audit_events also holds structural/reconciler/scan
26954
- * rows this list must never surface), groups by ruleId, computes
26955
- * per-filter-excluded facets, applies the requested filters, and sorts by
26956
- * severity then recency. Filtering
26957
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
26958
- * reflect the full filtered set; `items` is the requested
26959
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
26960
- * filter, `totals.findings` counts only instances whose derived status was
26961
- * requested, and each item's instance preview is narrowed the same way.
27015
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
27016
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
27017
+ * list must never surface), with per-filter-excluded facets, the requested
27018
+ * filters applied, and sorted by severity then recency. Filtering and faceting
27019
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
27020
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
27021
+ * Under a `status` filter, `totals.findings` counts only findings whose
27022
+ * derived status was requested.
27023
+ *
27024
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
27025
+ * folding EVERY finding into the numbers a type row and the filters need
27026
+ * (count, severity, category, providers, actions, statuses, latest, search
27027
+ * text). The findings OF a type come from listFindingInstances scoped to
27028
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
26962
27029
  *
26963
- * Two reads, neither of which materializes a row per finding:
26964
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
26965
- * the group and the filters need (count, providers, actions, statuses,
26966
- * latest, search text);
26967
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
26968
- * populate `instances` for the table's expanded rows.
26969
27030
  * The aggregates carry raw DB values and are translated by the same
26970
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
26971
- * rule is ever restated in SQL.
27031
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
27032
+ * status rule is ever restated in SQL.
26972
27033
  */
26973
- listGroupedFindings(query) {
27034
+ listFindingTypes(query) {
26974
27035
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
26975
27036
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
26976
27037
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -26983,12 +27044,7 @@ var SqliteFindingsRepository = class {
26983
27044
  predicate,
26984
27045
  params: sessionParams
26985
27046
  });
26986
- const rows = this.previewRows(aggregates, {
26987
- sessionId: query.sessionId,
26988
- from: query.from
26989
- });
26990
- const groupable = rows.map(toFlatFindingRow);
26991
- const allGroups = buildFindingGroups(groupable, { aggregates });
27047
+ const allTypes = buildFindingTypes(aggregates);
26992
27048
  const filterOpts = {
26993
27049
  severity: query.severity,
26994
27050
  providers: query.provider,
@@ -26997,30 +27053,25 @@ var SqliteFindingsRepository = class {
26997
27053
  subtype: query.subtype,
26998
27054
  q: query.q
26999
27055
  };
27000
- const facets = computeFindingFacets(allGroups, filterOpts);
27001
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
27056
+ const facets = computeFindingFacets(allTypes, filterOpts);
27057
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
27002
27058
  const statusFilter = query.status ?? [];
27003
27059
  const totals = {
27004
- findings: sorted.reduce((acc, g) => {
27005
- if (statusFilter.length === 0) return acc + g.instanceCount;
27006
- const agg = aggregates.get(g.id);
27007
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
27060
+ findings: sorted.reduce((acc, t) => {
27061
+ if (statusFilter.length === 0) return acc + t.instanceCount;
27062
+ const agg = aggregates.get(t.id);
27063
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
27008
27064
  }, 0),
27009
- groups: sorted.length
27065
+ types: sorted.length
27010
27066
  };
27011
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
27067
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
27012
27068
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
27013
27069
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
27014
27070
  const page = sorted.slice(start, start + limit);
27015
27071
  const lastOnPage = page.at(-1);
27016
27072
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
27017
27073
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
27018
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
27019
- const narrow = (g) => statusSet ? {
27020
- ...g,
27021
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
27022
- } : g;
27023
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
27074
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
27024
27075
  return Promise.resolve({
27025
27076
  totals,
27026
27077
  facets,
@@ -27031,7 +27082,7 @@ var SqliteFindingsRepository = class {
27031
27082
  }
27032
27083
  /**
27033
27084
  * One row per rule_id, folding EVERY instance of the group into the values
27034
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
27085
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
27035
27086
  * distinct rule_ids (the installed packs' rules), not by the store's size.
27036
27087
  *
27037
27088
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -27125,13 +27176,25 @@ var SqliteFindingsRepository = class {
27125
27176
  });
27126
27177
  }
27127
27178
  /**
27128
- * The same findings folded by location: repository, then file within it.
27179
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
27129
27180
  *
27130
27181
  * The grouping keys come from the capturing event's attributes, which is what
27131
- * the local store relates a finding to — there is no finding↔asset row to
27132
- * group by instead. A repo or file the event did not record folds into the
27133
- * empty-string bucket, which the view renders but does not link, since no
27134
- * filter can name it.
27182
+ * the local store relates a finding to; there is no finding↔asset row to group
27183
+ * by instead. A repo or file the event did not record folds into the
27184
+ * empty-string bucket, which is a real location like any other: it is listed,
27185
+ * it is selectable, and its `?loc=` token is as good as any other row's.
27186
+ *
27187
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
27188
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
27189
+ * list was rebuilt to remove — and two-level pagination inside an
27190
+ * expand/collapse table is what pushed that view to master/detail in the first
27191
+ * place.
27192
+ *
27193
+ * Every filter narrows the FINDINGS and the locations fall out of what
27194
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
27195
+ * reports for the same filters scoped to that pair. The view depends on it:
27196
+ * one toolbar sits over both panels precisely because a location owns none of
27197
+ * its fields.
27135
27198
  */
27136
27199
  listFindingLocations(query) {
27137
27200
  const opts = {
@@ -27143,13 +27206,16 @@ var SqliteFindingsRepository = class {
27143
27206
  tools: query.tool,
27144
27207
  q: query.q
27145
27208
  };
27146
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
27209
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
27210
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
27147
27211
  const byRepo = /* @__PURE__ */ new Map();
27212
+ const accumulator = createInstanceFacetAccumulator(opts);
27148
27213
  let total = 0;
27149
27214
  for (const row of this.scanFindingRows({
27150
27215
  sessionId: query.sessionId,
27151
27216
  from: query.from
27152
27217
  })) {
27218
+ accumulator.add(row);
27153
27219
  if (!matchesInstanceFilters(row, opts)) continue;
27154
27220
  total += 1;
27155
27221
  let files = byRepo.get(row.repo);
@@ -27164,103 +27230,35 @@ var SqliteFindingsRepository = class {
27164
27230
  }
27165
27231
  addToLocation(acc, row);
27166
27232
  }
27167
- let fileCount = 0;
27168
- const repos = [...byRepo.entries()].map(([repo, files]) => {
27169
- fileCount += files.size;
27170
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
27171
- file: file2,
27172
- instanceCount: acc.instanceCount,
27173
- maxSeverity: acc.maxSeverity,
27174
- latestDetectedAt: acc.latestDetectedAt,
27175
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
27176
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
27177
- })).sort(compareLocationOrder);
27178
- const rollup = fileRows.reduce(
27179
- (a, f) => ({
27180
- instanceCount: a.instanceCount + f.instanceCount,
27181
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
27182
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
27183
- }),
27184
- {
27185
- instanceCount: 0,
27186
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
27187
- latestDetectedAt: ""
27188
- }
27189
- );
27190
- const statuses = fileRows.map((f) => f.status);
27191
- const folded = foldGroupStatus(statuses);
27192
- return {
27193
- repo,
27194
- instanceCount: rollup.instanceCount,
27195
- maxSeverity: rollup.maxSeverity,
27196
- latestDetectedAt: rollup.latestDetectedAt,
27197
- ...folded === void 0 ? {} : { status: folded },
27198
- files: fileRows
27199
- };
27200
- });
27201
- repos.sort(compareLocationOrder);
27233
+ const sorted = [];
27234
+ for (const [repo, files] of byRepo) {
27235
+ for (const [file2, acc] of files) {
27236
+ const status = foldGroupStatus(acc.statuses);
27237
+ sorted.push({
27238
+ id: encodeLocationId(repo, file2),
27239
+ repo,
27240
+ file: file2,
27241
+ instanceCount: acc.instanceCount,
27242
+ maxSeverity: acc.maxSeverity,
27243
+ latestDetectedAt: acc.latestDetectedAt,
27244
+ ...status === void 0 ? {} : { status },
27245
+ ruleIds: [...acc.ruleIds]
27246
+ });
27247
+ }
27248
+ }
27249
+ sorted.sort(compareLocationOrder);
27250
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
27251
+ const page = sorted.slice(start, start + limit);
27252
+ const lastOnPage = page.at(-1);
27253
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
27254
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
27202
27255
  return Promise.resolve({
27203
- totals: { findings: total, repos: repos.length, files: fileCount },
27204
- items: repos.slice(0, limit),
27205
- hasMore: repos.length > limit
27256
+ totals: { findings: total, locations: sorted.length },
27257
+ facets: accumulator.facets(),
27258
+ items: [...page, ...deepLinked ? [deepLinked] : []],
27259
+ nextCursor
27206
27260
  });
27207
27261
  }
27208
- /**
27209
- * Each group's newest instances, for the table's expanded rows.
27210
- *
27211
- * ONE index-ordered scan with early termination, and the shape is the point.
27212
- * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27213
- * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27214
- * through a temp B-tree to keep a bounded preview of each group, and then
27215
- * sorts the survivors again for the page order. Both sorts grow with the
27216
- * store while the answer does not.
27217
- *
27218
- * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27219
- * (or the session or window index the scope names — see `findingScanSql`),
27220
- * which is already the order the page wants, and keeps rows per rule until
27221
- * each rule has as many as it can show. The aggregate the caller already holds
27222
- * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27223
- * per rule, summed, is the number of rows this scan has to find, and it stops
27224
- * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27225
- * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27226
- * store with many firing rules widens it. The bound that DOES hold
27227
- * unconditionally is the sorted form's floor: this scan visits at most as
27228
- * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27229
- * sorted, and stops the moment every rule has its cap, where the sorted form
27230
- * sorts the whole scope regardless. The true worst case — the rarest rule's
27231
- * wanted instances sitting at the tail of the scope — is one pass over
27232
- * everything in scope with a block sort of the id tie-break only, never a
27233
- * sort of the scope, which is still that floor.
27234
- *
27235
- * A row whose rule the aggregate did not see is skipped: the two statements
27236
- * run without a shared snapshot, so a capture landing between them can add a
27237
- * rule here that has no counts there, and the counts are what the group is
27238
- * built from.
27239
- */
27240
- previewRows(aggregates, scope) {
27241
- const wanted = /* @__PURE__ */ new Map();
27242
- let remaining = 0;
27243
- for (const [ruleId, agg] of aggregates) {
27244
- const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27245
- wanted.set(ruleId, n);
27246
- remaining += n;
27247
- }
27248
- const rows = [];
27249
- if (remaining === 0) return rows;
27250
- const { sql, params } = this.findingScanSql(scope);
27251
- const taken = /* @__PURE__ */ new Map();
27252
- for (const r of iterateRows(this.db.prepare(sql), params)) {
27253
- const want = wanted.get(r.rule_id);
27254
- if (want === void 0) continue;
27255
- const have = taken.get(r.rule_id) ?? 0;
27256
- if (have >= want) continue;
27257
- taken.set(r.rule_id, have + 1);
27258
- rows.push(r);
27259
- remaining -= 1;
27260
- if (remaining === 0) break;
27261
- }
27262
- return rows;
27263
- }
27264
27262
  /**
27265
27263
  * Every finding in scope as a FlatFindingRow, newest first, streamed.
27266
27264
  *
@@ -27287,6 +27285,33 @@ var SqliteFindingsRepository = class {
27287
27285
  yield toFlatFindingRow(r);
27288
27286
  }
27289
27287
  }
27288
+ /**
27289
+ * One finding by its own id, or null when no such row exists.
27290
+ *
27291
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
27292
+ * the store — and, unlike anything derived from a list page, it resolves a
27293
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
27294
+ * deep link needs: the id it carries may name a finding thousands of rows
27295
+ * older than anything a first page holds.
27296
+ *
27297
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
27298
+ * RESOLVES an id; whether that row would survive the list's current filters is
27299
+ * a different question, and hiding the target because a filter excludes it is
27300
+ * worse than showing it.
27301
+ *
27302
+ * `groupId` on the result IS the rule id, so this one read answers both "which
27303
+ * type should the list select?" and "what does the drawer show?".
27304
+ */
27305
+ findingInstance(id) {
27306
+ const row = this.db.prepare(
27307
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
27308
+ FROM inspection_findings f
27309
+ JOIN audit_events e ON e.id = f.audit_event_id
27310
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27311
+ WHERE f.id = ?`
27312
+ ).get(id);
27313
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
27314
+ }
27290
27315
  /**
27291
27316
  * The one statement both instance-level scans run: every finding in scope,
27292
27317
  * joined to its event and definition, newest first.
@@ -27320,17 +27345,7 @@ var SqliteFindingsRepository = class {
27320
27345
  conditions.push("e.started_at >= ?");
27321
27346
  params.push(isoToEpochMillis(scope.from));
27322
27347
  }
27323
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27324
- d.severity AS severity, f.masked_match AS masked_match,
27325
- f.action_taken AS action_taken, f.confidence AS confidence,
27326
- e.started_at AS occurred_at,
27327
- e.source_tool AS source_tool,
27328
- e.repo AS repo,
27329
- e.file_path AS file,
27330
- e.tool_name AS tool_name,
27331
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27332
- e.event_type AS kind, f.finding_key AS finding_key,
27333
- ${latestResolutionStatusSql("f")} AS latest_status
27348
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
27334
27349
  FROM audit_events e
27335
27350
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27336
27351
  CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -27344,6 +27359,26 @@ var SqliteFindingsRepository = class {
27344
27359
  group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27345
27360
  const rows = this.db.prepare(
27346
27361
  `SELECT rule_id,
27362
+ -- BARE columns beside max(latest_at), which is deliberate and
27363
+ -- is SQLite's documented behaviour: with a single min()/max()
27364
+ -- in an aggregate query, every bare column takes its value from
27365
+ -- the row that produced the extremum. So these are the severity
27366
+ -- and category of the definition whose finding is NEWEST, which
27367
+ -- is what the row-based build they replaced read off its first
27368
+ -- (newest-first) row.
27369
+ --
27370
+ -- min() is WRONG here and was the defect: inspection_definitions
27371
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
27372
+ -- mints a new row), so a rule whose severity moved between
27373
+ -- versions has several, and min() picks the ALPHABETICALLY
27374
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
27375
+ -- That is arbitrary in direction, and it feeds the badge, the
27376
+ -- filter, the facet counts and the primary sort key.
27377
+ --
27378
+ -- Adding a second min()/max() aggregate here would make these
27379
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
27380
+ severity,
27381
+ category,
27347
27382
  sum(tuple_count) AS instance_count,
27348
27383
  max(latest_at) AS latest_at,
27349
27384
  group_concat(source_tools) AS source_tools,
@@ -27354,6 +27389,14 @@ var SqliteFindingsRepository = class {
27354
27389
  group_concat(tool_names) AS tool_names
27355
27390
  FROM (
27356
27391
  SELECT d.rule_id AS rule_id,
27392
+ -- Severity and category are columns of the DEFINITION, and
27393
+ -- a rule can have SEVERAL definitions (one per version), so
27394
+ -- these are grouped on below and resolved to the newest
27395
+ -- firing version by the outer query's bare-column select.
27396
+ -- They ride the aggregate because the type build has no rows
27397
+ -- to read them off \u2014 see buildFindingTypes.
27398
+ d.severity AS severity,
27399
+ d.category AS category,
27357
27400
  e.event_type || '${TUPLE_SEP}' ||
27358
27401
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
27359
27402
  coalesce(latest.status, '') AS status_tuple,
@@ -27368,7 +27411,7 @@ var SqliteFindingsRepository = class {
27368
27411
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27369
27412
  ON latest.finding_key = f.finding_key
27370
27413
  ${scope.predicate}
27371
- GROUP BY d.rule_id, status_tuple
27414
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
27372
27415
  )
27373
27416
  GROUP BY rule_id`
27374
27417
  ).all(scope.params);
@@ -27377,6 +27420,8 @@ var SqliteFindingsRepository = class {
27377
27420
  r.rule_id,
27378
27421
  {
27379
27422
  instanceCount: r.instance_count,
27423
+ severity: r.severity,
27424
+ category: r.category,
27380
27425
  sourceTools: splitConcat(r.source_tools),
27381
27426
  actionsTaken: splitConcat(r.actions_taken),
27382
27427
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -27393,7 +27438,7 @@ var SqliteFindingsRepository = class {
27393
27438
  latestDetectedAt: epochMillisToIso(r.latest_at),
27394
27439
  // Free text only — joined and substring-matched, so group_concat's
27395
27440
  // commas need no unpicking (a repo/path containing one still matches).
27396
- // Left undefined (not '') when unfetched, so buildFindingGroups can
27441
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
27397
27442
  // tell "no q this request" from "a group with no repo/file at all"
27398
27443
  // and skip priming a haystack nothing will read.
27399
27444
  ...withSearchText ? {
@@ -27545,6 +27590,12 @@ var SqliteHistorySyncRepository = class {
27545
27590
  this.markOwedStmt = db.prepare(
27546
27591
  `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27547
27592
  );
27593
+ this.markCaptureBacklogOwedStmt = db.prepare(
27594
+ `UPDATE audit_events SET outbox_owed = 1
27595
+ WHERE synced_at IS NULL
27596
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27597
+ AND started_at < :before`
27598
+ );
27548
27599
  this.stampStmt = db.prepare(
27549
27600
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27550
27601
  );
@@ -27593,7 +27644,9 @@ var SqliteHistorySyncRepository = class {
27593
27644
  );
27594
27645
  this.disownCapturesStmt = db.prepare(
27595
27646
  `UPDATE audit_events SET outbox_owed = NULL
27596
- WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27647
+ WHERE outbox_owed IS NOT NULL
27648
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27649
+ AND started_at < :attachedAt`
27597
27650
  );
27598
27651
  this.rearmStmt = db.prepare(
27599
27652
  `UPDATE audit_events SET synced_at = NULL
@@ -27669,6 +27722,7 @@ var SqliteHistorySyncRepository = class {
27669
27722
  freezeBoundaryStmt;
27670
27723
  captureRowsStmt;
27671
27724
  markOwedStmt;
27725
+ markCaptureBacklogOwedStmt;
27672
27726
  captureSkipCountStmt;
27673
27727
  disownCapturesStmt;
27674
27728
  partitionStmt;
@@ -27732,6 +27786,23 @@ var SqliteHistorySyncRepository = class {
27732
27786
  markCaptureOwed(id) {
27733
27787
  this.markOwedStmt.run({ id });
27734
27788
  }
27789
+ /**
27790
+ * Mark every capture already on disk as owed, as of `before`.
27791
+ *
27792
+ * The consent-time backfill, called once from `aka attach` when a human
27793
+ * grants existing-history consent — never from an ongoing drain pass, and
27794
+ * never inferred from a boundary that could later move. `before` is the
27795
+ * caller's own "now" at the moment consent was granted, so what this marks
27796
+ * is exactly the backlog the consent prompt already counted, not whatever a
27797
+ * later re-attach or key rotation might widen it to.
27798
+ *
27799
+ * Returns how many rows matched, for the caller to log or test against. Not a
27800
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
27801
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
27802
+ */
27803
+ markCaptureBacklogOwed(before) {
27804
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
27805
+ }
27735
27806
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27736
27807
  markSynced(ids, atMs) {
27737
27808
  this.stampAll(ids, atMs);
@@ -27848,15 +27919,42 @@ var SqliteHistorySyncRepository = class {
27848
27919
  *
27849
27920
  * Delivery is a fact about ONE recipient: rows sent to the deployment a
27850
27921
  * machine has just left are undelivered as far as the new one is concerned.
27851
- * All three in one transaction, so a crash between them cannot leave stamps
27852
- * attributed to the wrong deployment, or a boundary that belongs to another.
27922
+ * All four in one transaction, so a crash between them cannot leave stamps
27923
+ * attributed to the wrong deployment, a boundary that belongs to another, or
27924
+ * a disown with no re-mark to follow it.
27853
27925
  *
27854
27926
  * The boundary is written HERE and only here, which is what freezes it: a
27855
27927
  * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
27856
27928
  * unchanged, so this never runs and the backlog does not widen back over rows
27857
27929
  * the live path has since delivered.
27930
+ *
27931
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
27932
+ * granted existing-history consent for the deployment this call is arming —
27933
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
27934
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
27935
+ * apart. Passed only when that grant is valid, since this method has no way
27936
+ * to check consent itself and must not mark a row owed for a machine that
27937
+ * never agreed to it. Applied AFTER the disown above, in the SAME
27938
+ * transaction: what the disown clears is every marker below `backlogBefore`,
27939
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
27940
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
27941
+ * on the cleared side of that bound — and the re-mark in the same
27942
+ * transaction is what puts those rows back. A crash between the two cannot
27943
+ * strand the ledger disowned with nothing re-marked — the transaction either
27944
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
27945
+ * committed re-enters this method on the very next pass. Omit it (the
27946
+ * structural-only tests do) to exercise the disown in isolation.
27947
+ *
27948
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
27949
+ * touching a marker the NEW deployment's OWN live path has already set: B's
27950
+ * live path can mark a capture owed from the moment `aka attach` writes the
27951
+ * descriptor, before the drain's first pass ever reaches this method, and
27952
+ * such a row sits at or after the bound rather than below it. What keeps the
27953
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
27954
+ * bound — disown runs first, re-mark second, both inside the one
27955
+ * transaction above.
27858
27956
  */
27859
- rearmFor(fingerprint, backlogBefore) {
27957
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
27860
27958
  this.ensureRowStmt.run();
27861
27959
  withTransaction(
27862
27960
  this.db,
@@ -27864,7 +27962,10 @@ var SqliteHistorySyncRepository = class {
27864
27962
  const previous = getRow(this.fingerprintStmt)?.fingerprint;
27865
27963
  this.rearmStmt.run();
27866
27964
  if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27867
- this.disownCapturesStmt.run();
27965
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
27966
+ }
27967
+ if (backfillCapturesBefore !== void 0) {
27968
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
27868
27969
  }
27869
27970
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27870
27971
  },
@@ -28137,7 +28238,8 @@ function managedSettingsPaths(platform2 = process.platform) {
28137
28238
  }
28138
28239
  return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28139
28240
  }
28140
- function readManagedSettings(paths = managedSettingsPaths()) {
28241
+ var testOnlyManagedPaths = null;
28242
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
28141
28243
  for (const path of paths) {
28142
28244
  let text;
28143
28245
  try {
@@ -30617,7 +30719,7 @@ function toUtcDateString(ms) {
30617
30719
  return new Date(ms).toISOString().slice(0, 10);
30618
30720
  }
30619
30721
  function isTimeseriesSeverity(s) {
30620
- return s === "critical" || s === "high" || s === "medium";
30722
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
30621
30723
  }
30622
30724
  var SqliteSecurityRepository = class {
30623
30725
  constructor(db, now = () => Date.now()) {
@@ -30746,12 +30848,16 @@ var SqliteSecurityRepository = class {
30746
30848
  const now = this.now();
30747
30849
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
30748
30850
  const rows = this.findingsInRange(windowStart, now);
30749
- const points = Array.from({ length: numBuckets }, (_, i) => ({
30750
- timestamp: toUtcDateString(windowStart + i * bucketMs),
30751
- critical: 0,
30752
- high: 0,
30753
- medium: 0
30754
- }));
30851
+ const points = Array.from(
30852
+ { length: numBuckets },
30853
+ (_, i) => ({
30854
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
30855
+ critical: 0,
30856
+ high: 0,
30857
+ medium: 0,
30858
+ low: 0
30859
+ })
30860
+ );
30755
30861
  for (const r of rows) {
30756
30862
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
30757
30863
  const bucket = points[idx];
@@ -30969,6 +31075,7 @@ var SqliteSecurityRepository = class {
30969
31075
  `SELECT f.finding_key AS finding_key,
30970
31076
  d.rule_id AS rule_id,
30971
31077
  d.severity AS severity,
31078
+ e.repo AS repo,
30972
31079
  e.file_path AS path,
30973
31080
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
30974
31081
  latest.resolved_at AS latest_resolved_at
@@ -30988,6 +31095,7 @@ var SqliteSecurityRepository = class {
30988
31095
  const items = rows.map((r) => ({
30989
31096
  findingKey: r.finding_key,
30990
31097
  ruleId: r.rule_id,
31098
+ repo: r.repo ?? "",
30991
31099
  severity: r.severity,
30992
31100
  path: r.path ?? "",
30993
31101
  resolvedAt: new Date(r.latest_resolved_at).toISOString(),
@@ -30997,13 +31105,66 @@ var SqliteSecurityRepository = class {
30997
31105
  }));
30998
31106
  return Promise.resolve({ items });
30999
31107
  }
31108
+ /**
31109
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
31110
+ *
31111
+ * Scoped by status rather than by time, because the card this feeds is a to-do
31112
+ * list: a secret committed three weeks ago and never rotated is still the most
31113
+ * important thing to fix, and any window hides it. It carried a "newest N
31114
+ * findings" cap and then a range; the first meant a different span on every
31115
+ * machine, and the second reported "no recommendations" over live exposure.
31116
+ *
31117
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
31118
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
31119
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
31120
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
31121
+ * The two answer different questions and only this one has to match a link.
31122
+ *
31123
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
31124
+ * whole-store scope costs a grouped scan rather than a row per finding.
31125
+ */
31126
+ recommendationInputs() {
31127
+ const rows = allRows(
31128
+ this.db.prepare(
31129
+ `SELECT d.rule_id AS rule_id,
31130
+ d.category AS category,
31131
+ d.severity AS severity,
31132
+ COUNT(*) AS count
31133
+ FROM inspection_findings f
31134
+ JOIN audit_events e ON e.id = f.audit_event_id
31135
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31136
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31137
+ ON latest.finding_key = f.finding_key
31138
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31139
+ AND e.event_type = 'code_change'
31140
+ AND (
31141
+ f.finding_key IS NULL
31142
+ OR latest.status IS NULL
31143
+ OR latest.status NOT IN ('resolved', 'dismissed')
31144
+ )
31145
+ GROUP BY d.rule_id, d.category, d.severity`
31146
+ )
31147
+ );
31148
+ return Promise.resolve(
31149
+ rows.map((r) => ({
31150
+ ruleId: r.rule_id,
31151
+ category: r.category,
31152
+ severity: r.severity,
31153
+ count: r.count
31154
+ }))
31155
+ );
31156
+ }
31000
31157
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
31001
31158
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
31002
31159
  // numeric and the JS aggregations bucket/split on ms directly.
31003
31160
  findingsInRange(fromMs, toMs) {
31004
31161
  const rows = allRows(
31005
31162
  this.db.prepare(
31006
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
31163
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
31164
+ // joined for `severity`, so they are two more columns off a row this read
31165
+ // already fetches. They feed the recommended-actions rollup.
31166
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31167
+ d.rule_id AS rule_id, d.category AS category
31007
31168
  FROM inspection_findings f
31008
31169
  JOIN audit_events e ON e.id = f.audit_event_id
31009
31170
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -31016,7 +31177,9 @@ var SqliteSecurityRepository = class {
31016
31177
  return rows.map((r) => ({
31017
31178
  occurredAt: r.occurred_at,
31018
31179
  severity: r.severity,
31019
- actionTaken: r.action_taken
31180
+ actionTaken: r.action_taken,
31181
+ ruleId: r.rule_id,
31182
+ category: r.category
31020
31183
  }));
31021
31184
  }
31022
31185
  };
@@ -32186,23 +32349,30 @@ function openLocalDatabase(dir) {
32186
32349
  };
32187
32350
  }
32188
32351
 
32189
- // ../../packages/persistence/src/finding-key.ts
32352
+ // ../../packages/persistence/src/egress-wire.ts
32190
32353
  import { createHash as createHash3 } from "crypto";
32191
32354
 
32355
+ // ../../packages/persistence/src/finding-key.ts
32356
+ import { createHash as createHash4 } from "crypto";
32357
+
32192
32358
  // ../../packages/persistence/src/fingerprint.ts
32193
32359
  import { createHmac, randomBytes } from "crypto";
32194
32360
  import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
32195
32361
  import { join as join8 } from "path";
32196
32362
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
32197
32363
 
32198
- // ../../packages/persistence/src/history-preview.ts
32364
+ // ../../packages/persistence/src/history-backfill.ts
32199
32365
  import { existsSync as existsSync4 } from "fs";
32200
32366
  import { join as join9 } from "path";
32367
+
32368
+ // ../../packages/persistence/src/history-preview.ts
32369
+ import { existsSync as existsSync5 } from "fs";
32370
+ import { join as join10 } from "path";
32201
32371
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32202
32372
 
32203
32373
  // ../../packages/persistence/src/store-symlinks.ts
32204
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32205
- import { dirname as dirname3, join as join10, resolve } from "path";
32374
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32375
+ import { dirname as dirname3, join as join11, resolve } from "path";
32206
32376
 
32207
32377
  // ../../packages/persistence/src/vault/crypto.ts
32208
32378
  import {
@@ -32217,18 +32387,18 @@ import {
32217
32387
  import { execFileSync } from "child_process";
32218
32388
  import { randomBytes as randomBytes2 } from "crypto";
32219
32389
  import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32220
- import { join as join11 } from "path";
32390
+ import { join as join12 } from "path";
32221
32391
 
32222
32392
  // ../../packages/persistence/src/vault/vault.ts
32223
32393
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32224
32394
 
32225
32395
  // ../../packages/persistence/src/warn-era-cap.ts
32226
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32227
- import { join as join12 } from "path";
32396
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
32397
+ import { join as join13 } from "path";
32228
32398
 
32229
32399
  // ../../packages/plugin-sdk/src/config.ts
32230
- import { existsSync as existsSync7 } from "fs";
32231
- import { join as join13 } from "path";
32400
+ import { existsSync as existsSync8 } from "fs";
32401
+ import { join as join14 } from "path";
32232
32402
 
32233
32403
  // ../../packages/plugin-sdk/src/provider-env.ts
32234
32404
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32282,8 +32452,8 @@ function resolveProvider() {
32282
32452
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32283
32453
  try {
32284
32454
  ensureLayoutDirSync(base);
32285
- const settingsFile = join13(settingsDir(base), "settings.json");
32286
- if (existsSync7(settingsFile)) tightenFile(settingsFile);
32455
+ const settingsFile = join14(settingsDir(base), "settings.json");
32456
+ if (existsSync8(settingsFile)) tightenFile(settingsFile);
32287
32457
  } catch {
32288
32458
  }
32289
32459
  migrateLegacyLayout(base);
@@ -32308,7 +32478,7 @@ function resolveProviderSafe(resolveProviderFn) {
32308
32478
  // ../../packages/plugin-sdk/src/config-inventory.ts
32309
32479
  import { readdirSync as readdirSync2, readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32310
32480
  import { homedir as homedir2 } from "os";
32311
- import { basename as basename3, join as join15 } from "path";
32481
+ import { basename as basename3, join as join16 } from "path";
32312
32482
 
32313
32483
  // ../../packages/detections/src/egress/registry.ts
32314
32484
  var EXTRACTOR_VERSION = "1";
@@ -35343,24 +35513,20 @@ function maskText(text) {
35343
35513
  }
35344
35514
 
35345
35515
  // ../../packages/plugin-sdk/src/repo.ts
35346
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
35347
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join14, sep as sep2 } from "path";
35516
+ import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
35517
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
35348
35518
 
35349
35519
  // ../../packages/plugin-sdk/src/events.ts
35350
- import { createHash as createHash4, randomUUID as randomUUID13 } from "crypto";
35520
+ import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
35351
35521
 
35352
35522
  // ../../packages/plugin-sdk/src/isolated-scan.ts
35353
- import { existsSync as existsSync9 } from "fs";
35523
+ import { existsSync as existsSync10 } from "fs";
35354
35524
  import { fileURLToPath } from "url";
35355
35525
  import { Worker } from "worker_threads";
35356
35526
 
35357
- // ../../packages/plugin-sdk/src/ignore-layers.ts
35358
- var import_ignore = __toESM(require_ignore(), 1);
35359
- import { readFileSync as readFileSync10 } from "fs";
35360
- import { join as join16 } from "path";
35361
-
35362
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
35363
- import { arch, hostname as hostname4, platform, release } from "os";
35527
+ // ../../packages/plugin-sdk/src/host-floor.ts
35528
+ import { readFileSync as readFileSync11 } from "fs";
35529
+ import { join as join18 } from "path";
35364
35530
 
35365
35531
  // ../../packages/plugin-sdk/src/model-governance.ts
35366
35532
  import {
@@ -35368,16 +35534,42 @@ import {
35368
35534
  fstatSync,
35369
35535
  mkdirSync as mkdirSync2,
35370
35536
  openSync as openSync2,
35371
- readFileSync as readFileSync11,
35537
+ readFileSync as readFileSync10,
35372
35538
  readSync,
35373
35539
  writeFileSync as writeFileSync5
35374
35540
  } from "fs";
35375
35541
  import { join as join17 } from "path";
35376
35542
  var TAIL_BYTES = 256 * 1024;
35377
35543
 
35544
+ // ../../packages/plugin-sdk/src/host-floor.ts
35545
+ var HOST_FEATURE = {
35546
+ ModelSwitch: "model-switch",
35547
+ VaultPointerDisplay: "vault-pointer-display"
35548
+ };
35549
+ var HOST_FLOORS = {
35550
+ [HOST_FEATURE.ModelSwitch]: {
35551
+ label: "model-switch protection",
35552
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
35553
+ since: "2.1.251"
35554
+ },
35555
+ [HOST_FEATURE.VaultPointerDisplay]: {
35556
+ label: "vault pointer display",
35557
+ hookEvents: ["MessageDisplay"],
35558
+ since: "2.1.152"
35559
+ }
35560
+ };
35561
+
35562
+ // ../../packages/plugin-sdk/src/ignore-layers.ts
35563
+ var import_ignore = __toESM(require_ignore(), 1);
35564
+ import { readFileSync as readFileSync12 } from "fs";
35565
+ import { join as join19 } from "path";
35566
+
35567
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
35568
+ import { arch, hostname as hostname4, platform, release } from "os";
35569
+
35378
35570
  // ../../packages/plugin-sdk/src/nudge.ts
35379
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
35380
- import { join as join18 } from "path";
35571
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
35572
+ import { join as join20 } from "path";
35381
35573
 
35382
35574
  // ../../packages/plugin-sdk/src/paths.ts
35383
35575
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
@@ -35394,8 +35586,8 @@ function applyCategoryPosture(posture, repo, mode = "fill-gaps") {
35394
35586
  }
35395
35587
 
35396
35588
  // ../../packages/plugin-sdk/src/project-files.ts
35397
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
35398
- import { basename as basename5, join as join19 } from "path";
35589
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
35590
+ import { basename as basename5, join as join21 } from "path";
35399
35591
 
35400
35592
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
35401
35593
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -35513,11 +35705,11 @@ async function applySetupTriageSuppressions(entries, writer, opts) {
35513
35705
 
35514
35706
  // ../../packages/plugin-sdk/src/throttle.ts
35515
35707
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
35516
- import { join as join20 } from "path";
35708
+ import { join as join22 } from "path";
35517
35709
 
35518
35710
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
35519
35711
  import { writeFileSync as writeFileSync8 } from "fs";
35520
- import { join as join21 } from "path";
35712
+ import { join as join23 } from "path";
35521
35713
 
35522
35714
  // ../../packages/setup-wizard/src/triage/dedupe.ts
35523
35715
  function dedupeKey(hit) {
@@ -35572,12 +35764,12 @@ function deriveFalsePositivePatterns(hits, rec, plan) {
35572
35764
  }
35573
35765
 
35574
35766
  // ../../packages/setup-wizard/src/triage/gate-display.ts
35575
- function findContext(entry, join25) {
35576
- const byFingerprint = join25.find(
35767
+ function findContext(entry, join27) {
35768
+ const byFingerprint = join27.find(
35577
35769
  (j) => j.valueFingerprint !== void 0 && j.valueFingerprint === entry.valueFingerprint
35578
35770
  );
35579
35771
  if (byFingerprint) return byFingerprint.maskedContext;
35580
- const byRuleAndMask = join25.find(
35772
+ const byRuleAndMask = join27.find(
35581
35773
  (j) => j.ruleId === entry.ruleId && j.maskedMatch === entry.maskedValue
35582
35774
  );
35583
35775
  return byRuleAndMask?.maskedContext;
@@ -35648,13 +35840,13 @@ function renderShowcase(showcase) {
35648
35840
 
35649
35841
  ${blocks.join("\n\n")}`;
35650
35842
  }
35651
- function renderSuppressionGate(entries, join25) {
35843
+ function renderSuppressionGate(entries, join27) {
35652
35844
  if (entries.length === 0) {
35653
35845
  return "No false-positive suppressions to confirm \u2014 nothing will be written.";
35654
35846
  }
35655
35847
  const header = entries.length === 1 ? "This looks like a false positive \u2014 take a look before I suppress it:" : `These ${String(entries.length)} look like false positives \u2014 take a look before I suppress them:`;
35656
35848
  const blocks = entries.map((entry, i) => {
35657
- const context = findContext(entry, join25);
35849
+ const context = findContext(entry, join27);
35658
35850
  const lines = [
35659
35851
  `${String(i + 1)}. ${entry.ruleId} [${entry.category}]`,
35660
35852
  ` value: ${entry.maskedValue}`,
@@ -35740,9 +35932,9 @@ function mergeRecommendations(verdicts) {
35740
35932
  }
35741
35933
 
35742
35934
  // ../../packages/setup-wizard/src/triage/plan-file.ts
35743
- import { mkdtempSync, readFileSync as readFileSync13, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
35935
+ import { mkdtempSync, readFileSync as readFileSync14, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
35744
35936
  import { tmpdir } from "os";
35745
- import { basename as basename6, dirname as dirname6, join as join22 } from "path";
35937
+ import { basename as basename6, dirname as dirname6, join as join24 } from "path";
35746
35938
  var SuppressionEntrySchema = external_exports.object({
35747
35939
  ruleId: external_exports.string(),
35748
35940
  category: DetectionCategory,
@@ -35797,13 +35989,13 @@ function serializePlan(plan, current) {
35797
35989
  function writePlanFile(plan, current, rawValues, deps = {}) {
35798
35990
  const serialized = serializePlan(plan, current);
35799
35991
  assertRawFree(serialized, rawValues);
35800
- const dir = (deps.mkTempDir ?? (() => mkdtempSync(join22(tmpdir(), "aka-plan-"))))();
35801
- const path = join22(dir, "setup-plan.json");
35992
+ const dir = (deps.mkTempDir ?? (() => mkdtempSync(join24(tmpdir(), "aka-plan-"))))();
35993
+ const path = join24(dir, "setup-plan.json");
35802
35994
  writeFileSync9(path, serialized, { encoding: "utf8", mode: 384 });
35803
35995
  return path;
35804
35996
  }
35805
35997
  function readPlanFile(path) {
35806
- const text = readFileSync13(path, "utf8");
35998
+ const text = readFileSync14(path, "utf8");
35807
35999
  const json2 = JSON.parse(text);
35808
36000
  return PersistedPlanSchema.parse(json2);
35809
36001
  }
@@ -35877,8 +36069,8 @@ function buildJoinEntries(hits) {
35877
36069
  }
35878
36070
 
35879
36071
  // ../../packages/setup-wizard/src/triage/resolve.ts
35880
- function resolveSuppressions(rec, join25) {
35881
- const byId = new Map(join25.map((e) => [e.id, e]));
36072
+ function resolveSuppressions(rec, join27) {
36073
+ const byId = new Map(join27.map((e) => [e.id, e]));
35882
36074
  const entries = [];
35883
36075
  const skipped = [];
35884
36076
  for (const cat of rec.perCategory) {
@@ -35980,7 +36172,7 @@ function parseTriageStream(text) {
35980
36172
  return { hits, status: "complete" };
35981
36173
  }
35982
36174
  function planTriageWriteback(hits, rec) {
35983
- const join25 = buildJoinEntries(hits);
36175
+ const join27 = buildJoinEntries(hits);
35984
36176
  const rawValues = hits.map((h) => h.rawMatch);
35985
36177
  const skipped = [];
35986
36178
  const posture = {};
@@ -36020,7 +36212,7 @@ function planTriageWriteback(hits, rec) {
36020
36212
  }
36021
36213
  const { entries, skipped: resolveSkips } = resolveSuppressions(
36022
36214
  { perCategory: safeCategories, notes: rec.notes },
36023
- join25
36215
+ join27
36024
36216
  );
36025
36217
  skipped.push(...resolveSkips);
36026
36218
  let notes = rec.notes;
@@ -36030,7 +36222,7 @@ function planTriageWriteback(hits, rec) {
36030
36222
  if (err instanceof RawEgressError) notes = SCRUBBED_NOTES;
36031
36223
  else throw err;
36032
36224
  }
36033
- return { entries, posture, showcase, join: join25, notes, skipped };
36225
+ return { entries, posture, showcase, join: join27, notes, skipped };
36034
36226
  }
36035
36227
  function recommendedPosture(evidence) {
36036
36228
  return { ...severityFloorPosture(), ...evidence };
@@ -36299,9 +36491,9 @@ function parseRecommendation(text) {
36299
36491
 
36300
36492
  // src/triage/judge.ts
36301
36493
  import { execFileSync as execFileSync2 } from "child_process";
36302
- import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync8 } from "fs";
36494
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync15, rmSync as rmSync8 } from "fs";
36303
36495
  import { tmpdir as tmpdir2 } from "os";
36304
- import { dirname as dirname7, join as join23 } from "path";
36496
+ import { dirname as dirname7, join as join25 } from "path";
36305
36497
  import { fileURLToPath as fileURLToPath2 } from "url";
36306
36498
 
36307
36499
  // ../../packages/plugin-sdk/src/bare-command.ts
@@ -36413,7 +36605,7 @@ function planBareCommand(command, args, deps = {}) {
36413
36605
 
36414
36606
  // src/triage/judge.ts
36415
36607
  var TRIAGE_DIR = dirname7(fileURLToPath2(import.meta.url));
36416
- var DEFAULT_RUBRIC_PATH = join23(
36608
+ var DEFAULT_RUBRIC_PATH = join25(
36417
36609
  TRIAGE_DIR,
36418
36610
  "..",
36419
36611
  "..",
@@ -36450,7 +36642,7 @@ function judgeEnv(platform2 = process.platform) {
36450
36642
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
36451
36643
  };
36452
36644
  if (platform2 === "darwin") {
36453
- env.CLAUDE_CONFIG_DIR = mkdtempSync2(join23(tmpdir2(), "aka-judge-cfg-"));
36645
+ env.CLAUDE_CONFIG_DIR = mkdtempSync2(join25(tmpdir2(), "aka-judge-cfg-"));
36454
36646
  }
36455
36647
  return env;
36456
36648
  }
@@ -36485,7 +36677,7 @@ function runJudge(hits, deps) {
36485
36677
  if (typeof deps.spawn !== "function") {
36486
36678
  throw new TypeError("runJudge requires deps.spawn \u2014 there is no live-spawn fallback");
36487
36679
  }
36488
- const rubric = deps.loadRubric?.() ?? readFileSync14(DEFAULT_RUBRIC_PATH, "utf8");
36680
+ const rubric = deps.loadRubric?.() ?? readFileSync15(DEFAULT_RUBRIC_PATH, "utf8");
36489
36681
  const hitsJsonl = hits.map((h) => JSON.stringify(toJudgePayload(h))).join("\n");
36490
36682
  const fullPrompt = `${rubric}
36491
36683
 
@@ -36755,10 +36947,10 @@ function resolveCreatedBy() {
36755
36947
  }
36756
36948
  function loadRubric() {
36757
36949
  const here = dirname8(fileURLToPath4(import.meta.url));
36758
- const shipped = join24(here, "triage-rubric.md");
36759
- if (existsSync11(shipped)) return readFileSync15(shipped, "utf8");
36760
- return readFileSync15(
36761
- join24(here, "..", "..", "..", "packages", "setup-wizard", "assets", "triage-rubric.md"),
36950
+ const shipped = join26(here, "triage-rubric.md");
36951
+ if (existsSync12(shipped)) return readFileSync16(shipped, "utf8");
36952
+ return readFileSync16(
36953
+ join26(here, "..", "..", "..", "packages", "setup-wizard", "assets", "triage-rubric.md"),
36762
36954
  "utf8"
36763
36955
  );
36764
36956
  }
@@ -36768,7 +36960,7 @@ async function main() {
36768
36960
  argv,
36769
36961
  // fd 0 = stdin; the wizard pipes `backfill.js --triage` into this on preview.
36770
36962
  // Called only on the preview path — the confirm path never reads a stream.
36771
- readStream: (streamPath) => streamPath !== void 0 ? readFileSync15(streamPath, "utf8") : readFileSync15(0, "utf8"),
36963
+ readStream: (streamPath) => streamPath !== void 0 ? readFileSync16(streamPath, "utf8") : readFileSync16(0, "utf8"),
36772
36964
  runJudge: (hits) => runJudge(hits, { spawn: spawnClaude, loadRubric }),
36773
36965
  // The distinct model-judge egress consent, read from settings.json. When it
36774
36966
  // is absent or stale the preview skips the judge instead of sending findings