@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.
@@ -20420,6 +20420,94 @@ function epochMillisToIso(ms) {
20420
20420
  return new Date(ms).toISOString();
20421
20421
  }
20422
20422
 
20423
+ // ../../packages/schema/src/security/recommendations.ts
20424
+ var SEVERITY_WEIGHT = {
20425
+ critical: 4,
20426
+ high: 3,
20427
+ medium: 2,
20428
+ low: 1
20429
+ };
20430
+ var SEVERITY_WEIGHT_BY_STRING = SEVERITY_WEIGHT;
20431
+ function severityWeight(severity) {
20432
+ return SEVERITY_WEIGHT_BY_STRING[severity] ?? 0;
20433
+ }
20434
+ var ADVICE = {
20435
+ secret: "Rotate the exposed credentials and move them out of prompts (secrets manager / env vars).",
20436
+ pii: "Remove or mask personal data before it reaches the model.",
20437
+ financial: "Strip card and account numbers; share only non-sensitive references.",
20438
+ phi: "Remove protected health information \u2014 it should never reach an external model.",
20439
+ code_context: "Confirm this proprietary code context is safe to share.",
20440
+ code_flaw: "Review the flagged pattern and apply the secure alternative (parameterized queries, safe deserializers, etc.).",
20441
+ config: "Review the setting \u2014 a hook conflict or an egress change applies to every session that follows.",
20442
+ custom: "Review against your organization\u2019s custom policy."
20443
+ };
20444
+ var REC_TEMPLATE = {
20445
+ secret: { title: "Exposed secret detected", action: "Rotate" },
20446
+ pii: { title: "Personal data in a prompt", action: "Remove" },
20447
+ financial: { title: "Financial data detected", action: "Strip" },
20448
+ phi: { title: "Health information detected", action: "Remove" },
20449
+ code_context: { title: "Proprietary code shared", action: "Review" },
20450
+ code_flaw: { title: "Insecure code pattern", action: "Fix" },
20451
+ config: { title: "Weakened configuration", action: "Review" },
20452
+ custom: { title: "Custom policy match", action: "Review" }
20453
+ };
20454
+ var MAX_RECOMMENDATIONS = 10;
20455
+ function bucketizeRecommendations(findings) {
20456
+ const byRule = /* @__PURE__ */ new Map();
20457
+ const buckets = /* @__PURE__ */ new Map();
20458
+ for (const f of findings) {
20459
+ const n = f.count ?? 1;
20460
+ byRule.set(f.ruleId, (byRule.get(f.ruleId) ?? 0) + n);
20461
+ const b = buckets.get(f.category) ?? {
20462
+ category: f.category,
20463
+ count: 0,
20464
+ categoryCount: 0,
20465
+ severity: f.severity,
20466
+ weight: 0,
20467
+ ruleId: f.ruleId
20468
+ };
20469
+ b.categoryCount += n;
20470
+ const w = severityWeight(f.severity);
20471
+ if (w > b.weight) {
20472
+ b.weight = w;
20473
+ b.severity = f.severity;
20474
+ b.ruleId = f.ruleId;
20475
+ }
20476
+ buckets.set(f.category, b);
20477
+ }
20478
+ for (const b of buckets.values()) b.count = byRule.get(b.ruleId) ?? 0;
20479
+ return [...buckets.values()].sort((a, b) => b.weight - a.weight || b.categoryCount - a.categoryCount).slice(0, MAX_RECOMMENDATIONS);
20480
+ }
20481
+ function buildRecommendations(findings) {
20482
+ return bucketizeRecommendations(findings).map((b) => {
20483
+ const copy = recommendationCopy(b.category);
20484
+ return {
20485
+ severity: b.severity,
20486
+ title: copy.title,
20487
+ description: copy.advice,
20488
+ context: `${b.ruleId} \xB7 ${String(b.count)} finding${b.count === 1 ? "" : "s"}`,
20489
+ action: copy.action
20490
+ };
20491
+ });
20492
+ }
20493
+ function recommendationCopy(category) {
20494
+ const template = REC_TEMPLATE_BY_STRING[category] ?? {
20495
+ title: `${category} finding`,
20496
+ action: "Review"
20497
+ };
20498
+ return {
20499
+ ...template,
20500
+ advice: ADVICE_BY_STRING[category] ?? "Review this finding against your policy."
20501
+ };
20502
+ }
20503
+ var ADVICE_BY_STRING = ADVICE;
20504
+ var REC_TEMPLATE_BY_STRING = REC_TEMPLATE;
20505
+ function healthScore(summary) {
20506
+ const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
20507
+ const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
20508
+ return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
20509
+ }
20510
+
20423
20511
  // ../../packages/schema/src/token/cost-model.ts
