pi-lens 4.1.1 → 4.1.3

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.
Files changed (290) hide show
  1. package/CHANGELOG.md +416 -1
  2. package/config/dependency-cruiser-eager-allowlist.json +79 -0
  3. package/dist/clients/actionable-warnings-logger.js +18 -2
  4. package/dist/clients/actionable-warnings.js +14 -5
  5. package/dist/clients/advisory-provenance.js +48 -18
  6. package/dist/clients/ast-grep-client.js +14 -9
  7. package/dist/clients/atomic-write-staging.js +16 -4
  8. package/dist/clients/availability-probe-flight.js +12 -0
  9. package/dist/clients/bash-file-access.js +3 -1
  10. package/dist/clients/biome-client.js +8 -3
  11. package/dist/clients/blocker-freshness.js +41 -8
  12. package/dist/clients/bounded-cache.js +24 -8
  13. package/dist/clients/bounded-pid-file-lock.js +4 -3
  14. package/dist/clients/bounded-telemetry.js +56 -19
  15. package/dist/clients/build-identity.js +95 -0
  16. package/dist/clients/bus-publish.js +3 -0
  17. package/dist/clients/cache/rule-cache.js +2 -1
  18. package/dist/clients/cache-manager.js +6 -7
  19. package/dist/clients/cache-observability.js +463 -67
  20. package/dist/clients/call-graph.js +238 -63
  21. package/dist/clients/cascade-logger.js +15 -1
  22. package/dist/clients/codebase-model.js +13 -5
  23. package/dist/clients/complexity-client.js +1 -3
  24. package/dist/clients/dead-code-client.js +33 -19
  25. package/dist/clients/dead-code-logger.js +1 -2
  26. package/dist/clients/deadline-utils.js +3 -1
  27. package/dist/clients/debug-heap.js +8 -1
  28. package/dist/clients/degradation-ledger.js +57 -5
  29. package/dist/clients/demoted-finding-render.js +14 -0
  30. package/dist/clients/dependency-checker.js +31 -10
  31. package/dist/clients/deps/minimatch.js +1 -1
  32. package/dist/clients/diagnostic-dispositions.js +39 -2
  33. package/dist/clients/diagnostic-line-freshness.js +5 -1
  34. package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
  35. package/dist/clients/dispatch/collect-later-tier.js +39 -0
  36. package/dist/clients/dispatch/dispatcher.js +163 -10
  37. package/dist/clients/dispatch/fact-store.js +231 -9
  38. package/dist/clients/dispatch/facts/function-facts.js +3 -1
  39. package/dist/clients/dispatch/facts/import-facts.js +2 -1
  40. package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
  41. package/dist/clients/dispatch/integration.js +214 -104
  42. package/dist/clients/dispatch/pending-runner-findings.js +87 -0
  43. package/dist/clients/dispatch/plan.js +21 -5
  44. package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
  45. package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
  46. package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
  47. package/dist/clients/dispatch/runners/actionlint.js +5 -2
  48. package/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
  49. package/dist/clients/dispatch/runners/biome-check.js +10 -14
  50. package/dist/clients/dispatch/runners/cpp-check.js +5 -1
  51. package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
  52. package/dist/clients/dispatch/runners/elixir-check.js +3 -1
  53. package/dist/clients/dispatch/runners/eslint.js +30 -7
  54. package/dist/clients/dispatch/runners/fish-indent.js +1 -1
  55. package/dist/clients/dispatch/runners/gleam-check.js +1 -1
  56. package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
  57. package/dist/clients/dispatch/runners/hadolint.js +13 -8
  58. package/dist/clients/dispatch/runners/helm-lint.js +20 -4
  59. package/dist/clients/dispatch/runners/helm-render.js +54 -9
  60. package/dist/clients/dispatch/runners/htmlhint.js +25 -10
  61. package/dist/clients/dispatch/runners/index.js +1 -3
  62. package/dist/clients/dispatch/runners/javac.js +1 -1
  63. package/dist/clients/dispatch/runners/ktlint.js +1 -1
  64. package/dist/clients/dispatch/runners/lsp.js +38 -2
  65. package/dist/clients/dispatch/runners/markdownlint.js +32 -5
  66. package/dist/clients/dispatch/runners/mypy.js +6 -8
  67. package/dist/clients/dispatch/runners/oxlint.js +219 -11
  68. package/dist/clients/dispatch/runners/php-lint.js +1 -1
  69. package/dist/clients/dispatch/runners/phpstan.js +8 -4
  70. package/dist/clients/dispatch/runners/psscriptanalyzer.js +14 -6
  71. package/dist/clients/dispatch/runners/pyright.js +23 -14
  72. package/dist/clients/dispatch/runners/rust-clippy.js +9 -4
  73. package/dist/clients/dispatch/runners/shellcheck.js +7 -9
  74. package/dist/clients/dispatch/runners/shfmt.js +5 -2
  75. package/dist/clients/dispatch/runners/sqlfluff.js +7 -7
  76. package/dist/clients/dispatch/runners/stylelint.js +6 -8
  77. package/dist/clients/dispatch/runners/terragrunt.js +13 -8
  78. package/dist/clients/dispatch/runners/tflint.js +13 -8
  79. package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
  80. package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
  81. package/dist/clients/dispatch/runners/utils/runner-helpers.js +100 -26
  82. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  83. package/dist/clients/dispatch/runners/utils/tool-failure.js +54 -7
  84. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
  85. package/dist/clients/dispatch/runners/yaml-rule-parser.js +52 -20
  86. package/dist/clients/dispatch/runners/yamllint.js +6 -8
  87. package/dist/clients/dispatch/runners/zig-check.js +1 -1
  88. package/dist/clients/dispatch/suppress-writer.js +2 -1
  89. package/dist/clients/dispatch/types.js +6 -1
  90. package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
  91. package/dist/clients/event-loop-monitor.js +66 -1
  92. package/dist/clients/extension-log.js +5 -3
  93. package/dist/clients/feature-hints.js +8 -1
  94. package/dist/clients/file-kinds.js +34 -129
  95. package/dist/clients/file-utils.js +174 -8
  96. package/dist/clients/finding-delivery-gate.js +62 -2
  97. package/dist/clients/formatters.js +169 -27
  98. package/dist/clients/freshness.js +38 -0
  99. package/dist/clients/git-guard.js +233 -53
  100. package/dist/clients/git-tracked-ignore.js +32 -1
  101. package/dist/clients/go-client.js +1 -3
  102. package/dist/clients/govulncheck-client.js +5 -1
  103. package/dist/clients/grammar-source.js +1 -1
  104. package/dist/clients/gzip-stage-write.js +37 -4
  105. package/dist/clients/host-edit-normalize.js +1 -3
  106. package/dist/clients/install-diagnostics.js +4 -2
  107. package/dist/clients/installer/index.js +211 -105
  108. package/dist/clients/installer/managed-tool-refresh.js +3 -1
  109. package/dist/clients/instance-reaper.js +9 -15
  110. package/dist/clients/instance-registry-lock.js +145 -0
  111. package/dist/clients/instance-registry.js +436 -124
  112. package/dist/clients/jscpd-client.js +12 -2
  113. package/dist/clients/knip-client.js +29 -15
  114. package/dist/clients/language-profile.js +8 -3
  115. package/dist/clients/latency-logger.js +32 -3
  116. package/dist/clients/lens-config.js +1 -1
  117. package/dist/clients/lens-flag-registry.js +8 -0
  118. package/dist/clients/lens-map.js +11 -4
  119. package/dist/clients/live-bus-emitter.js +1 -1
  120. package/dist/clients/lsp/cascade-tier.js +4 -0
  121. package/dist/clients/lsp/client.js +516 -31
  122. package/dist/clients/lsp/config.js +29 -8
  123. package/dist/clients/lsp/diagnostic-binding.js +9 -4
  124. package/dist/clients/lsp/edits.js +119 -32
  125. package/dist/clients/lsp/index.js +595 -209
  126. package/dist/clients/lsp/inferred-project.js +2 -1
  127. package/dist/clients/lsp/jvm-runtime.js +9 -1
  128. package/dist/clients/lsp/launch.js +47 -11
  129. package/dist/clients/lsp/pending-aux-coverage.js +264 -0
  130. package/dist/clients/lsp/server.js +161 -20
  131. package/dist/clients/lsp/session-roots.js +99 -0
  132. package/dist/clients/lsp/tsserver-sync.js +10 -2
  133. package/dist/clients/lsp/workspace-diagnostics-cache.js +25 -10
  134. package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  135. package/dist/clients/lsp-document-symbols.js +1 -3
  136. package/dist/clients/lsp-mutation.js +26 -22
  137. package/dist/clients/mcp/analyze.js +12 -6
  138. package/dist/clients/mcp/host-shim.js +1 -1
  139. package/dist/clients/mcp/session.js +14 -1
  140. package/dist/clients/memory-sampler.js +96 -8
  141. package/dist/clients/message-end-attribution.js +30 -0
  142. package/dist/clients/metrics-client.js +1 -3
  143. package/dist/clients/metrics-history.js +21 -4
  144. package/dist/clients/middle-man-analysis.js +8 -3
  145. package/dist/clients/module-report-lsp.js +7 -1
  146. package/dist/clients/module-report.js +14 -17
  147. package/dist/clients/ndjson-logger.js +3 -5
  148. package/dist/clients/opaque-mutation-scan.js +434 -0
  149. package/dist/clients/package-manager.js +12 -19
  150. package/dist/clients/path-keyed-map.js +14 -1
  151. package/dist/clients/path-utils.js +32 -0
  152. package/dist/clients/pipeline.js +49 -9
  153. package/dist/clients/process-singletons.js +210 -0
  154. package/dist/clients/project-conventions.js +2 -8
  155. package/dist/clients/project-diagnostics/cache.js +10 -7
  156. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
  157. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
  158. package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  159. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
  160. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
  161. package/dist/clients/project-diagnostics/scanner.js +15 -6
  162. package/dist/clients/project-lens-config.js +12 -5
  163. package/dist/clients/project-snapshot-fingerprint.js +48 -0
  164. package/dist/clients/project-snapshot-persist-worker.js +15 -1
  165. package/dist/clients/project-snapshot.js +505 -77
  166. package/dist/clients/read-expansion.js +12 -2
  167. package/dist/clients/read-guard-logger.js +29 -6
  168. package/dist/clients/read-guard-tool-lines.js +5 -2
  169. package/dist/clients/read-guard.js +75 -8
  170. package/dist/clients/recent-touches.js +17 -5
  171. package/dist/clients/resource-sampler.js +1 -1
  172. package/dist/clients/reverse-deps.js +6 -5
  173. package/dist/clients/review-graph/builder.js +680 -78
  174. package/dist/clients/review-graph/git-identity.js +8 -2
  175. package/dist/clients/review-graph/import-resolvers.js +10 -1
  176. package/dist/clients/review-graph/query.js +2 -1
  177. package/dist/clients/review-graph/tsconfig-paths.js +9 -3
  178. package/dist/clients/review-graph/workspace-modules.js +2 -2
  179. package/dist/clients/review-graph-logger.js +21 -1
  180. package/dist/clients/ruff-client.js +6 -5
  181. package/dist/clients/runtime-agent-end.js +63 -31
  182. package/dist/clients/runtime-context.js +12 -4
  183. package/dist/clients/runtime-coordinator.js +151 -4
  184. package/dist/clients/runtime-session.js +44 -12
  185. package/dist/clients/runtime-tool-call.js +74 -3
  186. package/dist/clients/runtime-tool-result.js +250 -37
  187. package/dist/clients/runtime-turn.js +373 -17
  188. package/dist/clients/rust-client.js +1 -3
  189. package/dist/clients/safe-spawn.js +308 -29
  190. package/dist/clients/security-scan-client.js +49 -20
  191. package/dist/clients/session-lifecycle.js +238 -42
  192. package/dist/clients/session-summary.js +3 -1
  193. package/dist/clients/sg-runner.js +28 -18
  194. package/dist/clients/shared-checkout-guard.js +338 -0
  195. package/dist/clients/source-filter.js +10 -6
  196. package/dist/clients/source-walker.js +1 -1
  197. package/dist/clients/spawn-output-cap.js +37 -0
  198. package/dist/clients/spawn-timeout-cooldown.js +110 -0
  199. package/dist/clients/startup-scan.js +10 -8
  200. package/dist/clients/startup-timing.js +69 -4
  201. package/dist/clients/string-utils.js +20 -0
  202. package/dist/clients/test-runner-client.js +419 -88
  203. package/dist/clients/tool-definition.js +7 -0
  204. package/dist/clients/tool-policy.js +27 -12
  205. package/dist/clients/tool-render.js +2 -1
  206. package/dist/clients/tree-sitter-client.js +21 -9
  207. package/dist/clients/tree-sitter-logger.js +19 -3
  208. package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
  209. package/dist/clients/warm-attach.js +19 -14
  210. package/dist/clients/widget-state.js +27 -11
  211. package/dist/clients/word-index-logger.js +13 -1
  212. package/dist/clients/word-index-store.js +448 -0
  213. package/dist/clients/word-index.js +585 -105
  214. package/dist/clients/workspace-topology.js +25 -2
  215. package/dist/clients/zizmor-config.js +34 -6
  216. package/dist/i18n.js +13 -5
  217. package/dist/index.js +19617 -12855
  218. package/dist/mcp/build-staleness.js +8 -7
  219. package/dist/mcp/cli.js +3 -1
  220. package/dist/mcp/server.js +9 -2
  221. package/dist/tools/ast-grep-replace.js +68 -12
  222. package/dist/tools/lens-diagnostic-mark.js +21 -7
  223. package/dist/tools/lens-diagnostics.js +168 -35
  224. package/dist/tools/lsp-diagnostics.js +38 -12
  225. package/dist/tools/lsp-navigation.js +6 -2
  226. package/dist/tools/module-report.js +1 -1
  227. package/dist/tools/project-report.js +1 -1
  228. package/dist/tools/symbol-search.js +5 -2
  229. package/docs/agent-guide.md +4 -2
  230. package/docs/ast-grep_rules_catalog.md +41 -10
  231. package/docs/astplayground.md +26 -17
  232. package/docs/durable-store-audit-1202.md +1 -1
  233. package/docs/servercapabilities.md +1 -1
  234. package/package.json +164 -156
  235. package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  236. package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  237. package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  238. package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  239. package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  240. package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  241. package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  242. package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  243. package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  244. package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  245. package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  246. package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  247. package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  248. package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  249. package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  250. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  251. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  252. package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  253. package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  254. package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  255. package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  256. package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  257. package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  258. package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  259. package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  260. package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  261. package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  262. package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  263. package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  264. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  265. package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  266. package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  267. package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  268. package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  269. package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  270. package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  271. package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  272. package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  273. package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  274. package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  275. package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  276. package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  277. package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  278. package/rules/ast-grep-rules/rules/no-important.yml +21 -0
  279. package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  280. package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  281. package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  282. package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  283. package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  284. package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  285. package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  286. package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  287. package/rules/typos/_typos.toml +2 -2
  288. package/scripts/analyze-pi-lens-logs.mjs +31 -13
  289. package/scripts/download-grammars.js +9 -3
  290. package/scripts/rpc-load-check.mjs +30 -9