20424
20512
  var PROVIDER_PLATFORM = /* @__PURE__ */ new Map([
20425
20513
  ["anthropic", "anthropic"],
@@ -20739,13 +20827,11 @@ var FindingGroup = external_exports.object({
20739
20827
  latestDetectedAt: external_exports.iso.datetime(),
20740
20828
  instances: external_exports.array(FindingInstance),
20741
20829
  // Derived from instances' statuses with open-dominates precedence (see
20742
- // buildFindingGroups). Undefined only when no instance carries a status.
20830
+ // foldGroupStatus). Undefined only when no instance carries a status.
20743
20831
  status: FindingStatus.optional(),
20744
- // The distinct people across the WHOLE group, not just the `instances`
20745
- // preview — from the store's whole-group aggregate when it supplies one,
20746
- // else folded from the rows (see buildFindingGroups). Undefined when no
20747
- // instance carries a user, or when the store supplied whole-group folds
20748
- // without one.
20832
+ // The distinct people across the WHOLE group, not just the instances
20833
+ // carried here. Undefined when no instance carries a user, or when the
20834
+ // store supplied whole-group folds without one.
20749
20835
  users: external_exports.array(FindingUser).optional()
20750
20836
  }).meta({ id: "FindingGroup" });
20751
20837
  var FindingStats = external_exports.object({
@@ -20774,21 +20860,31 @@ var FindingFacets = external_exports.object({
20774
20860
  // counted under no value.
20775
20861
  status: external_exports.array(FindingFacetItem),
20776
20862
  // Host tool (attributes.tool_name). Present only on the instance-level
20777
- // reads, which can filter by it; the grouped read omits the dimension
20863
+ // reads, which can filter by it; the type-level read omits the dimension
20778
20864
  // because a group spans tools.
20779
20865
  tool: external_exports.array(FindingFacetItem).optional()
20780
20866
  }).meta({ id: "FindingFacets" });
20781
- var DEFAULT_GROUPED_FINDINGS_LIMIT = 50;
20782
- var ListGroupedFindingsQuery = external_exports.object({
20867
+ var FindingTypeSummary = FindingGroup.omit({ instances: true, match: true }).meta({
20868
+ id: "FindingTypeSummary"
20869
+ });
20870
+ var DEFAULT_FINDING_TYPES_LIMIT = 50;
20871
+ var MAX_FINDING_TYPES_LIMIT = 100;
20872
+ var ListFindingTypesQuery = external_exports.object({
20783
20873
  // NOTE: severity filters by Severity (critical/high/medium/low), not by
20784
- // FindingAction.
20874
+ // FindingAction. It narrows TYPES: a type's severity is the one its newest
20875
+ // firing version carries, and this list pages types.
20876
+ //
20877
+ // That is NOT a claim the findings of a type share it. A rule can hold several
20878
+ // definition versions at different severities, so a type kept by this filter
20879
+ // can hold findings that individually do not match — see totals.findings on
20880
+ // ListFindingTypesResponse, which counts them all.
20785
20881
  severity: external_exports.array(Severity).optional(),
20786
20882
  subtype: external_exports.array(external_exports.string()).optional(),
20787
20883
  provider: external_exports.array(FindingProvider).optional(),
20788
20884
  action: external_exports.array(FindingAction).optional(),
20789
- // Matches a group's DERIVED status (see FindingGroup.status), not its
20790
- // individual instances' — so a filtered group's Status column always reads
20791
- // one of the requested values.
20885
+ // Matches a type's DERIVED status (see FindingGroup.status), not its
20886
+ // individual findings' — so a filtered row's status always reads one of the
20887
+ // requested values.
20792
20888
  status: external_exports.array(FindingStatus).optional(),
20793
20889
  q: external_exports.string().optional(),
20794
20890
  // Scope to findings whose event carries this session id (the Activity page's
@@ -20798,23 +20894,37 @@ var ListGroupedFindingsQuery = external_exports.object({
20798
20894
  // from a time-scoped page (Activity's range) can carry that scope. Absent
20799
20895
  // means all time — this list has no default window.
20800
20896
  from: external_exports.iso.datetime().optional(),
20801
- // A group or instance id that must appear in the page even when the cursor
20802
- // has already advanced past its sort position. This is what keeps the
20803
- // Findings page's one-shot ?finding= deep link resolving once the list
20804
- // paginates: the target group is appended out of sort order rather than
20805
- // scanning forward for it. Never affects totals, facets or the cursor.
20897
+ // A RULE id that must appear in the page even when the cursor has already
20898
+ // advanced past its sort position. This is what keeps the selected type
20899
+ // visible in the list once it paginates: the target is appended out of sort
20900
+ // order rather than scanned forward for. Never affects totals, facets or the
20901
+ // cursor. Unlike the grouped read this replaces, it names a rule only — an
20902
+ // instance id is resolved by `findingInstance`, which is a primary-key seek
20903
+ // and so is not bounded by what any page happens to hold.
20806
20904
  includeId: external_exports.string().optional(),
20807
- groupBy: external_exports.literal("type").optional(),
20808
- limit: external_exports.coerce.number().int().min(1).max(100).optional(),
20905
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_TYPES_LIMIT).optional(),
20809
20906
  cursor: external_exports.string().optional()
20810
20907
  });
20811
- var ListGroupedFindingsResponse = external_exports.object({
20908
+ var ListFindingTypesResponse = external_exports.object({
20812
20909
  totals: external_exports.object({
20910
+ // Findings belonging to the matching TYPES — not findings that each match
20911
+ // the filters. The filters here select types, so a type that survives
20912
+ // contributes its whole instanceCount.
20913
+ //
20914
+ // `status` is the one exception, narrowed per finding via
20915
+ // countInstancesByStatus. `severity`, `provider` and `action` are not, so
20916
+ // this can exceed what the instance read reports for the same filters: a
20917
+ // rule whose severity moved between versions is kept on its newest and
20918
+ // still counts its older findings. Narrowing the other three needs
20919
+ // per-dimension counts the aggregate does not carry today.
20813
20920
  findings: external_exports.number().int().nonnegative(),
20814
- groups: external_exports.number().int().nonnegative()
20921
+ // Counts TYPES, which is the unit this read pages. The instance read's
20922
+ // own totals count findings; the two deliberately answer different
20923
+ // questions and are never summed.
20924
+ types: external_exports.number().int().nonnegative()
20815
20925
  }),
20816
20926
  facets: FindingFacets,
20817
- items: external_exports.array(FindingGroup),
20927
+ items: external_exports.array(FindingTypeSummary),
20818
20928
  nextCursor: external_exports.string().nullable(),
20819
20929
  // Present only on session-scoped queries (`sessionId` set): per ruleId, how
20820
20930
  // many times that rule fired in the session's persisted transcript. Findings
@@ -20822,7 +20932,7 @@ var ListGroupedFindingsResponse = external_exports.object({
20822
20932
  // every firing, so the two numbers legitimately differ — this map lets a
20823
20933
  // session-scoped view show both.
20824
20934
  sessionFirings: external_exports.record(external_exports.string(), external_exports.number().int().nonnegative()).optional()
20825
- }).meta({ id: "ListGroupedFindingsResponse" });
20935
+ }).meta({ id: "ListFindingTypesResponse" });
20826
20936
  var ApplyFindingActionRequest = external_exports.object({
20827
20937
  // 'quarantined' is system-assigned (see FindingAction) — clients may not set
20828
20938
  // it, so it is excluded from the request contract. The mapping helper
@@ -20852,12 +20962,13 @@ var DEFAULT_FLAT_FINDINGS_LIMIT = 50;
20852
20962
  var MAX_FLAT_FINDINGS_LIMIT = 200;
20853
20963
  var ListFindingInstancesQuery = external_exports.object({
20854
20964
  severity: external_exports.array(Severity).optional(),
20855
- // Rule ids, the same vocabulary the grouped list's `subtype` carries.
20965
+ // Rule ids, the same vocabulary the types list's `subtype` carries. Pinning
20966
+ // ONE of them is how the master/detail view scopes its right-hand panel.
20856
20967
  subtype: external_exports.array(external_exports.string()).optional(),
20857
20968
  provider: external_exports.array(FindingProvider).optional(),
20858
20969
  action: external_exports.array(FindingAction).optional(),
20859
20970
  // Matches each instance's OWN derived status (deriveFindingStatus), unlike
20860
- // the grouped query's group-level fold.
20971
+ // the types query's type-level fold.
20861
20972
  status: external_exports.array(FindingStatus).optional(),
20862
20973
  // Exact host-tool names (attributes.tool_name, e.g. 'Bash'). A real filter,
20863
20974
  // where the free-text `q` can only match the rendered "via Bash" label.
@@ -20874,37 +20985,47 @@ var ListFindingInstancesQuery = external_exports.object({
20874
20985
  });
20875
20986
  var ListFindingInstancesResponse = external_exports.object({
20876
20987
  // Instances matching the filters across the whole scope, not just this
20877
- // page — cursor-independent, like the grouped list's totals.
20988
+ // page — cursor-independent, like the types list's totals.
20878
20989
  totals: external_exports.object({ findings: external_exports.number().int().nonnegative() }),
20879
- // Counts in INSTANCES here, where the grouped response counts groups. Each
20990
+ // Counts in INSTANCES here, where the types response counts types. Each
20880
20991
  // dimension still excludes its own filter.
20881
20992
  facets: FindingFacets,
20882
20993
  items: external_exports.array(FindingInstanceDetail),
20883
20994
  nextCursor: external_exports.string().nullable()
20884
20995
  }).meta({ id: "ListFindingInstancesResponse" });
20885
- var FindingLocationFile = external_exports.object({
20886
- // Empty when the instances carried no file path (a prompt or a tool call
20887
- // with no file attribution).
20888
- file: external_exports.string(),
20889
- instanceCount: external_exports.number().int().nonnegative(),
20890
- maxSeverity: Severity,
20891
- latestDetectedAt: external_exports.iso.datetime(),
20892
- // Folded from the instances' derived statuses with the same
20893
- // open-dominates precedence a group uses.
20894
- status: FindingStatus.optional(),
20895
- // Distinct rules seen at this location, capped — the row shows them as
20896
- // chips, and the count is what conveys scale.
20897
- ruleIds: external_exports.array(external_exports.string())
20898
- }).meta({ id: "FindingLocationFile" });
20899
- var FindingLocationRepo = external_exports.object({
20996
+ var FindingLocationSummary = external_exports.object({
20997
+ // Opaque, stable, minted from the pair by encodeLocationId. It exists
20998
+ // because a location's identity is two values and a URL param carries one:
20999
+ // `?loc=` names a location the way `?rule=` names a type. Only ever compared
21000
+ // for EQUALITY — the page's selection check, this read's `includeId`, the
21001
+ // client's page dedupe — never decoded, and never a sort key.
21002
+ id: external_exports.string(),
20900
21003
  /** Empty when the instances carried no repo attribute. */
20901
21004
  repo: external_exports.string(),
21005
+ // Empty when the instances carried no file path (a prompt, or a tool call
21006
+ // with no file attribution). Both halves empty is a real location — usually
21007
+ // the largest one in a store — and is selectable like any other.
21008
+ file: external_exports.string(),
20902
21009
  instanceCount: external_exports.number().int().nonnegative(),
21010
+ // The WORST severity present, not the first row's. It is this list's primary
21011
+ // sort key, so it is also what explains why a row is where it is, and it is
21012
+ // how a reader decides what to open without opening everything.
20903
21013
  maxSeverity: Severity,
20904
21014
  latestDetectedAt: external_exports.iso.datetime(),
21015
+ // Folded from the instances' derived statuses with the same open-dominates
21016
+ // precedence a group uses, so it answers "is anything left to do here" and
21017
+ // not much more: a location holding 1 open among 40 resolved reads like one
21018
+ // holding 40 open. That loss is accepted — the panel beside this list
21019
+ // carries each finding's own status, and instanceCount sits next to the
21020
+ // badge.
20905
21021
  status: FindingStatus.optional(),
20906
- files: external_exports.array(FindingLocationFile)
20907
- }).meta({ id: "FindingLocationRepo" });
21022
+ // Every distinct rule seen at this location, UNCAPPED — so the length is a
21023
+ // tally rather than a sample and a row can say how many there are. Bounded
21024
+ // by the ruleset, not by the store. The view bounds what it DISPLAYS.
21025
+ ruleIds: external_exports.array(external_exports.string())
21026
+ }).meta({ id: "FindingLocationSummary" });
21027
+ var DEFAULT_FINDING_LOCATIONS_LIMIT = 50;
21028
+ var MAX_FINDING_LOCATIONS_LIMIT = 100;
20908
21029
  var ListFindingLocationsQuery = external_exports.object({
20909
21030
  severity: external_exports.array(Severity).optional(),
20910
21031
  subtype: external_exports.array(external_exports.string()).optional(),
@@ -20917,18 +21038,42 @@ var ListFindingLocationsQuery = external_exports.object({
20917
21038
  q: external_exports.string().optional(),
20918
21039
  sessionId: external_exports.string().optional(),
20919
21040
  from: external_exports.iso.datetime().optional(),
20920
- limit: external_exports.coerce.number().int().min(1).max(500).optional()
21041
+ // A LOCATION id (see FindingLocationSummary.id) that must appear in the page
21042
+ // even when the cursor has already advanced past its sort position — the
21043
+ // counterpart of ListFindingTypesQuery.includeId, and needed far more often
21044
+ // here. Selecting a row pushes the URL, which re-renders the server and resets
21045
+ // the client's page cache to page 0; with distinct (repo, file) pairs running
21046
+ // into the thousands, a selection sitting off page 0 is the ordinary case
21047
+ // rather than a deep-link corner. Never affects totals, facets or the cursor.
21048
+ includeId: external_exports.string().optional(),
21049
+ limit: external_exports.coerce.number().int().min(1).max(MAX_FINDING_LOCATIONS_LIMIT).optional(),
21050
+ cursor: external_exports.string().optional()
20921
21051
  });
20922
21052
  var ListFindingLocationsResponse = external_exports.object({
20923
21053
  totals: external_exports.object({
21054
+ // Findings matching the filters across the whole scope. Unlike the types
21055
+ // read's same-named field this needs no caveat: the filters here narrow
21056
+ // per finding, so this is the sum of every row's instanceCount.
20924
21057
  findings: external_exports.number().int().nonnegative(),
20925
- repos: external_exports.number().int().nonnegative(),
20926
- files: external_exports.number().int().nonnegative()
21058
+ // Counts LOCATIONS, the unit this read pages — the number the paginator
21059
+ // states. The facets beside it count FINDINGS (see below); a surface
21060
+ // showing both says which is which.
21061
+ locations: external_exports.number().int().nonnegative()
20927
21062
  }),
20928
- /** Sorted by max severity, then most recent. */
20929
- items: external_exports.array(FindingLocationRepo),
20930
- /** Whether `limit` truncated the repo list. */
20931
- hasMore: external_exports.boolean()
21063
+ // Counts in FINDINGS, where the types response counts types, each dimension
21064
+ // still excluding its own filter. Deliberately not locations: counting those
21065
+ // needs a set of location keys per dimension per value — memory tracking the
21066
+ // store times the vocabulary, in a read whose scan promises flat memory —
21067
+ // and the cheap per-location version is not an approximation but WRONG. A
21068
+ // location holding {claudecode, block} and {codex, warn} would survive
21069
+ // provider=claudecode AND action=warn, under which no single finding
21070
+ // matches, so the facet would contradict the instanceCount this whole view
21071
+ // rests on. Findings also keep the toolbar in the same unit as the page
21072
+ // tally and the panel it sits above.
21073
+ facets: FindingFacets,
21074
+ /** Sorted by max severity, then most recent, then (repo, file). */
21075
+ items: external_exports.array(FindingLocationSummary),
21076
+ nextCursor: external_exports.string().nullable()
20932
21077
  }).meta({ id: "ListFindingLocationsResponse" });
20933
21078
 
20934
21079
  // ../../packages/schema/src/zod/meta.ts
@@ -22097,6 +22242,14 @@ var ControlPlaneErrorBody = external_exports.object({
22097
22242
  message: external_exports.string().optional()
22098
22243
  }).optional()
22099
22244
  });
22245
+ var RemoteFailureKind = external_exports.enum([
22246
+ "unauthorized",
22247
+ "forbidden",
22248
+ "route-absent",
22249
+ "invalid-request",
22250
+ "rejected",
22251
+ "unreachable"
22252
+ ]);
22100
22253
  var AttachDeviceRequest = external_exports.object({
22101
22254
  // This machine's own continuity id, so re-attaching ROTATES the credential
22102
22255
  // on one machine record instead of producing a second one. Client-minted
@@ -22790,139 +22943,62 @@ function deriveFindingStatus(row) {
22790
22943
  if (row.latestResolutionStatus === "dismissed") return "dismissed";
22791
22944
  return "open";
22792
22945
  }
22793
- function distinctUsers(instances) {
22794
- const seen = /* @__PURE__ */ new Set();
22795
- const users = [];
22796
- for (const i of instances) {
22797
- if (i.user === void 0 || seen.has(i.user.id)) continue;
22798
- seen.add(i.user.id);
22799
- users.push(i.user);
22800
- }
22801
- return users;
22802
- }
22803
22946
  function sortUsers(users) {
22804
22947
  return [...users].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
22805
22948
  }
22806
- function buildFindingGroups(rows, opts = {}) {
22807
- const overrides = opts.overrides;
22949
+ function buildFindingTypes(aggregates, opts = {}) {
22808
22950
  const packNames = opts.packNames;
22809
- const aggregates = opts.aggregates;
22810
- const byRuleId = /* @__PURE__ */ new Map();
22811
- for (const row of rows) {
22812
- const existing = byRuleId.get(row.ruleId);
22813
- if (existing) existing.push(row);
22814
- else byRuleId.set(row.ruleId, [row]);
22815
- }
22816
- const groups = [];
22817
- for (const [ruleId, ruleRows] of byRuleId) {
22818
- const instances = ruleRows.map((r) => {
22819
- const effectiveDbAction = overrides?.get(r.id) ?? r.actionTaken;
22820
- return {
22821
- id: r.id,
22822
- provider: toApiProvider(r.sourceTool),
22823
- repo: r.repo,
22824
- file: r.file,
22825
- ...r.toolName === void 0 ? {} : { toolName: r.toolName },
22826
- ...r.eventId === void 0 ? {} : { eventId: r.eventId },
22827
- ...r.sessionId === void 0 ? {} : { sessionId: r.sessionId },
22828
- ...r.user === void 0 ? {} : { user: r.user },
22829
- action: toApiAction(effectiveDbAction),
22830
- detectedAt: r.occurredAt,
22831
- confidence: r.confidence,
22832
- status: r.status
22833
- };
22834
- });
22835
- const agg = aggregates?.get(ruleId);
22836
- const users = agg ? sortUsers(agg.users ?? []) : distinctUsers(instances);
22837
- const latestDetectedAt = agg?.latestDetectedAt ?? ruleRows.reduce(
22838
- (max, r) => r.occurredAt > max ? r.occurredAt : max,
22839
- ruleRows[0]?.occurredAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
22840
- );
22841
- const seenProviders = /* @__PURE__ */ new Set();
22842
- const providers = (agg ? [...new Set(agg.sourceTools.map(toApiProvider))].sort() : instances.map((i) => i.provider)).filter((p) => {
22843
- if (seenProviders.has(p)) return false;
22844
- seenProviders.add(p);
22845
- return true;
22846
- });
22847
- const actionSet = new Set(
22848
- agg ? agg.actionsTaken.map(toApiAction) : instances.map((i) => i.action)
22849
- );
22951
+ const types = [];
22952
+ for (const [ruleId, agg] of aggregates) {
22953
+ const users = sortUsers(agg.users ?? []);
22954
+ const providers = [...new Set(agg.sourceTools.map(toApiProvider))].sort();
22955
+ const actionSet = new Set(agg.actionsTaken.map(toApiAction));
22850
22956
  const aggregateAction = actionSet.size === 1 ? [...actionSet][0] ?? null : null;
22851
- const severity = ruleRows[0]?.severity ?? "low";
22852
- const detection = {
22853
- id: ruleId,
22854
- name: packNames?.get(ruleId) ?? null
22855
- };
22856
- const apiCategory = toApiCategory(ruleRows[0]?.category ?? "custom");
22857
- const policy = { id: `category:${apiCategory}`, name: apiCategory };
22858
- const match = {
22859
- maskedValue: ruleRows[0]?.maskedMatch ?? "",
22860
- contextPrefix: ""
22861
- // empty (pending privacy review)
22862
- };
22863
- const status = foldGroupStatus(
22864
- agg ? agg.statusInputs.map(deriveFindingStatus) : instances.map((i) => i.status)
22865
- );
22866
- const group = {
22957
+ const apiCategory = toApiCategory(agg.category ?? "custom");
22958
+ const type = {
22867
22959
  id: ruleId,
22868
22960
  category: apiCategory,
22869
22961
  subtype: ruleId,
22870
22962
  // human label comes with pack metadata later
22871
- severity,
22872
- match,
22873
- detection,
22874
- policy,
22875
- instanceCount: agg?.instanceCount ?? instances.length,
22963
+ severity: agg.severity ?? "low",
22964
+ detection: { id: ruleId, name: packNames?.get(ruleId) ?? null },
22965
+ policy: { id: `category:${apiCategory}`, name: apiCategory },
22966
+ instanceCount: agg.instanceCount,
22876
22967
  providers,
22877
22968
  aggregateAction,
22878
- latestDetectedAt,
22879
- instances,
22880
- status,
22969
+ latestDetectedAt: agg.latestDetectedAt,
22970
+ status: foldGroupStatus(agg.statusInputs.map(deriveFindingStatus)),
22881
22971
  ...users.length > 0 ? { users } : {}
22882
22972
  };
22883
- if (agg) {
22884
- actionsCache.set(group, [...actionSet]);
22885
- if (agg.searchText !== void 0) {
22886
- haystackCache.set(group, buildHaystack(group, agg.searchText));
22887
- }
22973
+ actionsCache.set(type, [...actionSet]);
22974
+ if (agg.searchText !== void 0) {
22975
+ haystackCache.set(type, buildHaystack(type, agg.searchText));
22888
22976
  }
22889
- groups.push(group);
22977
+ types.push(type);
22890
22978
  }
22891
- return groups;
22979
+ return types;
22892
22980
  }
22893
22981
  var haystackCache = /* @__PURE__ */ new WeakMap();
22894
- function buildHaystack(g, extra) {
22982
+ function buildHaystack(t, extra) {
22895
22983
  return [
22896
- g.subtype,
22897
- g.category,
22898
- g.match.maskedValue,
22899
- g.policy.name,
22900
- g.id,
22901
- ...g.instances.map((i) => i.repo),
22902
- ...g.instances.map((i) => i.file),
22903
- ...g.instances.map((i) => i.toolName ? `via ${i.toolName}` : ""),
22904
- ...g.instances.map((i) => i.id),
22905
- // The people: the whole group's list when the store folded one, plus the
22906
- // preview's own — the two overlap, and a haystack does not mind.
22907
- ...(g.users ?? []).map((u) => u.name),
22908
- ...g.instances.map((i) => i.user?.name ?? ""),
22984
+ t.subtype,
22985
+ t.category,
22986
+ t.policy.name,
22987
+ t.id,
22988
+ ...(t.users ?? []).map((u) => u.name),
22909
22989
  ...extra === void 0 ? [] : [extra]
22910
22990
  ].join(" ").toLowerCase();
22911
22991
  }
22912
- function groupHaystack(g) {
22913
- const cached2 = haystackCache.get(g);
22992
+ function typeHaystack(t) {
22993
+ const cached2 = haystackCache.get(t);
22914
22994
  if (cached2 !== void 0) return cached2;
22915
- const haystack = buildHaystack(g);
22916
- haystackCache.set(g, haystack);
22995
+ const haystack = buildHaystack(t);
22996
+ haystackCache.set(t, haystack);
22917
22997
  return haystack;
22918
22998
  }
22919
22999
  var actionsCache = /* @__PURE__ */ new WeakMap();
22920
- function groupActions(g) {
22921
- const cached2 = actionsCache.get(g);
22922
- if (cached2 !== void 0) return cached2;
22923
- const actions = [...new Set(g.instances.map((i) => i.action))];
22924
- actionsCache.set(g, actions);
22925
- return actions;
23000
+ function typeActions(t) {
23001
+ return actionsCache.get(t) ?? [];
22926
23002
  }
22927
23003
  function countInstancesByStatus(statusInputs, statuses) {
22928
23004
  const statusSet = new Set(statuses);
@@ -22933,8 +23009,8 @@ function countInstancesByStatus(statusInputs, statuses) {
22933
23009
  }
22934
23010
  return sum;
22935
23011
  }
22936
- function applyFindingFilters(groups, opts) {
22937
- let filtered = groups;
23012
+ function applyFindingFilters(types, opts) {
23013
+ let filtered = types;
22938
23014
  if (opts.severity && opts.severity.length > 0) {
22939
23015
  const sevSet = new Set(opts.severity);
22940
23016
  filtered = filtered.filter((g) => sevSet.has(g.severity));
@@ -22945,7 +23021,7 @@ function applyFindingFilters(groups, opts) {
22945
23021
  }
22946
23022
  if (opts.actions && opts.actions.length > 0) {
22947
23023
  const actionSet = new Set(opts.actions);
22948
- filtered = filtered.filter((g) => groupActions(g).some((a) => actionSet.has(a)));
23024
+ filtered = filtered.filter((t) => typeActions(t).some((a) => actionSet.has(a)));
22949
23025
  }
22950
23026
  if (opts.subtype && opts.subtype.length > 0) {
22951
23027
  const subtypeSet = new Set(opts.subtype);
@@ -22957,7 +23033,7 @@ function applyFindingFilters(groups, opts) {
22957
23033
  }
22958
23034
  if (opts.q) {
22959
23035
  const q = opts.q.toLowerCase();
22960
- filtered = filtered.filter((g) => groupHaystack(g).includes(q));
23036
+ filtered = filtered.filter((t) => typeHaystack(t).includes(q));
22961
23037
  }
22962
23038
  return filtered;
22963
23039
  }
@@ -22972,11 +23048,11 @@ function compareFindingGroupOrder(a, b) {
22972
23048
  if (recencyDiff !== 0) return recencyDiff;
22973
23049
  return a.id.localeCompare(b.id);
22974
23050
  }
22975
- function sortFindingGroups(groups) {
22976
- return [...groups].sort(compareFindingGroupOrder);
23051
+ function sortFindingTypes(types) {
23052
+ return [...types].sort(compareFindingGroupOrder);
22977
23053
  }
22978
- function computeFindingFacets(allGroups, opts) {
22979
- const forSeverity = applyFindingFilters(allGroups, {
23054
+ function computeFindingFacets(allTypes, opts) {
23055
+ const forSeverity = applyFindingFilters(allTypes, {
22980
23056
  providers: opts.providers,
22981
23057
  actions: opts.actions,
22982
23058
  statuses: opts.statuses,
@@ -22987,7 +23063,7 @@ function computeFindingFacets(allGroups, opts) {
22987
23063
  for (const g of forSeverity) {
22988
23064
  severityMap.set(g.severity, (severityMap.get(g.severity) ?? 0) + 1);
22989
23065
  }
22990
- const forProvider = applyFindingFilters(allGroups, {
23066
+ const forProvider = applyFindingFilters(allTypes, {
22991
23067
  actions: opts.actions,
22992
23068
  statuses: opts.statuses,
22993
23069
  q: opts.q,
@@ -22998,7 +23074,7 @@ function computeFindingFacets(allGroups, opts) {
22998
23074
  for (const g of forProvider) {
22999
23075
  for (const p of g.providers) providerMap.set(p, (providerMap.get(p) ?? 0) + 1);
23000
23076
  }
23001
- const forAction = applyFindingFilters(allGroups, {
23077
+ const forAction = applyFindingFilters(allTypes, {
23002
23078
  providers: opts.providers,
23003
23079
  statuses: opts.statuses,
23004
23080
  q: opts.q,
@@ -23007,9 +23083,9 @@ function computeFindingFacets(allGroups, opts) {
23007
23083
  });
23008
23084
  const actionMap = /* @__PURE__ */ new Map();
23009
23085
  for (const g of forAction) {
23010
- for (const a of groupActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23086
+ for (const a of typeActions(g)) actionMap.set(a, (actionMap.get(a) ?? 0) + 1);
23011
23087
  }
23012
- const forSubtype = applyFindingFilters(allGroups, {
23088
+ const forSubtype = applyFindingFilters(allTypes, {
23013
23089
  providers: opts.providers,
23014
23090
  actions: opts.actions,
23015
23091
  statuses: opts.statuses,
@@ -23018,7 +23094,7 @@ function computeFindingFacets(allGroups, opts) {
23018
23094
  });
23019
23095
  const subtypeMap = /* @__PURE__ */ new Map();
23020
23096
  for (const g of forSubtype) subtypeMap.set(g.subtype, (subtypeMap.get(g.subtype) ?? 0) + 1);
23021
- const forStatus = applyFindingFilters(allGroups, {
23097
+ const forStatus = applyFindingFilters(allTypes, {
23022
23098
  providers: opts.providers,
23023
23099
  actions: opts.actions,
23024
23100
  q: opts.q,
@@ -23066,10 +23142,20 @@ function matchesDimension(row, opts, dimension) {
23066
23142
  return !opts.statuses?.length || row.status !== void 0 && opts.statuses.includes(row.status);
23067
23143
  case "tools":
23068
23144
  return !opts.tools?.length || row.toolName !== void 0 && opts.tools.includes(row.toolName);
23145
+ // An EMPTY value is a real filter here, not an absent one. The location
23146
+ // list buckets a finding whose event recorded no repo — or no file — under
23147
+ // the empty string, and selecting that bucket has to narrow the panel to
23148
+ // exactly it. Only `undefined` means "no filter"; a caller that wants every
23149
+ // row omits the key, which every call site already does.
23150
+ //
23151
+ // Reading '' as unset is what this replaced, and it failed in the one place
23152
+ // it mattered: the no-repo/no-file bucket is often the largest in a real
23153
+ // store, and its panel dropped both predicates and returned the WHOLE scope
23154
+ // — a row reading 3 findings beside a panel listing every finding there is.
23069
23155
  case "repo":
23070
- return opts.repo === void 0 || opts.repo === "" || row.repo === opts.repo;
23156
+ return opts.repo === void 0 || row.repo === opts.repo;
23071
23157
  case "file":
23072
- return opts.file === void 0 || opts.file === "" || row.file === opts.file;
23158
+ return opts.file === void 0 || row.file === opts.file;
23073
23159
  case "q":
23074
23160
  return !opts.q || rowHaystack(row).includes(opts.q.toLowerCase());
23075
23161
  }
@@ -23183,6 +23269,23 @@ function addToLocation(acc, row) {
23183
23269
  acc.statuses.push(row.status);
23184
23270
  acc.ruleIds.add(row.ruleId);
23185
23271
  }
23272
+ function compareLocationOrder(a, b) {
23273
+ const rankA = SEVERITY_ORDER2[a.maxSeverity] ?? -1;
23274
+ const rankB = SEVERITY_ORDER2[b.maxSeverity] ?? -1;
23275
+ if (rankA !== rankB) return rankA - rankB;
23276
+ if (a.latestDetectedAt !== b.latestDetectedAt) {
23277
+ return a.latestDetectedAt < b.latestDetectedAt ? 1 : -1;
23278
+ }
23279
+ if (a.repo !== b.repo) return a.repo < b.repo ? -1 : 1;
23280
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
23281
+ return 0;
23282
+ }
23283
+ function encodeLocationId(repo, file2) {
23284
+ return `${encodePart(repo)}/${encodePart(file2)}`;
23285
+ }
23286
+ function encodePart(value) {
23287
+ return encodeURIComponent(value.replace(/[\uD800-\uDFFF]/gu, "\uFFFD"));
23288
+ }
23186
23289
 
23187
23290
  // ../../packages/schema/src/zod/installed-pack.ts
23188
23291
  var InstalledPack = external_exports.object({
@@ -23686,11 +23789,11 @@ var WorkspaceSettings = external_exports.object({
23686
23789
  // covers the current payload and must be re-granted.
23687
23790
  modelJudgeConsent: ModelJudgeConsent.optional(),
23688
23791
  // Records that the user consented to the DEFERRED send — the outbox — along
23689
- // with the payload shape and the endpoint they agreed to. Since payload v2
23690
- // that covers both the pre-attach backlog and undelivered captures (which
23691
- // carry prompt/reply text in `content`); the key name predates the widening.
23692
- // Absent until granted, and a grant for a different endpoint or an older
23693
- // payload no longer counts.
23792
+ // with the payload shape and the endpoint they agreed to. Since payload v3
23793
+ // that covers the pre-attach backlog AND undelivered captures alike, and both
23794
+ // carry prompt/reply/tool-output text in `content`; the key name predates
23795
+ // both widenings. Absent until granted, and a grant for a different endpoint
23796
+ // or an older payload no longer counts.
23694
23797
  historySyncConsent: HistorySyncConsent.optional()
23695
23798
  });
23696
23799
  function defaultWorkspaceSettings() {
@@ -23821,6 +23924,9 @@ var ManagedSettingKey = external_exports.enum([
23821
23924
  "dataSharesInPlace",
23822
23925
  "redactFallback"
23823
23926
  ]).meta({ id: "ManagedSettingKey" });
23927
+ function isManagedSettingKey(value) {
23928
+ return ManagedSettingKey.safeParse(value).success;
23929
+ }
23824
23930
  var ManagedSettingsValues = external_exports.object({
23825
23931
  runMode: external_exports.enum(["standalone", "attached"]).optional(),
23826
23932
  controlPlane: external_exports.object({
@@ -23845,7 +23951,27 @@ var ManagedSettings = external_exports.object({
23845
23951
  // Which of those the user may not change. A key here with no matching value
23846
23952
  // freezes whatever the user last chose; a value with no lock is a DEFAULT
23847
23953
  // the user may still override. The two are separable on purpose.
23848
- lockedFields: external_exports.array(ManagedSettingKey).default([])
23954
+ //
23955
+ // Parsed as NAMES rather than as the enum, and split below: a name this
23956
+ // build does not know is dropped from the locked set and reported, never a
23957
+ // reason to refuse the file. The same shape reaches an older build whenever
23958
+ // an administrator locks a key a newer build added, and refusing it there
23959
+ // ran that build entirely unmanaged — every pin and lock gone — on exactly
23960
+ // the fleets most likely to carry a version skew. A name outside the enum
23961
+ // is still never HONOURED: the lockable set stays explicit above.
23962
+ lockedFields: external_exports.array(external_exports.string()).default([])
23963
+ }).transform(({ lockedFields, ...rest }) => {
23964
+ const known = [];
23965
+ const unknown2 = [];
23966
+ for (const name of lockedFields) {
23967
+ if (isManagedSettingKey(name)) known.push(name);
23968
+ else unknown2.push(name);
23969
+ }
23970
+ return {
23971
+ ...rest,
23972
+ lockedFields: known,
23973
+ ...unknown2.length > 0 ? { unknownLockedFields: unknown2 } : {}
23974
+ };
23849
23975
  }).meta({ id: "ManagedSettings" });
23850
23976
 
23851
23977
  // ../../packages/schema/src/zod/project-files.ts
@@ -23969,7 +24095,11 @@ var FindingsTimeseriesPoint = external_exports.object({
23969
24095
  timestamp: external_exports.iso.date(),
23970
24096
  critical: external_exports.number().int().nonnegative(),
23971
24097
  high: external_exports.number().int().nonnegative(),
23972
- medium: external_exports.number().int().nonnegative()
24098
+ medium: external_exports.number().int().nonnegative(),
24099
+ // Optional and additive, so a producer written against the earlier
24100
+ // three-series contract keeps validating. A consumer plotting it resolves the
24101
+ // absent case itself — the chart point requires a number.
24102
+ low: external_exports.number().int().nonnegative().optional()
23973
24103
  }).meta({ id: "FindingsTimeseriesPoint" });
23974
24104
  var FindingsTimeseriesResponse = external_exports.object({
23975
24105
  range: TimeRange,
@@ -23995,6 +24125,10 @@ var ResolvedFeedItem = external_exports.object({
23995
24125
  findingKey: external_exports.string(),
23996
24126
  ruleId: external_exports.string(),
23997
24127
  severity: Severity,
24128
+ // Repository slug, and the file path RELATIVE to it. The pair is what
24129
+ // identifies the file: a bare path matches the same name in every repo.
24130
+ // Optional and additive; empty when the event carried no repo.
24131
+ repo: external_exports.string().optional(),
23998
24132
  path: external_exports.string(),
23999
24133
  // ISO-8601 datetime (matches FindingInstance.detectedAt / the rest of the
24000
24134
  // findings domain). The reader `.toISOString()`s the DB epoch-ms values.
@@ -25538,7 +25672,8 @@ var SqliteActivityRepository = class {
25538
25672
  SELECT 1 FROM audit_events d
25539
25673
  WHERE d.root_session_id = audit_events.id
25540
25674
  AND (d.content LIKE ? ESCAPE '\\'
25541
- OR json_extract(d.attributes, '$.detail') LIKE ? ESCAPE '\\')))`
25675
+ OR coalesce(json_extract(d.attributes, '$.detail'),
25676
+ json_extract(d.attributes, '$.target')) LIKE ? ESCAPE '\\')))`
25542
25677
  );
25543
25678
  params.push(pattern, pattern, pattern, pattern, pattern, pattern);
25544
25679
  }
@@ -26876,23 +27011,6 @@ var LATEST_RESOLUTION_BY_KEY_SQL = `(
26876
27011
  )`;
26877
27012
 
26878
27013
  // ../../packages/persistence/src/repositories/findings.ts
26879
- var PREVIEW_INSTANCES_PER_GROUP = 200;
26880
- var DEFAULT_LOCATIONS_LIMIT = 100;
26881
- var LOCATION_RULE_IDS_CAP = 20;
26882
- function compareLocationOrder(a, b) {
26883
- return compareFindingGroupOrder(
26884
- {
26885
- severity: a.maxSeverity,
26886
- latestDetectedAt: a.latestDetectedAt,
26887
- id: ""
26888
- },
26889
- {
26890
- severity: b.maxSeverity,
26891
- latestDetectedAt: b.latestDetectedAt,
26892
- id: ""
26893
- }
26894
- );
26895
- }
26896
27014
  var CONCAT_SEP = ",";
26897
27015
  var TUPLE_SEP = "|";
26898
27016
  function splitConcat(value) {
@@ -26944,13 +27062,48 @@ function decodeGroupCursor(cursor) {
26944
27062
  return null;
26945
27063
  }
26946
27064
  function firstAfter(sorted, cursor) {
26947
- const index = sorted.findIndex((g) => compareFindingGroupOrder(g, cursor) > 0);
27065
+ const index = sorted.findIndex((t) => compareFindingGroupOrder(t, cursor) > 0);
26948
27066
  return index === -1 ? sorted.length : index;
26949
27067
  }
26950
27068
  function findDeepLinked(sorted, page, id) {
26951
- if (page.some((g) => g.id === id || g.instances.some((i) => i.id === id))) return void 0;
26952
- return sorted.find((g) => g.id === id || g.instances.some((i) => i.id === id));
27069
+ if (page.some((t) => t.id === id)) return void 0;
27070
+ return sorted.find((t) => t.id === id);
27071
+ }
27072
+ function encodeLocationCursor(location) {
27073
+ const payload = {
27074
+ sev: location.maxSeverity,
27075
+ t: location.latestDetectedAt,
27076
+ r: location.repo,
27077
+ f: location.file
27078
+ };
27079
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
26953
27080
  }
27081
+ function decodeLocationCursor(cursor) {
27082
+ const parsed2 = parseJsonObject(Buffer.from(cursor, "base64url").toString("utf8"));
27083
+ if (parsed2 !== void 0 && typeof parsed2.sev === "string" && typeof parsed2.t === "string" && typeof parsed2.r === "string" && typeof parsed2.f === "string") {
27084
+ return { maxSeverity: parsed2.sev, latestDetectedAt: parsed2.t, repo: parsed2.r, file: parsed2.f };
27085
+ }
27086
+ return null;
27087
+ }
27088
+ function firstLocationAfter(sorted, cursor) {
27089
+ const index = sorted.findIndex((l) => compareLocationOrder(l, cursor) > 0);
27090
+ return index === -1 ? sorted.length : index;
27091
+ }
27092
+ function findDeepLinkedLocation(sorted, page, id) {
27093
+ if (page.some((l) => l.id === id)) return void 0;
27094
+ return sorted.find((l) => l.id === id);
27095
+ }
27096
+ var FINDING_ROW_COLUMNS_SQL = `f.id AS id, d.rule_id AS rule_id, d.category AS category,
27097
+ d.severity AS severity, f.masked_match AS masked_match,
27098
+ f.action_taken AS action_taken, f.confidence AS confidence,
27099
+ e.started_at AS occurred_at,
27100
+ e.source_tool AS source_tool,
27101
+ e.repo AS repo,
27102
+ e.file_path AS file,
27103
+ e.tool_name AS tool_name,
27104
+ f.audit_event_id AS event_id, e.root_session_id AS session_id,
27105
+ e.event_type AS kind, f.finding_key AS finding_key,
27106
+ ${latestResolutionStatusSql("f")} AS latest_status`;
26954
27107
  var DAY_MS3 = 864e5;
26955
27108
  var SqliteFindingsRepository = class {
26956
27109
  constructor(db) {
@@ -27071,30 +27224,26 @@ var SqliteFindingsRepository = class {
27071
27224
  );
27072
27225
  }
27073
27226
  /**
27074
- * Grouped findings for the dashboard — joins inspection_findings⋈audit_events
27075
- * ⋈inspection_definitions (repo/file/toolName from the audit event's
27076
- * attributes bag, rule_id/category/severity from the definition), scoped to
27077
- * the four capture kinds (audit_events also holds structural/reconciler/scan
27078
- * rows this list must never surface), groups by ruleId, computes
27079
- * per-filter-excluded facets, applies the requested filters, and sorts by
27080
- * severity then recency. Filtering
27081
- * and faceting run in JS via the shared @akasecurity/schema helpers. `totals`
27082
- * reflect the full filtered set; `items` is the requested
27083
- * page (default 50); no cursor (nextCursor is always null). Under a `status`
27084
- * filter, `totals.findings` counts only instances whose derived status was
27085
- * requested, and each item's instance preview is narrowed the same way.
27227
+ * Finding TYPES for the dashboard — one row per rule, scoped to the four
27228
+ * capture kinds (audit_events also holds structural/reconciler/scan rows this
27229
+ * list must never surface), with per-filter-excluded facets, the requested
27230
+ * filters applied, and sorted by severity then recency. Filtering and faceting
27231
+ * run in JS via the shared @akasecurity/schema helpers. `totals` reflect the
27232
+ * full filtered set; `items` is the requested page (default 50), keyset-paged.
27233
+ * Under a `status` filter, `totals.findings` counts only findings whose
27234
+ * derived status was requested.
27235
+ *
27236
+ * ONE read, which materializes no findings: a single aggregate per rule_id,
27237
+ * folding EVERY finding into the numbers a type row and the filters need
27238
+ * (count, severity, category, providers, actions, statuses, latest, search
27239
+ * text). The findings OF a type come from listFindingInstances scoped to
27240
+ * `subtype`, so neither list bounds the other and no per-type cap exists.
27086
27241
  *
27087
- * Two reads, neither of which materializes a row per finding:
27088
- * 1. one aggregate row per rule_id, folding EVERY instance into the numbers
27089
- * the group and the filters need (count, providers, actions, statuses,
27090
- * latest, search text);
27091
- * 2. each group's newest PREVIEW_INSTANCES_PER_GROUP instances, which
27092
- * populate `instances` for the table's expanded rows.
27093
27242
  * The aggregates carry raw DB values and are translated by the same
27094
- * @akasecurity/schema mappers the row path uses, so no enum mapping or status
27095
- * rule is ever restated in SQL.
27243
+ * @akasecurity/schema mappers every other path uses, so no enum mapping or
27244
+ * status rule is ever restated in SQL.
27096
27245
  */
27097
- listGroupedFindings(query) {
27246
+ listFindingTypes(query) {
27098
27247
  const sessionPredicate = query.sessionId ? ` AND e.root_session_id = :sessionId` : "";
27099
27248
  const fromMs = query.from === void 0 ? void 0 : isoToEpochMillis(query.from);
27100
27249
  const fromPredicate = fromMs === void 0 ? "" : ` AND e.started_at >= :fromMs`;
@@ -27107,12 +27256,7 @@ var SqliteFindingsRepository = class {
27107
27256
  predicate,
27108
27257
  params: sessionParams
27109
27258
  });
27110
- const rows = this.previewRows(aggregates, {
27111
- sessionId: query.sessionId,
27112
- from: query.from
27113
- });
27114
- const groupable = rows.map(toFlatFindingRow);
27115
- const allGroups = buildFindingGroups(groupable, { aggregates });
27259
+ const allTypes = buildFindingTypes(aggregates);
27116
27260
  const filterOpts = {
27117
27261
  severity: query.severity,
27118
27262
  providers: query.provider,
@@ -27121,30 +27265,25 @@ var SqliteFindingsRepository = class {
27121
27265
  subtype: query.subtype,
27122
27266
  q: query.q
27123
27267
  };
27124
- const facets = computeFindingFacets(allGroups, filterOpts);
27125
- const sorted = sortFindingGroups(applyFindingFilters(allGroups, filterOpts));
27268
+ const facets = computeFindingFacets(allTypes, filterOpts);
27269
+ const sorted = sortFindingTypes(applyFindingFilters(allTypes, filterOpts));
27126
27270
  const statusFilter = query.status ?? [];
27127
27271
  const totals = {
27128
- findings: sorted.reduce((acc, g) => {
27129
- if (statusFilter.length === 0) return acc + g.instanceCount;
27130
- const agg = aggregates.get(g.id);
27131
- return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? g.instanceCount : g.instanceCount);
27272
+ findings: sorted.reduce((acc, t) => {
27273
+ if (statusFilter.length === 0) return acc + t.instanceCount;
27274
+ const agg = aggregates.get(t.id);
27275
+ return acc + (agg ? countInstancesByStatus(agg.statusInputs, statusFilter) ?? t.instanceCount : t.instanceCount);
27132
27276
  }, 0),
27133
- groups: sorted.length
27277
+ types: sorted.length
27134
27278
  };
27135
- const limit = query.limit ?? DEFAULT_GROUPED_FINDINGS_LIMIT;
27279
+ const limit = query.limit ?? DEFAULT_FINDING_TYPES_LIMIT;
27136
27280
  const cursor = query.cursor === void 0 ? null : decodeGroupCursor(query.cursor);
27137
27281
  const start = cursor === null ? 0 : firstAfter(sorted, cursor);
27138
27282
  const page = sorted.slice(start, start + limit);
27139
27283
  const lastOnPage = page.at(-1);
27140
27284
  const nextCursor = start + limit < sorted.length && lastOnPage ? encodeGroupCursor(lastOnPage) : null;
27141
27285
  const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinked(sorted, page, query.includeId);
27142
- const statusSet = statusFilter.length > 0 ? new Set(statusFilter) : null;
27143
- const narrow = (g) => statusSet ? {
27144
- ...g,
27145
- instances: g.instances.filter((i) => i.status !== void 0 && statusSet.has(i.status))
27146
- } : g;
27147
- const items = [...page, ...deepLinked ? [deepLinked] : []].map(narrow);
27286
+ const items = [...page, ...deepLinked ? [deepLinked] : []];
27148
27287
  return Promise.resolve({
27149
27288
  totals,
27150
27289
  facets,
@@ -27155,7 +27294,7 @@ var SqliteFindingsRepository = class {
27155
27294
  }
27156
27295
  /**
27157
27296
  * One row per rule_id, folding EVERY instance of the group into the values
27158
- * buildFindingGroups cannot recover from a preview. Bounded by the number of
27297
+ * buildFindingTypes cannot recover from an aggregate. Bounded by the number of
27159
27298
  * distinct rule_ids (the installed packs' rules), not by the store's size.
27160
27299
  *
27161
27300
  * A single scan, folded in two levels: the inner SELECT groups by
@@ -27249,13 +27388,25 @@ var SqliteFindingsRepository = class {
27249
27388
  });
27250
27389
  }
27251
27390
  /**
27252
- * The same findings folded by location: repository, then file within it.
27391
+ * The same findings folded by WHERE they live — one row per (repo, file) pair.
27253
27392
  *
27254
27393
  * The grouping keys come from the capturing event's attributes, which is what
27255
- * the local store relates a finding to — there is no finding↔asset row to
27256
- * group by instead. A repo or file the event did not record folds into the
27257
- * empty-string bucket, which the view renders but does not link, since no
27258
- * filter can name it.
27394
+ * the local store relates a finding to; there is no finding↔asset row to group
27395
+ * by instead. A repo or file the event did not record folds into the
27396
+ * empty-string bucket, which is a real location like any other: it is listed,
27397
+ * it is selectable, and its `?loc=` token is as good as any other row's.
27398
+ *
27399
+ * ONE flat list rather than repos nesting files. A rollup can only be paged by
27400
+ * repo, which leaves the file list inside it unbounded — the shape the by-type
27401
+ * list was rebuilt to remove — and two-level pagination inside an
27402
+ * expand/collapse table is what pushed that view to master/detail in the first
27403
+ * place.
27404
+ *
27405
+ * Every filter narrows the FINDINGS and the locations fall out of what
27406
+ * survives, so each row's `instanceCount` is exactly what listFindingInstances
27407
+ * reports for the same filters scoped to that pair. The view depends on it:
27408
+ * one toolbar sits over both panels precisely because a location owns none of
27409
+ * its fields.
27259
27410
  */
27260
27411
  listFindingLocations(query) {
27261
27412
  const opts = {
@@ -27267,13 +27418,16 @@ var SqliteFindingsRepository = class {
27267
27418
  tools: query.tool,
27268
27419
  q: query.q
27269
27420
  };
27270
- const limit = query.limit ?? DEFAULT_LOCATIONS_LIMIT;
27421
+ const limit = query.limit ?? DEFAULT_FINDING_LOCATIONS_LIMIT;
27422
+ const cursor = query.cursor === void 0 ? null : decodeLocationCursor(query.cursor);
27271
27423
  const byRepo = /* @__PURE__ */ new Map();
27424
+ const accumulator = createInstanceFacetAccumulator(opts);
27272
27425
  let total = 0;
27273
27426
  for (const row of this.scanFindingRows({
27274
27427
  sessionId: query.sessionId,
27275
27428
  from: query.from
27276
27429
  })) {
27430
+ accumulator.add(row);
27277
27431
  if (!matchesInstanceFilters(row, opts)) continue;
27278
27432
  total += 1;
27279
27433
  let files = byRepo.get(row.repo);
@@ -27288,103 +27442,35 @@ var SqliteFindingsRepository = class {
27288
27442
  }
27289
27443
  addToLocation(acc, row);
27290
27444
  }
27291
- let fileCount = 0;
27292
- const repos = [...byRepo.entries()].map(([repo, files]) => {
27293
- fileCount += files.size;
27294
- const fileRows = [...files.entries()].map(([file2, acc]) => ({
27295
- file: file2,
27296
- instanceCount: acc.instanceCount,
27297
- maxSeverity: acc.maxSeverity,
27298
- latestDetectedAt: acc.latestDetectedAt,
27299
- ...foldGroupStatus(acc.statuses) === void 0 ? {} : { status: foldGroupStatus(acc.statuses) },
27300
- ruleIds: [...acc.ruleIds].slice(0, LOCATION_RULE_IDS_CAP)
27301
- })).sort(compareLocationOrder);
27302
- const rollup = fileRows.reduce(
27303
- (a, f) => ({
27304
- instanceCount: a.instanceCount + f.instanceCount,
27305
- maxSeverity: compareLocationOrder(f, a) < 0 ? f.maxSeverity : a.maxSeverity,
27306
- latestDetectedAt: f.latestDetectedAt > a.latestDetectedAt ? f.latestDetectedAt : a.latestDetectedAt
27307
- }),
27308
- {
27309
- instanceCount: 0,
27310
- maxSeverity: fileRows[0]?.maxSeverity ?? "low",
27311
- latestDetectedAt: ""
27312
- }
27313
- );
27314
- const statuses = fileRows.map((f) => f.status);
27315
- const folded = foldGroupStatus(statuses);
27316
- return {
27317
- repo,
27318
- instanceCount: rollup.instanceCount,
27319
- maxSeverity: rollup.maxSeverity,
27320
- latestDetectedAt: rollup.latestDetectedAt,
27321
- ...folded === void 0 ? {} : { status: folded },
27322
- files: fileRows
27323
- };
27324
- });
27325
- repos.sort(compareLocationOrder);
27445
+ const sorted = [];
27446
+ for (const [repo, files] of byRepo) {
27447
+ for (const [file2, acc] of files) {
27448
+ const status = foldGroupStatus(acc.statuses);
27449
+ sorted.push({
27450
+ id: encodeLocationId(repo, file2),
27451
+ repo,
27452
+ file: file2,
27453
+ instanceCount: acc.instanceCount,
27454
+ maxSeverity: acc.maxSeverity,
27455
+ latestDetectedAt: acc.latestDetectedAt,
27456
+ ...status === void 0 ? {} : { status },
27457
+ ruleIds: [...acc.ruleIds]
27458
+ });
27459
+ }
27460
+ }
27461
+ sorted.sort(compareLocationOrder);
27462
+ const start = cursor === null ? 0 : firstLocationAfter(sorted, cursor);
27463
+ const page = sorted.slice(start, start + limit);
27464
+ const lastOnPage = page.at(-1);
27465
+ const nextCursor = start + limit < sorted.length && lastOnPage ? encodeLocationCursor(lastOnPage) : null;
27466
+ const deepLinked = query.includeId === void 0 || query.includeId === "" ? void 0 : findDeepLinkedLocation(sorted, page, query.includeId);
27326
27467
  return Promise.resolve({
27327
- totals: { findings: total, repos: repos.length, files: fileCount },
27328
- items: repos.slice(0, limit),
27329
- hasMore: repos.length > limit
27468
+ totals: { findings: total, locations: sorted.length },
27469
+ facets: accumulator.facets(),
27470
+ items: [...page, ...deepLinked ? [deepLinked] : []],
27471
+ nextCursor
27330
27472
  });
27331
27473
  }
27332
- /**
27333
- * Each group's newest instances, for the table's expanded rows.
27334
- *
27335
- * ONE index-ordered scan with early termination, and the shape is the point.
27336
- * The natural spelling — `ROW_NUMBER() OVER (PARTITION BY rule_id ORDER BY
27337
- * started_at DESC)` then `WHERE rn <= cap` — sorts EVERY finding in scope
27338
- * through a temp B-tree to keep a bounded preview of each group, and then
27339
- * sorts the survivors again for the page order. Both sorts grow with the
27340
- * store while the answer does not.
27341
- *
27342
- * Instead the scan walks `audit_events` newest-first off `idx_audit_started_at`
27343
- * (or the session or window index the scope names — see `findingScanSql`),
27344
- * which is already the order the page wants, and keeps rows per rule until
27345
- * each rule has as many as it can show. The aggregate the caller already holds
27346
- * says how many that is: `min(instanceCount, PREVIEW_INSTANCES_PER_GROUP)`
27347
- * per rule, summed, is the number of rows this scan has to find, and it stops
27348
- * on the last one. That sum is bounded by `rules * PREVIEW_INSTANCES_PER_GROUP`
27349
- * (8,000 at this repo's 40-rule bench corpus), not by a fixed row count — a
27350
- * store with many firing rules widens it. The bound that DOES hold
27351
- * unconditionally is the sorted form's floor: this scan visits at most as
27352
- * many rows as `ROW_NUMBER() OVER (PARTITION BY rule_id …)` would have
27353
- * sorted, and stops the moment every rule has its cap, where the sorted form
27354
- * sorts the whole scope regardless. The true worst case — the rarest rule's
27355
- * wanted instances sitting at the tail of the scope — is one pass over
27356
- * everything in scope with a block sort of the id tie-break only, never a
27357
- * sort of the scope, which is still that floor.
27358
- *
27359
- * A row whose rule the aggregate did not see is skipped: the two statements
27360
- * run without a shared snapshot, so a capture landing between them can add a
27361
- * rule here that has no counts there, and the counts are what the group is
27362
- * built from.
27363
- */
27364
- previewRows(aggregates, scope) {
27365
- const wanted = /* @__PURE__ */ new Map();
27366
- let remaining = 0;
27367
- for (const [ruleId, agg] of aggregates) {
27368
- const n = Math.min(agg.instanceCount, PREVIEW_INSTANCES_PER_GROUP);
27369
- wanted.set(ruleId, n);
27370
- remaining += n;
27371
- }
27372
- const rows = [];
27373
- if (remaining === 0) return rows;
27374
- const { sql, params } = this.findingScanSql(scope);
27375
- const taken = /* @__PURE__ */ new Map();
27376
- for (const r of iterateRows(this.db.prepare(sql), params)) {
27377
- const want = wanted.get(r.rule_id);
27378
- if (want === void 0) continue;
27379
- const have = taken.get(r.rule_id) ?? 0;
27380
- if (have >= want) continue;
27381
- taken.set(r.rule_id, have + 1);
27382
- rows.push(r);
27383
- remaining -= 1;
27384
- if (remaining === 0) break;
27385
- }
27386
- return rows;
27387
- }
27388
27474
  /**
27389
27475
  * Every finding in scope as a FlatFindingRow, newest first, streamed.
27390
27476
  *
@@ -27411,6 +27497,33 @@ var SqliteFindingsRepository = class {
27411
27497
  yield toFlatFindingRow(r);
27412
27498
  }
27413
27499
  }
27500
+ /**
27501
+ * One finding by its own id, or null when no such row exists.
27502
+ *
27503
+ * A primary-key seek on `inspection_findings`, so its cost does not grow with
27504
+ * the store — and, unlike anything derived from a list page, it resolves a
27505
+ * finding of ANY age. That is what the Findings page's one-shot `?finding=`
27506
+ * deep link needs: the id it carries may name a finding thousands of rows
27507
+ * older than anything a first page holds.
27508
+ *
27509
+ * Deliberately UNFILTERED — no capture-kind, session or time predicate. It
27510
+ * RESOLVES an id; whether that row would survive the list's current filters is
27511
+ * a different question, and hiding the target because a filter excludes it is
27512
+ * worse than showing it.
27513
+ *
27514
+ * `groupId` on the result IS the rule id, so this one read answers both "which
27515
+ * type should the list select?" and "what does the drawer show?".
27516
+ */
27517
+ findingInstance(id) {
27518
+ const row = this.db.prepare(
27519
+ `SELECT ${FINDING_ROW_COLUMNS_SQL}
27520
+ FROM inspection_findings f
27521
+ JOIN audit_events e ON e.id = f.audit_event_id
27522
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
27523
+ WHERE f.id = ?`
27524
+ ).get(id);
27525
+ return Promise.resolve(row === void 0 ? null : toInstanceDetail(toFlatFindingRow(row)));
27526
+ }
27414
27527
  /**
27415
27528
  * The one statement both instance-level scans run: every finding in scope,
27416
27529
  * joined to its event and definition, newest first.
@@ -27444,17 +27557,7 @@ var SqliteFindingsRepository = class {
27444
27557
  conditions.push("e.started_at >= ?");
27445
27558
  params.push(isoToEpochMillis(scope.from));
27446
27559
  }
27447
- const sql = `SELECT f.id AS id, d.rule_id AS rule_id, d.category AS category,
27448
- d.severity AS severity, f.masked_match AS masked_match,
27449
- f.action_taken AS action_taken, f.confidence AS confidence,
27450
- e.started_at AS occurred_at,
27451
- e.source_tool AS source_tool,
27452
- e.repo AS repo,
27453
- e.file_path AS file,
27454
- e.tool_name AS tool_name,
27455
- f.audit_event_id AS event_id, e.root_session_id AS session_id,
27456
- e.event_type AS kind, f.finding_key AS finding_key,
27457
- ${latestResolutionStatusSql("f")} AS latest_status
27560
+ const sql = `SELECT ${FINDING_ROW_COLUMNS_SQL}
27458
27561
  FROM audit_events e
27459
27562
  CROSS JOIN inspection_findings f ON f.audit_event_id = e.id
27460
27563
  CROSS JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -27468,6 +27571,26 @@ var SqliteFindingsRepository = class {
27468
27571
  group_concat(DISTINCT 'via ' || e.tool_name) AS tool_names` : `, NULL AS repos, NULL AS files, NULL AS tool_names`;
27469
27572
  const rows = this.db.prepare(
27470
27573
  `SELECT rule_id,
27574
+ -- BARE columns beside max(latest_at), which is deliberate and
27575
+ -- is SQLite's documented behaviour: with a single min()/max()
27576
+ -- in an aggregate query, every bare column takes its value from
27577
+ -- the row that produced the extremum. So these are the severity
27578
+ -- and category of the definition whose finding is NEWEST, which
27579
+ -- is what the row-based build they replaced read off its first
27580
+ -- (newest-first) row.
27581
+ --
27582
+ -- min() is WRONG here and was the defect: inspection_definitions
27583
+ -- holds one row per rule VERSION (see its writer \u2014 a version bump
27584
+ -- mints a new row), so a rule whose severity moved between
27585
+ -- versions has several, and min() picks the ALPHABETICALLY
27586
+ -- smallest \u2014 'low' over 'medium', but 'critical' over 'high'.
27587
+ -- That is arbitrary in direction, and it feeds the badge, the
27588
+ -- filter, the facet counts and the primary sort key.
27589
+ --
27590
+ -- Adding a second min()/max() aggregate here would make these
27591
+ -- bare columns ambiguous again; keep max(latest_at) the only one.
27592
+ severity,
27593
+ category,
27471
27594
  sum(tuple_count) AS instance_count,
27472
27595
  max(latest_at) AS latest_at,
27473
27596
  group_concat(source_tools) AS source_tools,
@@ -27478,6 +27601,14 @@ var SqliteFindingsRepository = class {
27478
27601
  group_concat(tool_names) AS tool_names
27479
27602
  FROM (
27480
27603
  SELECT d.rule_id AS rule_id,
27604
+ -- Severity and category are columns of the DEFINITION, and
27605
+ -- a rule can have SEVERAL definitions (one per version), so
27606
+ -- these are grouped on below and resolved to the newest
27607
+ -- firing version by the outer query's bare-column select.
27608
+ -- They ride the aggregate because the type build has no rows
27609
+ -- to read them off \u2014 see buildFindingTypes.
27610
+ d.severity AS severity,
27611
+ d.category AS category,
27481
27612
  e.event_type || '${TUPLE_SEP}' ||
27482
27613
  (CASE WHEN f.finding_key IS NULL THEN '' ELSE 'k' END) || '${TUPLE_SEP}' ||
27483
27614
  coalesce(latest.status, '') AS status_tuple,
@@ -27492,7 +27623,7 @@ var SqliteFindingsRepository = class {
27492
27623
  LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
27493
27624
  ON latest.finding_key = f.finding_key
27494
27625
  ${scope.predicate}
27495
- GROUP BY d.rule_id, status_tuple
27626
+ GROUP BY d.rule_id, d.severity, d.category, status_tuple
27496
27627
  )
27497
27628
  GROUP BY rule_id`
27498
27629
  ).all(scope.params);
@@ -27501,6 +27632,8 @@ var SqliteFindingsRepository = class {
27501
27632
  r.rule_id,
27502
27633
  {
27503
27634
  instanceCount: r.instance_count,
27635
+ severity: r.severity,
27636
+ category: r.category,
27504
27637
  sourceTools: splitConcat(r.source_tools),
27505
27638
  actionsTaken: splitConcat(r.actions_taken),
27506
27639
  statusInputs: splitConcat(r.status_inputs).map((tuple2) => {
@@ -27517,7 +27650,7 @@ var SqliteFindingsRepository = class {
27517
27650
  latestDetectedAt: epochMillisToIso(r.latest_at),
27518
27651
  // Free text only — joined and substring-matched, so group_concat's
27519
27652
  // commas need no unpicking (a repo/path containing one still matches).
27520
- // Left undefined (not '') when unfetched, so buildFindingGroups can
27653
+ // Left undefined (not '') when unfetched, so buildFindingTypes can
27521
27654
  // tell "no q this request" from "a group with no repo/file at all"
27522
27655
  // and skip priming a haystack nothing will read.
27523
27656
  ...withSearchText ? {
@@ -27669,6 +27802,12 @@ var SqliteHistorySyncRepository = class {
27669
27802
  this.markOwedStmt = db.prepare(
27670
27803
  `UPDATE audit_events SET outbox_owed = 1 WHERE id = :id AND synced_at IS NULL`
27671
27804
  );
27805
+ this.markCaptureBacklogOwedStmt = db.prepare(
27806
+ `UPDATE audit_events SET outbox_owed = 1
27807
+ WHERE synced_at IS NULL
27808
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27809
+ AND started_at < :before`
27810
+ );
27672
27811
  this.stampStmt = db.prepare(
27673
27812
  `UPDATE audit_events SET synced_at = :at, sync_claimed_at = NULL WHERE id = :id`
27674
27813
  );
@@ -27717,7 +27856,9 @@ var SqliteHistorySyncRepository = class {
27717
27856
  );
27718
27857
  this.disownCapturesStmt = db.prepare(
27719
27858
  `UPDATE audit_events SET outbox_owed = NULL
27720
- WHERE outbox_owed IS NOT NULL AND event_type IN (${CAPTURE_TYPE_LIST})`
27859
+ WHERE outbox_owed IS NOT NULL
27860
+ AND event_type IN (${CAPTURE_TYPE_LIST})
27861
+ AND started_at < :attachedAt`
27721
27862
  );
27722
27863
  this.rearmStmt = db.prepare(
27723
27864
  `UPDATE audit_events SET synced_at = NULL
@@ -27793,6 +27934,7 @@ var SqliteHistorySyncRepository = class {
27793
27934
  freezeBoundaryStmt;
27794
27935
  captureRowsStmt;
27795
27936
  markOwedStmt;
27937
+ markCaptureBacklogOwedStmt;
27796
27938
  captureSkipCountStmt;
27797
27939
  disownCapturesStmt;
27798
27940
  partitionStmt;
@@ -27856,6 +27998,23 @@ var SqliteHistorySyncRepository = class {
27856
27998
  markCaptureOwed(id) {
27857
27999
  this.markOwedStmt.run({ id });
27858
28000
  }
28001
+ /**
28002
+ * Mark every capture already on disk as owed, as of `before`.
28003
+ *
28004
+ * The consent-time backfill, called once from `aka attach` when a human
28005
+ * grants existing-history consent — never from an ongoing drain pass, and
28006
+ * never inferred from a boundary that could later move. `before` is the
28007
+ * caller's own "now" at the moment consent was granted, so what this marks
28008
+ * is exactly the backlog the consent prompt already counted, not whatever a
28009
+ * later re-attach or key rotation might widen it to.
28010
+ *
28011
+ * Returns how many rows matched, for the caller to log or test against. Not a
28012
+ * count of NEWLY marked rows — a row still unsynced from an earlier call
28013
+ * matches again and is counted again, the same as `UPDATE`'s own `changes`.
28014
+ */
28015
+ markCaptureBacklogOwed(before) {
28016
+ return Number(this.markCaptureBacklogOwedStmt.run({ before }).changes);
28017
+ }
27859
28018
  /** Record delivery. Called only AFTER the far side has accepted the rows. */
27860
28019
  markSynced(ids, atMs) {
27861
28020
  this.stampAll(ids, atMs);
@@ -27972,15 +28131,42 @@ var SqliteHistorySyncRepository = class {
27972
28131
  *
27973
28132
  * Delivery is a fact about ONE recipient: rows sent to the deployment a
27974
28133
  * machine has just left are undelivered as far as the new one is concerned.
27975
- * All three in one transaction, so a crash between them cannot leave stamps
27976
- * attributed to the wrong deployment, or a boundary that belongs to another.
28134
+ * All four in one transaction, so a crash between them cannot leave stamps
28135
+ * attributed to the wrong deployment, a boundary that belongs to another, or
28136
+ * a disown with no re-mark to follow it.
27977
28137
  *
27978
28138
  * The boundary is written HERE and only here, which is what freezes it: a
27979
28139
  * re-attach to the SAME deployment (a key rotation) leaves the fingerprint
27980
28140
  * unchanged, so this never runs and the backlog does not widen back over rows
27981
28141
  * the live path has since delivered.
28142
+ *
28143
+ * `backfillCapturesBefore` is the caller's OWN "now" at the instant a human
28144
+ * granted existing-history consent for the deployment this call is arming —
28145
+ * a DIFFERENT instant from `backlogBefore`: the re-mark boundary is the GRANT
28146
+ * instant, `backlogBefore` is the ATTACH instant, and the two can be far
28147
+ * apart. Passed only when that grant is valid, since this method has no way
28148
+ * to check consent itself and must not mark a row owed for a machine that
28149
+ * never agreed to it. Applied AFTER the disown above, in the SAME
28150
+ * transaction: what the disown clears is every marker below `backlogBefore`,
28151
+ * which includes this deployment's OWN pre-attach rows — `aka attach` calls
28152
+ * `seedCaptureBacklogOwed` at the attach instant, so every row it marks sits
28153
+ * on the cleared side of that bound — and the re-mark in the same
28154
+ * transaction is what puts those rows back. A crash between the two cannot
28155
+ * strand the ledger disowned with nothing re-marked — the transaction either
28156
+ * lands whole or not at all, and a fingerprint mismatch that has not yet
28157
+ * committed re-enters this method on the very next pass. Omit it (the
28158
+ * structural-only tests do) to exercise the disown in isolation.
28159
+ *
28160
+ * The disown is bounded by `backlogBefore`, which is what keeps it from
28161
+ * touching a marker the NEW deployment's OWN live path has already set: B's
28162
+ * live path can mark a capture owed from the moment `aka attach` writes the
28163
+ * descriptor, before the drain's first pass ever reaches this method, and
28164
+ * such a row sits at or after the bound rather than below it. What keeps the
28165
+ * disown from eating THIS SAME CALL's own re-mark is the order, not the
28166
+ * bound — disown runs first, re-mark second, both inside the one
28167
+ * transaction above.
27982
28168
  */
27983
- rearmFor(fingerprint, backlogBefore) {
28169
+ rearmFor(fingerprint, backlogBefore, backfillCapturesBefore) {
27984
28170
  this.ensureRowStmt.run();
27985
28171
  withTransaction(
27986
28172
  this.db,
@@ -27988,7 +28174,10 @@ var SqliteHistorySyncRepository = class {
27988
28174
  const previous = getRow(this.fingerprintStmt)?.fingerprint;
27989
28175
  this.rearmStmt.run();
27990
28176
  if (previous !== null && previous !== void 0 && previous !== fingerprint) {
27991
- this.disownCapturesStmt.run();
28177
+ this.disownCapturesStmt.run({ attachedAt: backlogBefore });
28178
+ }
28179
+ if (backfillCapturesBefore !== void 0) {
28180
+ this.markCaptureBacklogOwedStmt.run({ before: backfillCapturesBefore });
27992
28181
  }
27993
28182
  this.setFingerprintStmt.run({ fingerprint, backlogBefore });
27994
28183
  },
@@ -28265,7 +28454,8 @@ function managedSettingsPaths(platform2 = process.platform) {
28265
28454
  }
28266
28455
  return [posix.join("/etc", "aka", MANAGED_SETTINGS_FILENAME)];
28267
28456
  }
28268
- function readManagedSettings(paths = managedSettingsPaths()) {
28457
+ var testOnlyManagedPaths = null;
28458
+ function readManagedSettings(paths = testOnlyManagedPaths ?? managedSettingsPaths()) {
28269
28459
  for (const path of paths) {
28270
28460
  let text;
28271
28461
  try {
@@ -30745,7 +30935,7 @@ function toUtcDateString(ms) {
30745
30935
  return new Date(ms).toISOString().slice(0, 10);
30746
30936
  }
30747
30937
  function isTimeseriesSeverity(s) {
30748
- return s === "critical" || s === "high" || s === "medium";
30938
+ return s === "critical" || s === "high" || s === "medium" || s === "low";
30749
30939
  }
30750
30940
  var SqliteSecurityRepository = class {
30751
30941
  constructor(db, now = () => Date.now()) {
@@ -30874,12 +31064,16 @@ var SqliteSecurityRepository = class {
30874
31064
  const now = this.now();
30875
31065
  const windowStart = startOfUtcDay2(now) - (lenDays - 1) * DAY_MS4;
30876
31066
  const rows = this.findingsInRange(windowStart, now);
30877
- const points = Array.from({ length: numBuckets }, (_, i) => ({
30878
- timestamp: toUtcDateString(windowStart + i * bucketMs),
30879
- critical: 0,
30880
- high: 0,
30881
- medium: 0
30882
- }));
31067
+ const points = Array.from(
31068
+ { length: numBuckets },
31069
+ (_, i) => ({
31070
+ timestamp: toUtcDateString(windowStart + i * bucketMs),
31071
+ critical: 0,
31072
+ high: 0,
31073
+ medium: 0,
31074
+ low: 0
31075
+ })
31076
+ );
30883
31077
  for (const r of rows) {
30884
31078
  const idx = Math.floor((r.occurredAt - windowStart) / bucketMs);
30885
31079
  const bucket = points[idx];
@@ -31097,6 +31291,7 @@ var SqliteSecurityRepository = class {
31097
31291
  `SELECT f.finding_key AS finding_key,
31098
31292
  d.rule_id AS rule_id,
31099
31293
  d.severity AS severity,
31294
+ e.repo AS repo,
31100
31295
  e.file_path AS path,
31101
31296
  COALESCE(f.first_detected_at, e.started_at) AS first_detected_at,
31102
31297
  latest.resolved_at AS latest_resolved_at
@@ -31116,6 +31311,7 @@ var SqliteSecurityRepository = class {
31116
31311
  const items = rows.map((r) => ({
31117
31312
  findingKey: r.finding_key,
31118
31313
  ruleId: r.rule_id,
31314
+ repo: r.repo ?? "",
31119
31315
  severity: r.severity,
31120
31316
  path: r.path ?? "",
31121
31317
  resolvedAt: new Date(r.latest_resolved_at).toISOString(),
@@ -31125,13 +31321,66 @@ var SqliteSecurityRepository = class {
31125
31321
  }));
31126
31322
  return Promise.resolve({ items });
31127
31323
  }
31324
+ /**
31325
+ * Per-rule tallies of the findings that are still OPEN, whole-store.
31326
+ *
31327
+ * Scoped by status rather than by time, because the card this feeds is a to-do
31328
+ * list: a secret committed three weeks ago and never rotated is still the most
31329
+ * important thing to fix, and any window hides it. It carried a "newest N
31330
+ * findings" cap and then a range; the first meant a different span on every
31331
+ * machine, and the second reported "no recommendations" over live exposure.
31332
+ *
31333
+ * `open` mirrors `deriveFindingStatus` — at-rest, minus resolved and dismissed —
31334
+ * so a row's count is exactly what `?status=open&type=<rule>` returns. Note that
31335
+ * is NOT `severitySummary`'s `openAtRest`, which keeps dismissed findings (a
31336
+ * dismissal is a judgement, not a remediation) and drops untracked legacy rows.
31337
+ * The two answer different questions and only this one has to match a link.
31338
+ *
31339
+ * Aggregated in SQL: the result is O(distinct rule × category × severity), so a
31340
+ * whole-store scope costs a grouped scan rather than a row per finding.
31341
+ */
31342
+ recommendationInputs() {
31343
+ const rows = allRows(
31344
+ this.db.prepare(
31345
+ `SELECT d.rule_id AS rule_id,
31346
+ d.category AS category,
31347
+ d.severity AS severity,
31348
+ COUNT(*) AS count
31349
+ FROM inspection_findings f
31350
+ JOIN audit_events e ON e.id = f.audit_event_id
31351
+ JOIN inspection_definitions d ON d.id = f.inspection_definition_id
31352
+ LEFT JOIN ${LATEST_RESOLUTION_BY_KEY_SQL} latest
31353
+ ON latest.finding_key = f.finding_key
31354
+ WHERE e.event_type IN (${CAPTURE_EVENT_TYPES_SQL})
31355
+ AND e.event_type = 'code_change'
31356
+ AND (
31357
+ f.finding_key IS NULL
31358
+ OR latest.status IS NULL
31359
+ OR latest.status NOT IN ('resolved', 'dismissed')
31360
+ )
31361
+ GROUP BY d.rule_id, d.category, d.severity`
31362
+ )
31363
+ );
31364
+ return Promise.resolve(
31365
+ rows.map((r) => ({
31366
+ ruleId: r.rule_id,
31367
+ category: r.category,
31368
+ severity: r.severity,
31369
+ count: r.count
31370
+ }))
31371
+ );
31372
+ }
31128
31373
  // Findings whose parent event occurred in [fromMs, toMs), with the parent's
31129
31374
  // epoch-millis timestamp. started_at is an INTEGER column, so the bounds stay
31130
31375
  // numeric and the JS aggregations bucket/split on ms directly.
31131
31376
  findingsInRange(fromMs, toMs) {
31132
31377
  const rows = allRows(
31133
31378
  this.db.prepare(
31134
- `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken
31379
+ // `rule_id`/`category` cost nothing extra: inspection_definitions is already
31380
+ // joined for `severity`, so they are two more columns off a row this read
31381
+ // already fetches. They feed the recommended-actions rollup.
31382
+ `SELECT e.started_at AS occurred_at, d.severity AS severity, f.action_taken AS action_taken,
31383
+ d.rule_id AS rule_id, d.category AS category
31135
31384
  FROM inspection_findings f
31136
31385
  JOIN audit_events e ON e.id = f.audit_event_id
31137
31386
  JOIN inspection_definitions d ON d.id = f.inspection_definition_id
@@ -31144,7 +31393,9 @@ var SqliteSecurityRepository = class {
31144
31393
  return rows.map((r) => ({
31145
31394
  occurredAt: r.occurred_at,
31146
31395
  severity: r.severity,
31147
- actionTaken: r.action_taken
31396
+ actionTaken: r.action_taken,
31397
+ ruleId: r.rule_id,
31398
+ category: r.category
31148
31399
  }));
31149
31400
  }
31150
31401
  };
@@ -32314,8 +32565,45 @@ function openLocalDatabase(dir) {
32314
32565
  };
32315
32566
  }
32316
32567
 
32317
- // ../../packages/persistence/src/finding-key.ts
32568
+ // ../../packages/persistence/src/egress-wire.ts
32318
32569
  import { createHash as createHash3 } from "crypto";
32570
+ function hashProjectKey(projectKey) {
32571
+ return createHash3("sha256").update(projectKey, "utf8").digest("hex");
32572
+ }
32573
+ function toIngestHit(hit) {
32574
+ return {
32575
+ host: hit.host,
32576
+ kind: hit.kind,
32577
+ name: hit.name,
32578
+ category: hit.category,
32579
+ trust: hit.trust,
32580
+ network: hit.network,
32581
+ method: hit.method,
32582
+ transport: hit.transport,
32583
+ url: hit.url,
32584
+ template: hit.template,
32585
+ dataClass: hit.dataClass,
32586
+ site: {
32587
+ file: hit.site.file,
32588
+ line: hit.site.line,
32589
+ dynamic: hit.site.dynamic,
32590
+ vendored: hit.site.vendored
32591
+ }
32592
+ };
32593
+ }
32594
+ function toEgressIngestRequest(input2) {
32595
+ const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
32596
+ const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
32597
+ return {
32598
+ projectKey: hashProjectKey(input2.projectKey),
32599
+ project: input2.project,
32600
+ reconcile,
32601
+ hits: hits.map(toIngestHit)
32602
+ };
32603
+ }
32604
+
32605
+ // ../../packages/persistence/src/finding-key.ts
32606
+ import { createHash as createHash4 } from "crypto";
32319
32607
 
32320
32608
  // ../../packages/persistence/src/fingerprint.ts
32321
32609
  import { createHmac, randomBytes } from "crypto";
@@ -32356,14 +32644,18 @@ function readFingerprintKey(dataDir2) {
32356
32644
  return parseKeyFile(raw);
32357
32645
  }
32358
32646
 
32359
- // ../../packages/persistence/src/history-preview.ts
32647
+ // ../../packages/persistence/src/history-backfill.ts
32360
32648
  import { existsSync as existsSync4 } from "fs";
32361
32649
  import { join as join9 } from "path";
32650
+
32651
+ // ../../packages/persistence/src/history-preview.ts
32652
+ import { existsSync as existsSync5 } from "fs";
32653
+ import { join as join10 } from "path";
32362
32654
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
32363
32655
 
32364
32656
  // ../../packages/persistence/src/store-symlinks.ts
32365
- import { existsSync as existsSync5, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32366
- import { dirname as dirname3, join as join10, resolve } from "path";
32657
+ import { existsSync as existsSync6, lstatSync as lstatSync3, readlinkSync, realpathSync, statSync as statSync4 } from "fs";
32658
+ import { dirname as dirname3, join as join11, resolve } from "path";
32367
32659
 
32368
32660
  // ../../packages/persistence/src/vault/crypto.ts
32369
32661
  import {
@@ -32378,62 +32670,25 @@ import {
32378
32670
  import { execFileSync } from "child_process";
32379
32671
  import { randomBytes as randomBytes2 } from "crypto";
32380
32672
  import { chmodSync as chmodSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync3 } from "fs";
32381
- import { join as join11 } from "path";
32673
+ import { join as join12 } from "path";
32382
32674
 
32383
32675
  // ../../packages/persistence/src/vault/vault.ts
32384
32676
  import { randomBytes as randomBytes3, randomUUID as randomUUID12 } from "crypto";
32385
32677
 
32386
32678
  // ../../packages/persistence/src/warn-era-cap.ts
32387
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
32388
- import { join as join12 } from "path";
32679
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
32680
+ import { join as join13 } from "path";
32389
32681
  var MARKER = "warn-era-capped";
32390
32682
  function capWarnEraEnforcementOnce(db, policyMode, dataDir2) {
32391
32683
  if (policyMode !== "warn") return { capped: 0, skipped: "not-warn" };
32392
- const marker = join12(dataDir2, MARKER);
32393
- if (existsSync6(marker)) return { capped: 0, skipped: "already-run" };
32684
+ const marker = join13(dataDir2, MARKER);
32685
+ if (existsSync7(marker)) return { capped: 0, skipped: "already-run" };
32394
32686
  const capped = db.policies.capCategoryActions();
32395
32687
  writeFileSync4(marker, `${new Date(Date.now()).toISOString()}
32396
32688
  `, { mode: DATA_FILE_MODE });
32397
32689
  return { capped };
32398
32690
  }
32399
32691
 
32400
- // ../../packages/plugin-runtime/src/attached/egress-wire.ts
32401
- import { createHash as createHash4 } from "crypto";
32402
- function hashProjectKey(projectKey) {
32403
- return createHash4("sha256").update(projectKey, "utf8").digest("hex");
32404
- }
32405
- function toIngestHit(hit) {
32406
- return {
32407
- host: hit.host,
32408
- kind: hit.kind,
32409
- name: hit.name,
32410
- category: hit.category,
32411
- trust: hit.trust,
32412
- network: hit.network,
32413
- method: hit.method,
32414
- transport: hit.transport,
32415
- url: hit.url,
32416
- template: hit.template,
32417
- dataClass: hit.dataClass,
32418
- site: {
32419
- file: hit.site.file,
32420
- line: hit.site.line,
32421
- dynamic: hit.site.dynamic,
32422
- vendored: hit.site.vendored
32423
- }
32424
- };
32425
- }
32426
- function toEgressIngestRequest(input2) {
32427
- const { hits, droppedFiles } = capHits(input2.hits, input2.reconcile.mode);
32428
- const reconcile = withoutDroppedFiles(input2.reconcile, droppedFiles);
32429
- return {
32430
- projectKey: hashProjectKey(input2.projectKey),
32431
- project: input2.project,
32432
- reconcile,
32433
- hits: hits.map(toIngestHit)
32434
- };
32435
- }
32436
-
32437
32692
  // ../../packages/remote/src/http.ts
32438
32693
  import { request as httpRequest } from "http";
32439
32694
  import { request as httpsRequest } from "https";
@@ -32713,6 +32968,7 @@ function createRemoteClient(options) {
32713
32968
  url: url2(ROUTES.shares),
32714
32969
  body: JSON.stringify(validated.data)
32715
32970
  });
32971
+ if (response.status === 404) throw new RemoteRouteAbsent(ROUTES.shares);
32716
32972
  okBody(response);
32717
32973
  },
32718
32974
  async pollCommand() {
@@ -32735,19 +32991,51 @@ function createRemoteClient(options) {
32735
32991
  };
32736
32992
  }
32737
32993
 
32738
- // ../../packages/plugin-runtime/src/attached/failure.ts
32994
+ // ../../packages/remote/src/failure-kind.ts
32739
32995
  function statusOf(err) {
32740
32996
  if (typeof err !== "object" || err === null || !("status" in err)) return null;
32741
32997
  const { status } = err;
32742
32998
  if (typeof status !== "number" || !Number.isInteger(status)) return null;
32743
32999
  return status >= 100 && status <= 599 ? status : null;
32744
33000
  }
32745
- function classifyFailure(err) {
32746
- switch (statusOf(err)) {
33001
+ function nameOf(err) {
33002
+ if (typeof err !== "object" || err === null || !("name" in err)) return null;
33003
+ return typeof err.name === "string" ? err.name : null;
33004
+ }
33005
+ function classifyRemoteFailure(err) {
33006
+ switch (nameOf(err)) {
33007
+ case "RemoteRouteAbsent":
33008
+ return "route-absent";
33009
+ case "RemoteRequestInvalid":
33010
+ return "invalid-request";
33011
+ case "RemoteResponseInvalid":
33012
+ return "rejected";
33013
+ default:
33014
+ break;
33015
+ }
33016
+ const status = statusOf(err);
33017
+ if (status === null) return "unreachable";
33018
+ switch (status) {
32747
33019
  case 401:
32748
33020
  return "unauthorized";
32749
33021
  case 403:
32750
33022
  return "forbidden";
33023
+ case 429:
33024
+ return "unreachable";
33025
+ case 404:
33026
+ return "unreachable";
33027
+ default:
33028
+ return status >= 400 && status <= 499 ? "rejected" : "unreachable";
33029
+ }
33030
+ }
33031
+
33032
+ // ../../packages/plugin-runtime/src/attached/failure.ts
33033
+ function classifyFailure(err) {
33034
+ switch (classifyRemoteFailure(err)) {
33035
+ case "unauthorized":
33036
+ return "unauthorized";
33037
+ case "forbidden":
33038
+ return "forbidden";
32751
33039
  default:
32752
33040
  return "unreachable";
32753
33041
  }
@@ -32770,10 +33058,10 @@ function withTimeout(promise2, ms) {
32770
33058
 
32771
33059
  // ../../packages/plugin-runtime/src/attached/forward-drops.ts
32772
33060
  import { readFileSync as readFileSync8 } from "fs";
32773
- import { join as join13 } from "path";
33061
+ import { join as join14 } from "path";
32774
33062
  var FORWARD_DROPS_FILENAME = ATTACHED_FORWARD_DROPS_FILENAME;
32775
33063
  function forwardDropsPath(dataDir2) {
32776
- return join13(dataDir2, FORWARD_DROPS_FILENAME);
33064
+ return join14(dataDir2, FORWARD_DROPS_FILENAME);
32777
33065
  }
32778
33066
  function recordForwardDrops(dataDir2, count, nowMs) {
32779
33067
  if (count <= 0) return;
@@ -32809,13 +33097,13 @@ function readForwardDrops(dataDir2) {
32809
33097
 
32810
33098
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
32811
33099
  import { randomUUID as randomUUID15 } from "crypto";
32812
- import { readFileSync as readFileSync14 } from "fs";
33100
+ import { readFileSync as readFileSync15 } from "fs";
32813
33101
  import { readFile, rename, writeFile } from "fs/promises";
32814
- import { join as join22 } from "path";
33102
+ import { join as join24 } from "path";
32815
33103
 
32816
33104
  // ../../packages/plugin-sdk/src/config.ts
32817
- import { existsSync as existsSync7 } from "fs";
32818
- import { join as join14 } from "path";
33105
+ import { existsSync as existsSync8 } from "fs";
33106
+ import { join as join15 } from "path";
32819
33107
 
32820
33108
  // ../../packages/plugin-sdk/src/provider-env.ts
32821
33109
  var DEFAULT_ANTHROPIC_HOST = "api.anthropic.com";
@@ -32869,8 +33157,8 @@ function resolveProvider() {
32869
33157
  function loadConfig(base = defaultDataDir(), resolveProviderFn = resolveProvider) {
32870
33158
  try {
32871
33159
  ensureLayoutDirSync(base);
32872
- const settingsFile = join14(settingsDir(base), "settings.json");
32873
- if (existsSync7(settingsFile)) tightenFile(settingsFile);
33160
+ const settingsFile = join15(settingsDir(base), "settings.json");
33161
+ if (existsSync8(settingsFile)) tightenFile(settingsFile);
32874
33162
  } catch {
32875
33163
  }
32876
33164
  migrateLegacyLayout(base);
@@ -32895,7 +33183,7 @@ function resolveProviderSafe(resolveProviderFn) {
32895
33183
  // ../../packages/plugin-sdk/src/config-inventory.ts
32896
33184
  import { readdirSync as readdirSync2, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync7 } from "fs";
32897
33185
  import { homedir as homedir2 } from "os";
32898
- import { basename as basename3, join as join16 } from "path";
33186
+ import { basename as basename3, join as join17 } from "path";
32899
33187
 
32900
33188
  // ../../packages/detections/src/egress/registry.ts
32901
33189
  var EXTRACTOR_VERSION = "1";
@@ -35678,24 +35966,20 @@ function bundledDetections() {
35678
35966
  }
35679
35967
 
35680
35968
  // ../../packages/plugin-sdk/src/repo.ts
35681
- import { existsSync as existsSync8, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
35682
- import { basename as basename2, dirname as dirname4, isAbsolute, join as join15, sep as sep2 } from "path";
35969
+ import { existsSync as existsSync9, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
35970
+ import { basename as basename2, dirname as dirname4, isAbsolute, join as join16, sep as sep2 } from "path";
35683
35971
 
35684
35972
  // ../../packages/plugin-sdk/src/events.ts
35685
35973
  import { createHash as createHash5, randomUUID as randomUUID13 } from "crypto";
35686
35974
 
35687
35975
  // ../../packages/plugin-sdk/src/isolated-scan.ts
35688
- import { existsSync as existsSync9 } from "fs";
35976
+ import { existsSync as existsSync10 } from "fs";
35689
35977
  import { fileURLToPath } from "url";
35690
35978
  import { Worker } from "worker_threads";
35691
35979
 
35692
- // ../../packages/plugin-sdk/src/ignore-layers.ts
35693
- var import_ignore = __toESM(require_ignore(), 1);
35694
- import { readFileSync as readFileSync11 } from "fs";
35695
- import { join as join17 } from "path";
35696
-
35697
- // ../../packages/plugin-sdk/src/inventory-resolver.ts
35698
- import { arch, hostname as hostname4, platform, release } from "os";
35980
+ // ../../packages/plugin-sdk/src/host-floor.ts
35981
+ import { readFileSync as readFileSync12 } from "fs";
35982
+ import { join as join19 } from "path";
35699
35983
 
35700
35984
  // ../../packages/plugin-sdk/src/model-governance.ts
35701
35985
  import {
@@ -35703,24 +35987,50 @@ import {
35703
35987
  fstatSync,
35704
35988
  mkdirSync as mkdirSync2,
35705
35989
  openSync as openSync2,
35706
- readFileSync as readFileSync12,
35990
+ readFileSync as readFileSync11,
35707
35991
  readSync,
35708
35992
  writeFileSync as writeFileSync5
35709
35993
  } from "fs";
35710
35994
  import { join as join18 } from "path";
35711
35995
  var TAIL_BYTES = 256 * 1024;
35712
35996
 
35997
+ // ../../packages/plugin-sdk/src/host-floor.ts
35998
+ var HOST_FEATURE = {
35999
+ ModelSwitch: "model-switch",
36000
+ VaultPointerDisplay: "vault-pointer-display"
36001
+ };
36002
+ var HOST_FLOORS = {
36003
+ [HOST_FEATURE.ModelSwitch]: {
36004
+ label: "model-switch protection",
36005
+ hookEvents: ["PreModelSwitch", "PostModelSwitch"],
36006
+ since: "2.1.251"
36007
+ },
36008
+ [HOST_FEATURE.VaultPointerDisplay]: {
36009
+ label: "vault pointer display",
36010
+ hookEvents: ["MessageDisplay"],
36011
+ since: "2.1.152"
36012
+ }
36013
+ };
36014
+
36015
+ // ../../packages/plugin-sdk/src/ignore-layers.ts
36016
+ var import_ignore = __toESM(require_ignore(), 1);
36017
+ import { readFileSync as readFileSync13 } from "fs";
36018
+ import { join as join20 } from "path";
36019
+
36020
+ // ../../packages/plugin-sdk/src/inventory-resolver.ts
36021
+ import { arch, hostname as hostname4, platform, release } from "os";
36022
+
35713
36023
  // ../../packages/plugin-sdk/src/nudge.ts
35714
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
35715
- import { join as join19 } from "path";
36024
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
36025
+ import { join as join21 } from "path";
35716
36026
 
35717
36027
  // ../../packages/plugin-sdk/src/paths.ts
35718
36028
  import { readdirSync as readdirSync3, realpathSync as realpathSync3 } from "fs";
35719
36029
  import { basename as basename4, dirname as dirname5, sep as sep3 } from "path";
35720
36030
 
35721
36031
  // ../../packages/plugin-sdk/src/project-files.ts
35722
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
35723
- import { basename as basename5, join as join20 } from "path";
36032
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
36033
+ import { basename as basename5, join as join22 } from "path";
35724
36034
 
35725
36035
  // ../../packages/plugin-sdk/src/provider-env-antigravity.ts
35726
36036
  var optionalBaseUrl2 = external_exports.preprocess((v) => {
@@ -35756,7 +36066,7 @@ var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
35756
36066
 
35757
36067
  // ../../packages/plugin-sdk/src/throttle.ts
35758
36068
  import { mkdirSync as mkdirSync4, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
35759
- import { join as join21 } from "path";
36069
+ import { join as join23 } from "path";
35760
36070
 
35761
36071
  // ../../packages/plugin-runtime/src/attached/forward-policy.ts
35762
36072
  function isInvalidRequest(err) {
@@ -35796,7 +36106,7 @@ function parseBreakerState(raw, nowMs) {
35796
36106
  }
35797
36107
  function createForwardPolicy(deps) {
35798
36108
  const now = deps.now ?? (() => Date.now());
35799
- const file2 = join22(deps.dir, STATE_FILENAME);
36109
+ const file2 = join24(deps.dir, STATE_FILENAME);
35800
36110
  let state = null;
35801
36111
  let loading = null;
35802
36112
  async function readState() {
@@ -36518,8 +36828,8 @@ function toolAuditEvent(input2) {
36518
36828
  }
36519
36829
 
36520
36830
  // ../../packages/plugin-runtime/src/attached/history-state.ts
36521
- import { readFileSync as readFileSync15 } from "fs";
36522
- import { join as join23 } from "path";
36831
+ import { readFileSync as readFileSync16 } from "fs";
36832
+ import { join as join25 } from "path";
36523
36833
 
36524
36834
  // ../../packages/plugin-runtime/src/attached/history-sync.ts
36525
36835
  import { createHash as createHash6 } from "crypto";
@@ -36536,7 +36846,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
36536
36846
  var HISTORY_SYNC_THROTTLE_MS = 5 * 60 * 1e3;
36537
36847
 
36538
36848
  // ../../packages/plugin-runtime/src/attached/plugin-block.ts
36539
- import { readFileSync as readFileSync16 } from "fs";
36849
+ import { readFileSync as readFileSync17 } from "fs";
36540
36850
  function createPluginBlock(build, policyStore) {
36541
36851
  return async () => {
36542
36852
  const cached2 = await policyStore.read();
@@ -36555,7 +36865,7 @@ function createPluginBlock(build, policyStore) {
36555
36865
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
36556
36866
  import { randomUUID as randomUUID16 } from "crypto";
36557
36867
  import { readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
36558
- import { join as join24 } from "path";
36868
+ import { join as join26 } from "path";
36559
36869
 
36560
36870
  // ../../packages/plugin-runtime/src/attached/atomic-publish.ts
36561
36871
  import { rename as rename2 } from "fs/promises";
@@ -36579,7 +36889,7 @@ async function publishByRename(tmp, file2, move = rename2) {
36579
36889
 
36580
36890
  // ../../packages/plugin-runtime/src/attached/policy-store.ts
36581
36891
  function createPolicyStore(dir = dataDir()) {
36582
- const file2 = join24(dir, "policy-cache.json");
36892
+ const file2 = join26(dir, "policy-cache.json");
36583
36893
  async function read() {
36584
36894
  try {
36585
36895
  const raw = await readFile2(file2, "utf8");
@@ -36810,11 +37120,11 @@ function readStorePosture(dbPath2) {
36810
37120
  // ../../packages/plugin-runtime/src/attached/posture-store.ts
36811
37121
  import { randomUUID as randomUUID17 } from "crypto";
36812
37122
  import { readFile as readFile3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
36813
- import { join as join25 } from "path";
37123
+ import { join as join27 } from "path";
36814
37124
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
36815
37125
  function createPostureStore(dir = settingsDir(), legacyDir) {
36816
- const file2 = join25(dir, "posture-state.json");
36817
- const legacyFile = legacyDir === void 0 ? null : join25(legacyDir, "posture-state.json");
37126
+ const file2 = join27(dir, "posture-state.json");
37127
+ const legacyFile = legacyDir === void 0 ? null : join27(legacyDir, "posture-state.json");
36818
37128
  async function persist(state) {
36819
37129
  await ensureDataDir(dir);
36820
37130
  const tmp = `${file2}.${randomUUID17()}.tmp`;
@@ -36882,8 +37192,8 @@ function createPostureStore(dir = settingsDir(), legacyDir) {
36882
37192
  }
36883
37193
 
36884
37194
  // ../../packages/plugin-runtime/src/attached/sync-state.ts
36885
- import { readFileSync as readFileSync17 } from "fs";
36886
- import { join as join26 } from "path";
37195
+ import { readFileSync as readFileSync18 } from "fs";
37196
+ import { join as join28 } from "path";
36887
37197
 
36888
37198
  // ../../packages/plugin-runtime/src/attached/status.ts
36889
37199
  var REFUSAL_LINES = {
@@ -37467,7 +37777,7 @@ function show(body) {
37467
37777
 
37468
37778
  // ../../packages/setup-wizard/src/remediation/rotation-checklist.ts
37469
37779
  import { writeFileSync as writeFileSync8 } from "fs";
37470
- import { join as join27 } from "path";
37780
+ import { join as join29 } from "path";
37471
37781
 
37472
37782
  // ../../packages/setup-wizard/src/triage/merge.ts
37473
37783
  var RANK = Object.fromEntries(
@@ -37475,9 +37785,9 @@ var RANK = Object.fromEntries(
37475
37785
  );
37476
37786
 
37477
37787
  // ../../packages/setup-wizard/src/triage/plan-file.ts
37478
- import { mkdtempSync, readFileSync as readFileSync18, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
37788
+ import { mkdtempSync, readFileSync as readFileSync19, rmdirSync, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "fs";
37479
37789
  import { tmpdir } from "os";
37480
- import { basename as basename6, dirname as dirname6, join as join28 } from "path";
37790
+ import { basename as basename6, dirname as dirname6, join as join30 } from "path";
37481
37791
  var SuppressionEntrySchema = external_exports.object({
37482
37792
  ruleId: external_exports.string(),
37483
37793
  category: DetectionCategory,
@@ -37520,7 +37830,6 @@ var PersistedPlanSchema = external_exports.object({
37520
37830
 
37521
37831
  // src/render.ts
37522
37832
  var STORE_UNAVAILABLE_NOTE = "I couldn't check my records just now \u2014 we can check again soon. Your Claude session keeps going, and I'll fill in as you work.";
37523
- var SEVERITY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
37524
37833
  var SEVERITY_GLYPH = {
37525
37834
  critical: SHADE.full,
37526
37835
  high: SHADE.dark,
@@ -37530,15 +37839,6 @@ var SEVERITY_GLYPH = {
37530
37839
  function severityGlyph(severity) {
37531
37840
  return SEVERITY_GLYPH[severity] ?? SHADE.light;
37532
37841
  }
37533
- var ADVICE = {
37534
- secret: "Rotate the exposed credentials and move them out of prompts (secrets manager / env vars).",
37535
- pii: "Remove or mask personal data before it reaches the model.",
37536
- financial: "Strip card and account numbers; share only non-sensitive references.",
37537
- phi: "Remove protected health information \u2014 it should never reach an external model.",
37538
- code_context: "Confirm this proprietary code context is safe to share.",
37539
- code_flaw: "Review the flagged pattern and apply the secure alternative (parameterized queries, safe deserializers, etc.).",
37540
- custom: "Review against your organization\u2019s custom policy."
37541
- };
37542
37842
  var ACTION_LABEL = {
37543
37843
  log: "monitor",
37544
37844
  warn: "warn",
@@ -37556,15 +37856,10 @@ function renderPosture(rows) {
37556
37856
  return [...rows].sort((a, b) => categoryRank(a.category) - categoryRank(b.category)).map((r) => ` ${r.category.padEnd(width)} ${ACTION_LABEL[r.action] ?? r.action}`).join("\n");
37557
37857
  }
37558
37858
  var RULE_WIDTH = 64;
37559
- function healthScore(summary) {
37560
- const handled = summary.byAction.block + summary.byAction.redact + summary.byAction.warn;
37561
- const handledRatio = summary.findings === 0 ? 1 : handled / summary.findings;
37562
- return Math.round(100 * (0.6 * summary.coverage + 0.4 * handledRatio));
37563
- }
37564
37859
  var TRY_COMMANDS = ["/aka:dashboard", "/aka:scan"];
37565
37860
  function topFindings(findings, limit = 10) {
37566
37861
  return [...findings].sort((a, b) => {
37567
- const sev = (SEVERITY_WEIGHT[b.severity] ?? 0) - (SEVERITY_WEIGHT[a.severity] ?? 0);
37862
+ const sev = severityWeight(b.severity) - severityWeight(a.severity);
37568
37863
  return sev !== 0 ? sev : b.occurredAt.localeCompare(a.occurredAt);
37569
37864
  }).slice(0, limit);
37570
37865
  }
@@ -37624,45 +37919,6 @@ function renderFirstRun(s, registry2) {
37624
37919
  }
37625
37920
  return lines.join("\n");
37626
37921
  }
37627
- var REC_TEMPLATE = {
37628
- secret: { title: "Exposed secret detected", action: "Rotate" },
37629
- pii: { title: "Personal data in a prompt", action: "Remove" },
37630
- financial: { title: "Financial data detected", action: "Strip" },
37631
- phi: { title: "Health information detected", action: "Remove" },
37632
- code_context: { title: "Proprietary code shared", action: "Review" },
37633
- custom: { title: "Custom policy match", action: "Review" }
37634
- };
37635
- var MAX_RECOMMENDATIONS = 10;
37636
- function buildRecommendations(findings) {
37637
- const buckets = /* @__PURE__ */ new Map();
37638
- for (const f of findings) {
37639
- const b = buckets.get(f.category) ?? {
37640
- category: f.category,
37641
- count: 0,
37642
- severity: f.severity,
37643
- weight: 0,
37644
- ruleId: f.ruleId
37645
- };
37646
- b.count++;
37647
- const w = SEVERITY_WEIGHT[f.severity] ?? 0;
37648
- if (w > b.weight) {
37649
- b.weight = w;
37650
- b.severity = f.severity;
37651
- b.ruleId = f.ruleId;
37652
- }
37653
- buckets.set(f.category, b);
37654
- }
37655
- return [...buckets.values()].sort((a, b) => b.weight - a.weight || b.count - a.count).slice(0, MAX_RECOMMENDATIONS).map((b) => {
37656
- const t = REC_TEMPLATE[b.category] ?? { title: `${b.category} finding`, action: "Review" };
37657
- return {
37658
- severity: b.severity,
37659
- title: t.title,
37660
- description: ADVICE[b.category] ?? "Review this finding against your policy.",
37661
- context: `${b.ruleId} \xB7 ${String(b.count)} finding${b.count === 1 ? "" : "s"}`,
37662
- action: t.action
37663
- };
37664
- });
37665
- }
37666
37922
 
37667
37923
  // src/firstrun-core.ts
37668
37924
  function parseSurfacedCount(argv) {