@@ -2,20 +2,36 @@ import * as path from "node:path";
2
2
  import { isTestMode } from "./env-utils.js";
3
3
  import { getGlobalPiLensDir } from "./file-utils.js";
4
4
  import { createNdjsonLogger } from "./ndjson-logger.js";
5
+ import { normalizeFilePath } from "./path-utils.js";
5
6
  const AW_LOG_DIR = getGlobalPiLensDir();
6
7
  const AW_LOG_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log");
7
8
  const AW_LOG_BACKUP_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log.1");
8
- const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) || 1048576);
9
+ const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) ||
10
+ 1048576);
9
11
  const writer = createNdjsonLogger({
10
12
  filePath: AW_LOG_FILE,
11
13
  maxBytes: MAX_LOG_BYTES,
12
14
  backupPath: AW_LOG_BACKUP_FILE,
13
15
  });
16
+ /**
17
+ * #2219 (the #2141 class): `filePath` reaches here from
18
+ * `actionable-warnings.ts`'s raw `path.resolve(cwd, file)` — the file
19
+ * imports `normalizeMapKey` and uses it for map lookups on this same value,
20
+ * but never normalizes what gets logged. `filePath` is optional (several
21
+ * events, e.g. `report_started`/`report_complete`, carry none), so only
22
+ * normalize when present.
23
+ */
14
24
  export function logActionableWarningsEvent(entry) {
15
25
  if (isTestMode()) {
16
26
  return;
17
27
  }
18
- writer.log({ ts: new Date().toISOString(), ...entry });
28
+ writer.log({
29
+ ts: new Date().toISOString(),
30
+ ...entry,
31
+ ...(entry.filePath !== undefined
32
+ ? { filePath: normalizeFilePath(entry.filePath) }
33
+ : {}),
34
+ });
19
35
  }
20
36
  export function getActionableWarningsLogPath() {
21
37
  return AW_LOG_FILE;
@@ -633,7 +633,12 @@ export async function applyConservativeActionableWarningFixes(args) {
633
633
  }
634
634
  const edit = selected.edit;
635
635
  const applied = await applyWorkspaceEdit(edit, args.cwd, args.mutationContext
636
- ? { mutationContext: { ...args.mutationContext, emitSummary: false } }
636
+ ? {
637
+ mutationContext: {
638
+ ...args.mutationContext,
639
+ emitSummary: false,
640
+ },
641
+ }
637
642
  : undefined);
638
643
  appliedResults.push(applied);
639
644
  for (const changedFile of applied.files)
@@ -642,8 +647,7 @@ export async function applyConservativeActionableWarningFixes(args) {
642
647
  }
643
648
  catch (err) {
644
649
  failedCount++;
645
- const partial = err
646
- .appliedWorkspaceEdit;
650
+ const partial = err.appliedWorkspaceEdit;
647
651
  if (partial) {
648
652
  appliedResults.push(partial);
649
653
  for (const changedFile of partial.files)
@@ -656,13 +660,18 @@ export async function applyConservativeActionableWarningFixes(args) {
656
660
  }
657
661
  }
658
662
  summary.changedFiles = [...changedFiles];
659
- if (args.mutationContext && (summary.considered > 0 || appliedResults.length > 0)) {
663
+ if (args.mutationContext &&
664
+ (summary.considered > 0 || appliedResults.length > 0)) {
660
665
  recordLspMutationBatch(args.mutationContext, {
661
666
  results: appliedResults,
662
667
  considered: summary.considered,
663
668
  completed: summary.applied,
664
669
  failedCount,
665
- status: failedCount > 0 ? "failed" : appliedResults.length > 0 ? "success" : "skipped",
670
+ status: failedCount > 0
671
+ ? "failed"
672
+ : appliedResults.length > 0
673
+ ? "success"
674
+ : "skipped",
666
675
  bookkeep: false,
667
676
  });
668
677
  }
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { logLatency } from "./latency-logger.js";
5
5
  import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
6
6
  import { resolveRunnerPath } from "./dispatch/runner-context.js";
7
- import { MTIME_DRIFT_TOLERANCE_MS } from "./blocker-freshness.js";
7
+ import { freshnessFromMtime } from "./freshness.js";
8
8
  export const MAX_ADVISORY_AFFECTED_FILES = 256;
9
9
  export function advisoryPathKey(filePath, cwd) {
10
10
  return normalizeMapKey(path.resolve(cwd, filePath));
@@ -68,25 +68,41 @@ function isFiniteNumber(value) {
68
68
  return typeof value === "number" && Number.isFinite(value);
69
69
  }
70
70
  function isCapturedHash(value) {
71
- return typeof value === "string" &&
72
- (/^[a-f0-9]{64}$/.test(value) || value === "missing" || value.startsWith("unreadable:"));
71
+ return (typeof value === "string" &&
72
+ (/^[a-f0-9]{64}$/.test(value) ||
73
+ value === "missing" ||
74
+ value.startsWith("unreadable:")));
73
75
  }
74
76
  function isWellFormed(value) {
75
77
  if (!value || typeof value !== "object")
76
78
  return false;
77
79
  const record = value;
78
80
  const revision = record.revision;
79
- return !!revision && typeof revision.sessionId === "string" &&
80
- isFiniteNumber(revision.projectSeq) && isFiniteNumber(revision.turnIndex) &&
81
- isFiniteNumber(revision.generation) && isFiniteNumber(revision.capturedAt) &&
82
- Array.isArray(record.files) && record.files.length > 0 && record.files.every((file) => !!file && typeof file.path === "string" &&
83
- (file.role === "source" || file.role === "test" || file.role === "affected") &&
84
- isFiniteNumber(file.mtimeMs) && isFiniteNumber(file.size) &&
85
- isCapturedHash(file.sha256));
81
+ return (!!revision &&
82
+ typeof revision.sessionId === "string" &&
83
+ isFiniteNumber(revision.projectSeq) &&
84
+ isFiniteNumber(revision.turnIndex) &&
85
+ isFiniteNumber(revision.generation) &&
86
+ isFiniteNumber(revision.capturedAt) &&
87
+ Array.isArray(record.files) &&
88
+ record.files.length > 0 &&
89
+ record.files.every((file) => !!file &&
90
+ typeof file.path === "string" &&
91
+ (file.role === "source" ||
92
+ file.role === "test" ||
93
+ file.role === "affected") &&
94
+ isFiniteNumber(file.mtimeMs) &&
95
+ isFiniteNumber(file.size) &&
96
+ isCapturedHash(file.sha256)));
86
97
  }
87
98
  export function validateAdvisoryProvenance(record, cwd, runtime) {
88
99
  if (!isWellFormed(record.provenance)) {
89
- return { status: "unknown", reasons: ["malformed-or-legacy-provenance"], allFilesDeleted: false, changedPathCount: 0 };
100
+ return {
101
+ status: "unknown",
102
+ reasons: ["malformed-or-legacy-provenance"],
103
+ allFilesDeleted: false,
104
+ changedPathCount: 0,
105
+ };
90
106
  }
91
107
  const provenance = record.provenance;
92
108
  const reasons = [];
@@ -149,9 +165,19 @@ export function validateAdvisoryProvenance(record, cwd, runtime) {
149
165
  }
150
166
  const allFilesDeleted = deletedFiles === provenance.files.length;
151
167
  if (unknown)
152
- return { status: "unknown", reasons, allFilesDeleted, changedPathCount: changedPaths.size };
168
+ return {
169
+ status: "unknown",
170
+ reasons,
171
+ allFilesDeleted,
172
+ changedPathCount: changedPaths.size,
173
+ };
153
174
  return reasons.length > 0
154
- ? { status: "superseded", reasons, allFilesDeleted, changedPathCount: changedPaths.size }
175
+ ? {
176
+ status: "superseded",
177
+ reasons,
178
+ allFilesDeleted,
179
+ changedPathCount: changedPaths.size,
180
+ }
155
181
  : { status: "current", reasons: [], allFilesDeleted, changedPathCount: 0 };
156
182
  }
157
183
  // ── Finding-cited path existence (#1461 slice 1) ──────────────────────────────
@@ -224,11 +250,15 @@ const MAX_LOGGED_DEAD_PATHS = 3;
224
250
  export function findingPathFreshness(resolvedPath, scannedAtMs) {
225
251
  try {
226
252
  const stat = fs.statSync(resolvedPath);
227
- if (scannedAtMs !== undefined &&
228
- stat.mtimeMs > scannedAtMs + MTIME_DRIFT_TOLERANCE_MS) {
229
- return "stale";
230
- }
231
- return "live";
253
+ // No scan timestamp recorded: the pre-kernel behavior treated the
254
+ // path as live (no reference to be stale against) - preserved.
255
+ if (scannedAtMs === undefined)
256
+ return "live";
257
+ const verdict = freshnessFromMtime({
258
+ mtimeMs: stat.mtimeMs,
259
+ referenceMs: scannedAtMs,
260
+ });
261
+ return verdict.verdict === "stale" ? "stale" : "live";
232
262
  }
233
263
  catch (error) {
234
264
  const code = error.code ?? "unknown";
@@ -13,6 +13,7 @@ import * as os from "node:os";
13
13
  import * as path from "node:path";
14
14
  import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
15
15
  import { resolvePackagePath } from "./package-root.js";
16
+ import { truncatedByOutputCap } from "./spawn-output-cap.js";
16
17
  import { SgRunner, } from "./sg-runner.js";
17
18
  // --- Client ---
18
19
  function extractDebugAst(raw) {
@@ -113,9 +114,7 @@ export class AstGrepClient {
113
114
  (fs.existsSync(projectRuleDir)
114
115
  ? projectRuleDir
115
116
  : resolvePackagePath(import.meta.url, "rules"));
116
- this.log = verbose
117
- ? createSubsystemLogger("ast-grep")
118
- : () => { };
117
+ this.log = verbose ? createSubsystemLogger("ast-grep") : () => { };
119
118
  this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
120
119
  this.runner = new SgRunner(verbose);
121
120
  }
@@ -265,6 +264,18 @@ export class AstGrepClient {
265
264
  const result = await this.runner.execRaw(args, 10_000, options);
266
265
  const stderr = result.stderr.trim();
267
266
  const stdout = result.stdout.trim();
267
+ // FIRST (#2100): hitting `execRaw`'s cap SIGTERMs ast-grep, so a
268
+ // truncated run also carries the kill's error message. Read after the
269
+ // two checks below, this guard could only ever describe a run that
270
+ // exited before the signal reached it. `execRaw` re-spells `failure`
271
+ // in its own vocabulary but keeps the timeout/aborted spellings, so
272
+ // those runs still fall through to the error branch below.
273
+ if (truncatedByOutputCap(result)) {
274
+ return {
275
+ valid: false,
276
+ error: "ast-grep validation output was truncated",
277
+ };
278
+ }
268
279
  if (result.error)
269
280
  return { valid: false, error: result.error };
270
281
  if (result.status !== 0 && !(result.status === 1 && !stderr)) {
@@ -274,12 +285,6 @@ export class AstGrepClient {
274
285
  `ast-grep validation failed with exit code ${result.status}`,
275
286
  };
276
287
  }
277
- if (result.outputTruncated) {
278
- return {
279
- valid: false,
280
- error: "ast-grep validation output was truncated",
281
- };
282
- }
283
288
  if (stderrHasError(stderr))
284
289
  return { valid: false, error: stderr };
285
290
  const warning = stderr || stdout || undefined;
@@ -19,7 +19,13 @@ export function stageOwnerPidFromName(name) {
19
19
  }
20
20
  export async function sweepOwnStagingFiles(directories, options) {
21
21
  const uniqueDirectories = [...new Set(directories)].filter(Boolean);
22
- const result = { directories: uniqueDirectories.length, scanned: 0, removed: 0, liveSkipped: 0, truncated: false };
22
+ const result = {
23
+ directories: uniqueDirectories.length,
24
+ scanned: 0,
25
+ removed: 0,
26
+ liveSkipped: 0,
27
+ truncated: false,
28
+ };
23
29
  for (const directory of uniqueDirectories) {
24
30
  let handle;
25
31
  try {
@@ -47,14 +53,20 @@ export async function sweepOwnStagingFiles(directories, options) {
47
53
  continue;
48
54
  }
49
55
  try {
50
- await fs.promises.rm(path.join(directory, entry.name), { force: true });
56
+ await fs.promises.rm(path.join(directory, entry.name), {
57
+ force: true,
58
+ });
51
59
  result.removed++;
52
60
  }
53
- catch { /* leave it for the next bounded sweep */ }
61
+ catch {
62
+ /* leave it for the next bounded sweep */
63
+ }
54
64
  }
55
65
  result.truncated ||= reachedEntryCap;
56
66
  }
57
- catch { /* best-effort directory scan */ }
67
+ catch {
68
+ /* best-effort directory scan */
69
+ }
58
70
  finally {
59
71
  await handle.close().catch(() => { });
60
72
  }
@@ -0,0 +1,12 @@
1
+ import { createSingleFlight } from "./single-flight.js";
2
+ /** Create a flight registry whose lifetime belongs to the caller. */
3
+ export function createAvailabilityProbeFlight(options = {}) {
4
+ const flights = createSingleFlight(options);
5
+ return {
6
+ run(key, probe) {
7
+ const joined = flights.has(key);
8
+ return { promise: flights.run(key, probe), joined };
9
+ },
10
+ clear: () => flights.clear(),
11
+ };
12
+ }
@@ -634,7 +634,9 @@ function collectGrepCommandFiles(command, cwd) {
634
634
  return {
635
635
  hasLineNumberGrep,
636
636
  files,
637
- context: truncated ? { before: 0, after: 0 } : (context ?? { before: 0, after: 0 }),
637
+ context: truncated
638
+ ? { before: 0, after: 0 }
639
+ : (context ?? { before: 0, after: 0 }),
638
640
  };
639
641
  }
640
642
  function dedupePushSearchRead(out, seen, loc) {
@@ -58,9 +58,7 @@ export class BiomeClient {
58
58
  ensureFlight = createSingleFlight();
59
59
  log;
60
60
  constructor(verbose = false) {
61
- this.log = verbose
62
- ? createSubsystemLogger("biome")
63
- : () => { };
61
+ this.log = verbose ? createSubsystemLogger("biome") : () => { };
64
62
  }
65
63
  /**
66
64
  * Resolve the fastest available biome binary for `cwd`.
@@ -181,6 +179,7 @@ export class BiomeClient {
181
179
  latched: true,
182
180
  hostStallMs,
183
181
  budgetMs: PROBE_TIMEOUT_MS,
182
+ classifiedBy: "probe",
184
183
  });
185
184
  return { outcome: "success", value: true };
186
185
  }
@@ -227,6 +226,12 @@ export class BiomeClient {
227
226
  latched: true,
228
227
  hostStallMs: this.lastProbeHostStallMs,
229
228
  budgetMs: PROBE_TIMEOUT_MS,
229
+ classifiedBy: "caller",
230
+ evidence: {
231
+ install: "succeeded",
232
+ binary: "biome",
233
+ source: "managed-dir",
234
+ },
230
235
  });
231
236
  }
232
237
  return true;
@@ -43,12 +43,32 @@
43
43
  * runtime condition — that edge is invisible to this gate, so a blocker whose only
44
44
  * stale dependency arrives that way survives a replay. This is an honest boundary,
45
45
  * not a silently-closed one.
46
+ *
47
+ * Delivery cap (#1950). A demoted-but-confirmable entry (`alreadyStale` above)
48
+ * is deliberately NOT retired after its past-EOF sibling's one-delivery rule
49
+ * (#1944, `blocker-past-eof.ts`) — its coordinates are still in bounds, so a
50
+ * fresh dispatch can genuinely confirm or clear it, and retiring on delivery
51
+ * one would discard a recoverable finding. But nothing capped how many times
52
+ * the SAME demoted record re-serves: incident data (#1944's lane sweep) showed
53
+ * 18 `alreadyStale` re-serves in one window, with repeat deliveries carrying
54
+ * near-zero information after the first. `runtime-turn.ts` counts each
55
+ * delivery (`InlineBlockerRecord.staleDeliveryCount`) and retires the record
56
+ * via `RuntimeCoordinator.retireDemotedDependencyDriftBlocker` once it reaches
57
+ * `DEPENDENCY_DRIFT_MAX_DELIVERIES` — a count, not a TTL or a session
58
+ * boundary, because "how many times has the agent already been told" is the
59
+ * quantity that actually went to zero information, not "how much time has
60
+ * passed". The ledger reason says "capped, re-run can still confirm" so it
61
+ * reads distinctly from #1944's "retired, unrecoverable" — a capped record
62
+ * COULD still resolve a fresh dispatch; it just stopped being handed one for
63
+ * free.
46
64
  */
47
65
  import * as fs from "node:fs";
48
66
  import { normalizeEphemeralMapKey } from "./path-utils.js";
49
67
  import { resolveImportToFiles } from "./review-graph/import-resolvers.js";
50
68
  import { getSharedTreeSitterClient, resolveTreeSitterLanguage, } from "./tree-sitter-shared.js";
51
69
  import { TreeSitterSymbolExtractor } from "./tree-sitter-symbol-extractor.js";
70
+ /** See the module doc's "Delivery cap (#1950)" section above. */
71
+ export const DEPENDENCY_DRIFT_MAX_DELIVERIES = 3;
52
72
  /**
53
73
  * Upper bound on the number of forward imports stat-checked for a single blocker.
54
74
  * Blocking entries are rare, but one file can import many modules; the sweep must
@@ -126,7 +146,11 @@ async function resolveForwardImportsMemoized(cwd, filePath, resolveForwardImport
126
146
  catch {
127
147
  imports = [];
128
148
  }
129
- importResolutionMemo.set(filePath, { mtimeMs: sig.mtimeMs, size: sig.size, imports });
149
+ importResolutionMemo.set(filePath, {
150
+ mtimeMs: sig.mtimeMs,
151
+ size: sig.size,
152
+ imports,
153
+ });
130
154
  return imports;
131
155
  }
132
156
  /** Test-only: clear the extractor and import-resolution memos. */
@@ -232,17 +256,26 @@ export async function collectForwardImportMtimes(cwd, filePath, resolveForwardIm
232
256
  * +50ms). +50ms comfortably clears the measured skew while staying far below the
233
257
  * gap between genuinely distinct edits.
234
258
  */
235
- export const MTIME_DRIFT_TOLERANCE_MS = 50;
259
+ // Single implementation lives in the freshness kernel (#1739); this
260
+ // re-export preserves every existing importer's path.
261
+ export { MTIME_DRIFT_TOLERANCE_MS } from "./freshness.js";
262
+ import { freshnessFromMtime } from "./freshness.js";
236
263
  async function detectDrift(cwd, filePath, recordedAtMs, resolveForwardImports, turnIndex) {
237
264
  const drifted = [];
238
- const ownMtimeMs = await statMtimeMs(filePath);
239
- if (ownMtimeMs !== undefined && ownMtimeMs > recordedAtMs + MTIME_DRIFT_TOLERANCE_MS) {
265
+ const ownFreshness = freshnessFromMtime({
266
+ mtimeMs: await statMtimeMs(filePath),
267
+ referenceMs: recordedAtMs,
268
+ });
269
+ if (ownFreshness.verdict === "stale")
240
270
  drifted.push(filePath);
241
- }
242
271
  const { mtimes, truncated } = await collectForwardImportMtimes(cwd, filePath, resolveForwardImports, turnIndex);
243
- for (const { path, mtimeMs } of mtimes) {
244
- if (mtimeMs > recordedAtMs + MTIME_DRIFT_TOLERANCE_MS)
245
- drifted.push(path);
272
+ for (const { path: depPath, mtimeMs } of mtimes) {
273
+ const verdict = freshnessFromMtime({
274
+ mtimeMs,
275
+ referenceMs: recordedAtMs,
276
+ });
277
+ if (verdict.verdict === "stale")
278
+ drifted.push(depPath);
246
279
  }
247
280
  return { drifted, truncated };
248
281
  }
@@ -13,7 +13,9 @@ export class BoundedLruCache {
13
13
  }
14
14
  return value;
15
15
  }
16
- has(key) { return this.entries.has(key); }
16
+ has(key) {
17
+ return this.entries.has(key);
18
+ }
17
19
  set(key, value) {
18
20
  this.entries.delete(key);
19
21
  this.entries.set(key, value);
@@ -24,11 +26,25 @@ export class BoundedLruCache {
24
26
  this.entries.delete(oldest);
25
27
  }
26
28
  }
27
- delete(key) { return this.entries.delete(key); }
28
- clear() { this.entries.clear(); }
29
- get size() { return this.entries.size; }
30
- keys() { return this.entries.keys(); }
31
- values() { return this.entries.values(); }
32
- entriesArray() { return [...this.entries.entries()]; }
33
- [Symbol.iterator]() { return this.entries[Symbol.iterator](); }
29
+ delete(key) {
30
+ return this.entries.delete(key);
31
+ }
32
+ clear() {
33
+ this.entries.clear();
34
+ }
35
+ get size() {
36
+ return this.entries.size;
37
+ }
38
+ keys() {
39
+ return this.entries.keys();
40
+ }
41
+ values() {
42
+ return this.entries.values();
43
+ }
44
+ entriesArray() {
45
+ return [...this.entries.entries()];
46
+ }
47
+ [Symbol.iterator]() {
48
+ return this.entries[Symbol.iterator]();
49
+ }
34
50
  }
@@ -35,8 +35,7 @@ async function releaseQuarantineLock(lockPath, token) {
35
35
  break;
36
36
  }
37
37
  catch (error) {
38
- if (error.code !== "ENOENT" ||
39
- attempt === 2)
38
+ if (error.code !== "ENOENT" || attempt === 2)
40
39
  return;
41
40
  await new Promise((resolve) => setImmediate(resolve));
42
41
  }
@@ -111,7 +110,9 @@ async function tryAcquireQuarantineLock(lockPath, staleMs) {
111
110
  await fsp.writeFile(path.join(lockPath, "owner.json"), JSON.stringify(owner), "utf8");
112
111
  }
113
112
  catch (error) {
114
- await fsp.rm(lockPath, { recursive: true, force: true }).catch(() => { });
113
+ await fsp
114
+ .rm(lockPath, { recursive: true, force: true })
115
+ .catch(() => { });
115
116
  throw error;
116
117
  }
117
118
  return () => releaseQuarantineLock(lockPath, owner.token);
@@ -24,10 +24,9 @@
24
24
  * that must re-arm at `session_start` and will eventually be forgotten
25
25
  * (the #1266–#1625 defect class).
26
26
  * 2. **A per-turn cap names its turn.** `capPerTurn` carries the caller's
27
- * `turnIndex` in the same object as the limit, so there is no separate
28
- * reset to wire and forget: the counters clear when the observed turn
29
- * changes. This is the same lazy clear-on-transition shape as
30
- * `noteImportResolutionMemoTurn` (`clients/blocker-freshness.ts`).
27
+ * `turnIndex` in the same object as the limit. A bounded 64-turn window
28
+ * keeps overlapping async turns independent; results older than that window
29
+ * fail closed. Session start clears the window when numbering restarts.
31
30
  * 3. **Identity always survives.** Every emitted record carries the
32
31
  * discriminating identity in `filePath` and in `metadata.identity`, so
33
32
  * aggregation can still answer "which file, which method, which record is
@@ -85,6 +84,14 @@ export const BOUNDED_TELEMETRY_PHASES = [
85
84
  * exact count.
86
85
  */
87
86
  "lsp_warm_client_missing",
87
+ /** #2052: a file's nearest LSP root is outside the session ceiling. */
88
+ "lsp_capability_skip",
89
+ /**
90
+ * #2044: failed-first test state was retired after a confirmed missing path,
91
+ * retained because the filesystem verdict was indeterminate, or evicted at
92
+ * the state cap. Selection checks and detailed rows are both capped per turn.
93
+ */
94
+ "test_runner_failed_target_state",
88
95
  /**
89
96
  * #1723: an event-loop block at or above the floor. Not a degradation, so
90
97
  * no ledger kind; bounded by call cadence (one `turn_end` runs it once per
@@ -97,6 +104,17 @@ export const BOUNDED_TELEMETRY_PHASES = [
97
104
  * queue of them, so it is rising-edge per event name.
98
105
  */
99
106
  "session_event_stale_ctx_skip",
107
+ /**
108
+ * #2007: a worktree-mutating git command declined because another live
109
+ * session shares this dirty checkout. Rising edge per checkout root; the
110
+ * ledger keeps the exact repeat count.
111
+ */
112
+ "shared_checkout_switch_blocked",
113
+ /**
114
+ * #2007: `git status` could not report the working-tree state, so the same
115
+ * command was declined on an UNKNOWN rather than assumed clean.
116
+ */
117
+ "shared_checkout_probe_failed",
100
118
  ];
101
119
  const REGISTERED_PHASES = new Set(BOUNDED_TELEMETRY_PHASES);
102
120
  /** Report whether a phase name is in the registry (used by the sweep). */
@@ -104,14 +122,15 @@ export function isBoundedTelemetryPhase(phase) {
104
122
  return REGISTERED_PHASES.has(phase);
105
123
  }
106
124
  /**
107
- * Per-turn admission counters, keyed by phase. Cleared lazily when a call
108
- * observes a turn index different from the one the counters were built for,
109
- * and eagerly by `resetBoundedTelemetry` at `session_start` (a new session
110
- * restarts turn numbering at 0, so the lazy check alone could carry a stale
111
- * count across a session boundary that happened to land on the same index).
125
+ * Per-turn admission counters, keyed by turn and phase. Multiple turns remain
126
+ * live because async work can finish T1 after T2 has already emitted. The map
127
+ * retains a bounded recent window; work older than that window fails closed
128
+ * rather than reopening an exhausted budget. Session start clears all state.
112
129
  */
130
+ const MAX_TRACKED_TURN_CAPS = 64;
113
131
  let turnCounts = new Map();
114
- let countedTurnIndex;
132
+ let latestCountedTurnIndex;
133
+ let retiredThroughTurnIndex = Number.NEGATIVE_INFINITY;
115
134
  /**
116
135
  * Decide whether this occurrence earns a detailed record, and count it in the
117
136
  * ledger either way. Returns `true` when the caller should write the record.
@@ -180,14 +199,25 @@ export function emitBounded(phase, identity, payload, options = {}) {
180
199
  function admitAgainstTurnCap(phase, cap) {
181
200
  if (cap === undefined)
182
201
  return true;
183
- if (countedTurnIndex !== cap.turnIndex) {
184
- turnCounts = new Map();
185
- countedTurnIndex = cap.turnIndex;
202
+ if (cap.turnIndex <= retiredThroughTurnIndex)
203
+ return false;
204
+ let counts = turnCounts.get(cap.turnIndex);
205
+ if (!counts) {
206
+ if (turnCounts.size >= MAX_TRACKED_TURN_CAPS) {
207
+ const oldestTrackedTurn = Math.min(...turnCounts.keys());
208
+ if (cap.turnIndex < oldestTrackedTurn)
209
+ return false;
210
+ turnCounts.delete(oldestTrackedTurn);
211
+ retiredThroughTurnIndex = Math.max(retiredThroughTurnIndex, oldestTrackedTurn);
212
+ }
213
+ counts = new Map();
214
+ turnCounts.set(cap.turnIndex, counts);
186
215
  }
187
- const used = turnCounts.get(phase) ?? 0;
216
+ latestCountedTurnIndex = Math.max(latestCountedTurnIndex ?? cap.turnIndex, cap.turnIndex);
217
+ const used = counts.get(phase) ?? 0;
188
218
  if (used >= cap.limit)
189
219
  return false;
190
- turnCounts.set(phase, used + 1);
220
+ counts.set(phase, used + 1);
191
221
  return true;
192
222
  }
193
223
  /**
@@ -198,9 +228,16 @@ function admitAgainstTurnCap(phase, cap) {
198
228
  */
199
229
  export function resetBoundedTelemetry() {
200
230
  turnCounts = new Map();
201
- countedTurnIndex = undefined;
231
+ latestCountedTurnIndex = undefined;
232
+ retiredThroughTurnIndex = Number.NEGATIVE_INFINITY;
233
+ }
234
+ /** Test-only: one turn's live admission count for a phase. */
235
+ export function _boundedTurnCountForTest(phase, turnIndex = latestCountedTurnIndex) {
236
+ if (turnIndex === undefined)
237
+ return 0;
238
+ return turnCounts.get(turnIndex)?.get(phase) ?? 0;
202
239
  }
203
- /** Test-only: the live per-turn admission count for a phase. */
204
- export function _boundedTurnCountForTest(phase) {
205
- return turnCounts.get(phase) ?? 0;
240
+ /** Test-only: number of turn buckets retained by the bounded counter. */
241
+ export function _boundedTrackedTurnsForTest() {
242
+ return turnCounts.size;
206
243
  }