@herbertgao/pi-extensions 2026.9.0 → 2026.9.2

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 (360) hide show
  1. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
  2. package/node_modules/@czottmann/pi-automode/README.md +20 -0
  3. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
  4. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
  5. package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
  6. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
  9. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  10. package/node_modules/@narumitw/pi-btw/README.md +57 -141
  11. package/node_modules/@narumitw/pi-btw/dist/index.ts +466 -41
  12. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  13. package/node_modules/@narumitw/pi-btw/docs/workflows.md +74 -0
  14. package/node_modules/@narumitw/pi-btw/package.json +2 -1
  15. package/node_modules/@narumitw/pi-btw/src/btw.ts +15 -1
  16. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +32 -2
  17. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +330 -0
  18. package/node_modules/@narumitw/pi-btw/src/menu.ts +136 -19
  19. package/node_modules/@narumitw/pi-btw/src/settings.ts +32 -0
  20. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +57 -5
  21. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +52 -30
  22. package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
  23. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  24. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
  25. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  26. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  27. package/node_modules/pi-lens/CHANGELOG.md +1072 -0
  28. package/node_modules/pi-lens/README.md +3 -0
  29. package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
  30. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
  31. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -9
  32. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
  33. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +1 -1
  34. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
  35. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
  36. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
  37. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -9
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +2 -0
  39. package/node_modules/pi-lens/dist/clients/bash-file-access.js +133 -3
  40. package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
  41. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -6
  42. package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
  43. package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
  44. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +1 -1
  45. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -9
  46. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
  47. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +3 -11
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +40 -19
  51. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
  52. package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -9
  53. package/node_modules/pi-lens/dist/clients/child-unref.js +1 -19
  54. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
  56. package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
  57. package/node_modules/pi-lens/dist/clients/config-core/index.js +50 -0
  58. package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
  59. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
  60. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
  61. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +164 -0
  62. package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
  63. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
  64. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
  65. package/node_modules/pi-lens/dist/clients/config-core/schema.js +165 -0
  66. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
  67. package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
  68. package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
  69. package/node_modules/pi-lens/dist/clients/config-schema.js +234 -0
  70. package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
  71. package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
  72. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -6
  73. package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
  74. package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
  75. package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
  76. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
  77. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
  78. package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
  79. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +1 -1
  80. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
  81. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +15 -5
  82. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -4
  83. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -44
  84. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +27 -8
  86. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -6
  87. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +1 -1
  88. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +2 -2
  90. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +1 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +383 -27
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +1 -2
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +14 -18
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +6 -3
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
  122. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
  123. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +1 -2
  124. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
  125. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +1 -2
  126. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
  127. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +4 -3
  128. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
  129. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  130. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
  131. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
  132. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
  133. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
  134. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
  135. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  136. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
  137. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  138. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -67
  139. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
  140. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
  141. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -21
  142. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
  143. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +2 -20
  144. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +1 -1
  145. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
  146. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +2 -2
  147. package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -10
  148. package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
  149. package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
  150. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
  151. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +2 -2
  152. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
  153. package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
  154. package/node_modules/pi-lens/dist/clients/file-kinds.js +1 -38
  155. package/node_modules/pi-lens/dist/clients/file-time.js +7 -2
  156. package/node_modules/pi-lens/dist/clients/file-utils.js +117 -10
  157. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
  158. package/node_modules/pi-lens/dist/clients/format-service.js +6 -2
  159. package/node_modules/pi-lens/dist/clients/formatters.js +385 -104
  160. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
  161. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +116 -22
  162. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
  163. package/node_modules/pi-lens/dist/clients/git-guard.js +6 -1
  164. package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
  165. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
  166. package/node_modules/pi-lens/dist/clients/grammar-source.js +3 -3
  167. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
  168. package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
  169. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
  170. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  171. package/node_modules/pi-lens/dist/clients/installer/index.js +289 -59
  172. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +84 -18
  173. package/node_modules/pi-lens/dist/clients/instance-reaper.js +110 -236
  174. package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
  175. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -9
  176. package/node_modules/pi-lens/dist/clients/language-registry.js +599 -0
  177. package/node_modules/pi-lens/dist/clients/latency-logger.js +165 -9
  178. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +34 -1
  179. package/node_modules/pi-lens/dist/clients/lens-config.js +160 -30
  180. package/node_modules/pi-lens/dist/clients/lens-engine.js +33 -41
  181. package/node_modules/pi-lens/dist/clients/lens-events.js +1 -1
  182. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +77 -6
  183. package/node_modules/pi-lens/dist/clients/lens-map.js +1 -1
  184. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -38
  185. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
  186. package/node_modules/pi-lens/dist/clients/lsp/client.js +172 -33
  187. package/node_modules/pi-lens/dist/clients/lsp/config.js +484 -158
  188. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
  189. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +1 -1
  190. package/node_modules/pi-lens/dist/clients/lsp/edits.js +18 -5
  191. package/node_modules/pi-lens/dist/clients/lsp/index.js +892 -96
  192. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +1 -1
  193. package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -177
  194. package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  196. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -18
  197. package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
  198. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
  199. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
  200. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -1
  201. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +14 -7
  202. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +6 -0
  203. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +2 -1
  204. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
  205. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +1 -1
  206. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
  207. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
  208. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
  209. package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
  210. package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
  211. package/node_modules/pi-lens/dist/clients/metrics-history.js +28 -107
  212. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +3 -5
  213. package/node_modules/pi-lens/dist/clients/module-report.js +21 -46
  214. package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
  215. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
  216. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
  217. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -24
  218. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
  219. package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
  220. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +70 -28
  221. package/node_modules/pi-lens/dist/clients/opengrep-config.js +5 -1
  222. package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
  223. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
  224. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
  225. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
  226. package/node_modules/pi-lens/dist/clients/path-utils.js +451 -15
  227. package/node_modules/pi-lens/dist/clients/performance-report.js +2 -2
  228. package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
  229. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
  230. package/node_modules/pi-lens/dist/clients/pipeline.js +79 -18
  231. package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
  232. package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
  233. package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
  234. package/node_modules/pi-lens/dist/clients/project-changes.js +1 -1
  235. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +1 -1
  236. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  237. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +1 -1
  238. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +1 -1
  239. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +1 -1
  240. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +1 -1
  241. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +28 -4
  242. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
  243. package/node_modules/pi-lens/dist/clients/project-lens-config.js +438 -122
  244. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +3 -51
  245. package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
  246. package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
  247. package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
  248. package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
  249. package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
  250. package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
  251. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +16 -5
  252. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
  253. package/node_modules/pi-lens/dist/clients/read-guard.js +26 -2
  254. package/node_modules/pi-lens/dist/clients/recent-touches.js +2 -2
  255. package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
  256. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +155 -114
  257. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +1 -1
  258. package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
  259. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  260. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
  261. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
  262. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +19 -3
  263. package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
  264. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
  265. package/node_modules/pi-lens/dist/clients/runtime-session.js +184 -32
  266. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +179 -36
  267. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
  268. package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
  269. package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
  270. package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
  271. package/node_modules/pi-lens/dist/clients/sanitize.js +7 -12
  272. package/node_modules/pi-lens/dist/clients/scan-utils.js +1 -56
  273. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
  274. package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
  275. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
  277. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -24
  278. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
  279. package/node_modules/pi-lens/dist/clients/sgconfig.js +7 -6
  280. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +1 -1
  281. package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
  282. package/node_modules/pi-lens/dist/clients/smells-rollup.js +4 -4
  283. package/node_modules/pi-lens/dist/clients/startup-scan.js +2 -10
  284. package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
  285. package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
  286. package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
  287. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +244 -0
  288. package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
  289. package/node_modules/pi-lens/dist/clients/tool-policy.js +92 -22
  290. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
  291. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +87 -100
  292. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
  293. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
  294. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
  295. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  296. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -4
  297. package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
  298. package/node_modules/pi-lens/dist/clients/user-notify.js +6 -6
  299. package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
  300. package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -9
  301. package/node_modules/pi-lens/dist/clients/word-index-store.js +23 -12
  302. package/node_modules/pi-lens/dist/clients/word-index.js +252 -47
  303. package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
  304. package/node_modules/pi-lens/dist/clients/zizmor-config.js +4 -1
  305. package/node_modules/pi-lens/dist/index.js +65899 -55340
  306. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
  307. package/node_modules/pi-lens/dist/mcp/build-staleness.js +1 -1
  308. package/node_modules/pi-lens/dist/mcp/server.js +127 -5
  309. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
  310. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
  311. package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
  312. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +96 -25
  313. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +104 -86
  314. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
  315. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +3 -3
  316. package/node_modules/pi-lens/dist/tools/render-compact.js +1 -1
  317. package/node_modules/pi-lens/dist/tools/shared.js +0 -1
  318. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -2
  319. package/node_modules/pi-lens/docs/agent-guide.md +64 -1
  320. package/node_modules/pi-lens/docs/configuration.md +222 -0
  321. package/node_modules/pi-lens/docs/dependencies.md +3 -3
  322. package/node_modules/pi-lens/docs/features.md +44 -5
  323. package/node_modules/pi-lens/docs/globalconfig.md +20 -0
  324. package/node_modules/pi-lens/docs/language-coverage.md +2 -2
  325. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +9 -1
  326. package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
  327. package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
  328. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +33 -0
  329. package/node_modules/pi-lens/docs/pi-lens-subagent.md +49 -0
  330. package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
  331. package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
  332. package/node_modules/pi-lens/docs/release-qa-baseline.md +190 -0
  333. package/node_modules/pi-lens/docs/servercapabilities.md +7 -3
  334. package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
  335. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
  336. package/node_modules/pi-lens/docs/word-index.md +1 -1
  337. package/node_modules/pi-lens/package.json +18 -35
  338. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  339. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  340. package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  341. package/node_modules/pi-lens/rules/typos/_typos.toml +4 -2
  342. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
  343. package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
  344. package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
  345. package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +1 -1
  346. package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
  347. package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +4 -2
  348. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  349. package/node_modules/pi-web-access/README.md +62 -15
  350. package/node_modules/pi-web-access/curator-page.ts +3 -1
  351. package/node_modules/pi-web-access/curator-server.ts +5 -1
  352. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  353. package/node_modules/pi-web-access/github-extract.ts +242 -1
  354. package/node_modules/pi-web-access/index.ts +118 -64
  355. package/node_modules/pi-web-access/mistral-search.ts +281 -0
  356. package/node_modules/pi-web-access/package.json +2 -2
  357. package/node_modules/pi-web-access/perplexity.ts +14 -1
  358. package/node_modules/pi-web-access/utils.ts +23 -6
  359. package/node_modules/pi-web-access/xai-search.ts +96 -33
  360. package/package.json +6 -6
@@ -57,37 +57,28 @@
57
57
  * patterns apply to files inside that package, in addition to (and with
58
58
  * higher precedence than) the root config's `ignore` patterns.
59
59
  */
60
- import { logExtension } from "./extension-log.js";
61
- import { notifyUserDegradation } from "./user-notify.js";
60
+ import { resetIgnoredConfigWarnCache, warnIgnoredConfigOnce, } from "./config-warn.js";
62
61
  import * as fs from "node:fs";
63
62
  import * as os from "node:os";
64
63
  import * as path from "node:path";
65
64
  import { toPositiveFinite } from "./env-utils.js";
66
- import { assignFlagConfigSection, flagConfigSectionKeys, GLOBAL_NON_FLAG_CONFIG_SECTIONS, LENS_FLAGS, PROJECT_FOREIGN_CONFIG_NAMESPACES, PROJECT_SCOPED_LENS_FLAGS, readFlagConfigValue, } from "./lens-flag-registry.js";
67
- import { isAtOrAboveHomeDir, walkUpDirs } from "./path-utils.js";
65
+ import { FRESHNESS_CADENCE_MS } from "./freshness-cadence.js";
66
+ import { assignFlagConfigSection, flagConfigSectionKeys, GLOBAL_NON_FLAG_CONFIG_SECTIONS, LENS_FLAGS, PROJECT_FOREIGN_CONFIG_NAMESPACES, PROJECT_FOREIGN_NAMESPACE_HONORED_KEYS, PROJECT_NON_FLAG_CONFIG_SECTIONS, PROJECT_SCOPED_LENS_FLAGS, readFlagConfigValue, } from "./lens-flag-registry.js";
67
+ import { configSearchDirs, getPiLensGlobalConfigPath, PROJECT_CONFIG_BASENAMES, PROJECT_CONFIG_LOCATIONS, } from "./config-locations.js";
68
+ import { deprecationRecords, ignoredRecordCollector, projectLocationFor, readConfigDocument, reportConfigReadFailure, reportPiLensConfigRecords, resolveOnePiLensConfigDocument, } from "./config-resolve.js";
69
+ import { finalizeRecords, } from "./config-core/records.js";
70
+ import { homeRelativePath, isAtOrAboveHomeDir, walkUpDirs, } from "./path-utils.js";
68
71
  import { findPiLensConfigMarkerInDir } from "./workspace-topology.js";
69
- const PROJECT_CONFIG_BASENAMES = [".pi-lens.json", "pi-lens.json"];
70
72
  /**
71
- * The project loader's OWN recognized top-level keys — pi-lens-native sections,
72
- * whether parsed here into typed fields (`ignore`, `rules`, `maxProjectFiles`,
73
- * `reviewGraph`) or read off `PiLensProjectConfig.raw` by another pi-lens
74
- * consumer (`trivy`, read via `.raw` in `trivy-client.ts`). The project-scoped
75
- * flag sections (`format`, `autofix`, `actionableWarnings`) are NOT listed here;
76
- * they are derived from `PROJECT_SCOPED_LENS_FLAGS` so the registry stays the
77
- * single source of truth (#883). Foreign (non-pi-lens) namespaces the shared
78
- * file also carries live in `PROJECT_FOREIGN_CONFIG_NAMESPACES` beside the
79
- * registry.
73
+ * Project config basenames, in DESCENDING precedence (first match wins), for
74
+ * this loader's own discovery.
75
+ *
76
+ * Re-exported from `config-locations.ts`, which owns the derivation, so this
77
+ * loader's long-standing export keeps working while there is exactly ONE list
78
+ * (#2426 review round 2: `workspace-topology.ts` was still carrying a third
79
+ * copy).
80
80
  */
81
- const PROJECT_OWN_CONFIG_KEYS = [
82
- "ignore",
83
- "rules",
84
- "maxProjectFiles",
85
- "reviewGraph",
86
- "trivy",
87
- // `helm.renderValidation.enabled` — the opt-in for rendered-manifest
88
- // validation, read via `.raw` in the helm-render runner (#1283).
89
- "helm",
90
- ];
81
+ export { PROJECT_CONFIG_BASENAMES } from "./config-locations.js";
91
82
  /** Clamp bounds for `reviewGraph.maxFiles` — see the field's doc comment above. */
92
83
  const REVIEW_GRAPH_MAX_FILES_MIN = 100;
93
84
  const REVIEW_GRAPH_MAX_FILES_MAX = 20_000;
@@ -102,34 +93,93 @@ export const EMPTY_PROJECT_CONFIG = {
102
93
  /** Cache by absolute config path; we read each candidate's mtime before reuse. */
103
94
  const configCache = new Map();
104
95
  const discoveryCache = new Map();
105
- const warnedInvalidConfigs = new Set();
96
+ /**
97
+ * The shared body of `loadPiLensProjectConfig` and `loadPiLensConfigInDir`:
98
+ * reuse a fresh cache entry, or parse and cache one, and — EITHER WAY (#2426
99
+ * review round 3, F2) — report the resolution's migration records. Content
100
+ * caching (skip re-reading unchanged bytes off disk) and per-session
101
+ * reporting (tell the user once a session about a deprecated setting) are two
102
+ * different lifetimes; folding "already cached" into "already reported"
103
+ * silently dropped the report the moment the file stopped changing.
104
+ */
105
+ function loadCachedConfigFile(info) {
106
+ const cached = configCache.get(info.path);
107
+ if (cached && cached.mtimeMs === info.mtimeMs && cached.size === info.size) {
108
+ replayConfigNotices(cached);
109
+ return cached.config;
110
+ }
111
+ const parsed = parseConfigFile(info.path);
112
+ const entry = {
113
+ mtimeMs: info.mtimeMs,
114
+ size: info.size,
115
+ config: parsed.config,
116
+ records: parsed.records,
117
+ ignored: parsed.ignored,
118
+ };
119
+ configCache.set(info.path, entry);
120
+ replayConfigNotices(entry);
121
+ return parsed.config;
122
+ }
123
+ /**
124
+ * Report BOTH classes of notice a parse produced (#2426 review round 4, F3).
125
+ *
126
+ * The resolution's records go through the shared seam, which derives each
127
+ * one's subsystem from the record. This loader's own records do not: they are
128
+ * `project-lens-config`'s by construction — it composed their prose — so
129
+ * routing them through that derivation would relabel `"lsp.enabled" is a
130
+ * global-only pi-lens setting` as an LSP-config problem, which is the opposite
131
+ * of what it says.
132
+ */
133
+ function replayConfigNotices(entry) {
134
+ reportPiLensConfigRecords(entry.records);
135
+ for (const record of entry.ignored) {
136
+ warnIgnoredConfigOnce({
137
+ subsystem: "project-lens-config",
138
+ file: record.file,
139
+ reason: record.reason,
140
+ code: record.code,
141
+ });
142
+ }
143
+ }
106
144
  /**
107
145
  * Walk up from `startDir` looking for a `.pi-lens.json` or `pi-lens.json`.
108
146
  * Returns the parsed config, or an empty config if none was found.
109
147
  */
110
- export function loadPiLensProjectConfig(startDir, preloadedInfo = findPiLensProjectConfig(startDir)) {
111
- const configInfo = preloadedInfo;
148
+ export function loadPiLensProjectConfig(startDir, preloadedInfo) {
149
+ // ONE discovery per load (#2426 review round 5, F-C). The legacy-document
150
+ // enumeration below and the config-file lookup are two READS of the same
151
+ // cache entry, and resolving that entry twice — the default argument once,
152
+ // the record replay again — doubled the freshness stats every warm load
153
+ // pays, on a path called per dispatch. The entry is resolved here and both
154
+ // halves read it.
155
+ const entry = discoveryEntryFor(startDir);
156
+ // #2426 review round 4, F4. The half-migrated notices — a legacy
157
+ // `pi-lens.json` sitting BESIDE or ABOVE the canonical `.pi-lens.json` this
158
+ // walk stops at — used to be produced only by `loadLSPConfig`, whose
159
+ // discovery collects from every bearing directory rather than stopping at
160
+ // the first. Under `--no-lsp`, `lsp.enabled: false`, or a subagent session
161
+ // (#449, routine) that loader never runs, and the pi-lens-OWNED records for
162
+ // a pi-lens-owned file were then produced by nobody at all. The pi-lens
163
+ // loader now enumerates those documents itself, for records only: nothing
164
+ // here is merged, and which file supplies a VALUE is unchanged.
165
+ const configInfo = preloadedInfo ?? freshInfoFor(entry);
166
+ // The selected legacy document is resolved below and its bounded migration
167
+ // records are cached with that projection. Reporting the discovery copy as
168
+ // well would produce two suppression records with different totals, because
169
+ // the discovery walk and the single-document resolution see different
170
+ // record populations. Keep discovery-only documents here; the selected file
171
+ // is reported by `loadCachedConfigFile`.
172
+ const selectedLegacyPath = configInfo?.path;
173
+ reportPiLensConfigRecords(entry.legacyRecords.filter((record) => record.file !== selectedLegacyPath));
112
174
  if (!configInfo)
113
175
  return EMPTY_PROJECT_CONFIG;
114
- const cached = configCache.get(configInfo.path);
115
- if (cached &&
116
- cached.mtimeMs === configInfo.mtimeMs &&
117
- cached.size === configInfo.size) {
118
- return cached.config;
119
- }
120
- const config = parseConfigFile(configInfo.path);
121
- configCache.set(configInfo.path, {
122
- mtimeMs: configInfo.mtimeMs,
123
- size: configInfo.size,
124
- config,
125
- });
126
- return config;
176
+ return loadCachedConfigFile(configInfo);
127
177
  }
128
178
  /** For tests + callers that need to force a re-read (e.g. config-watcher hooks). */
129
179
  export function resetProjectLensConfigCache() {
130
180
  configCache.clear();
131
181
  discoveryCache.clear();
132
- warnedInvalidConfigs.clear();
182
+ resetIgnoredConfigWarnCache("project-lens-config");
133
183
  }
134
184
  /**
135
185
  * Look for a `.pi-lens.json`/`pi-lens.json` directly IN `dir` — no upward
@@ -189,31 +239,37 @@ export function loadPiLensConfigInDir(dir) {
189
239
  const info = findPiLensConfigInDir(dir);
190
240
  if (!info)
191
241
  return EMPTY_PROJECT_CONFIG;
192
- const cached = configCache.get(info.path);
193
- if (cached && cached.mtimeMs === info.mtimeMs && cached.size === info.size) {
194
- return cached.config;
195
- }
196
- const config = parseConfigFile(info.path);
197
- configCache.set(info.path, {
198
- mtimeMs: info.mtimeMs,
199
- size: info.size,
200
- config,
201
- });
202
- return config;
242
+ return loadCachedConfigFile(info);
203
243
  }
204
- export function findPiLensProjectConfig(startDir) {
244
+ /** The cached discovery for `startDir`, recomputed when it is no longer fresh. */
245
+ function discoveryEntryFor(startDir) {
205
246
  const cacheKey = path.resolve(startDir);
206
247
  const cached = discoveryCache.get(cacheKey);
207
- if (cached && discoveryCacheStillFresh(cached)) {
208
- if (!cached.info)
209
- return undefined;
210
- const stat = safeFileStat(cached.info.path);
211
- if (stat?.isFile())
212
- return { ...cached.info, mtimeMs: stat.mtimeMs, size: stat.size };
213
- }
248
+ if (cached && discoveryCacheStillFresh(cached))
249
+ return cached;
214
250
  const discovered = discoverPiLensProjectConfig(cacheKey);
215
251
  discoveryCache.set(cacheKey, discovered);
216
- return discovered.info;
252
+ return discovered;
253
+ }
254
+ export function findPiLensProjectConfig(startDir) {
255
+ return freshInfoFor(discoveryEntryFor(startDir));
256
+ }
257
+ /**
258
+ * The winning config file's info, re-statted.
259
+ *
260
+ * Split out of `findPiLensProjectConfig` so a caller that already holds the
261
+ * discovery entry does not resolve it a second time to get here (#2426 review
262
+ * round 5, F-C). The re-stat itself stays: the entry can be fresh by directory
263
+ * mtime while the config file was rewritten in place, and the caller keys its
264
+ * content cache on the mtime/size this returns.
265
+ */
266
+ function freshInfoFor(entry) {
267
+ if (!entry.info)
268
+ return undefined;
269
+ const stat = safeFileStat(entry.info.path);
270
+ if (!stat?.isFile())
271
+ return entry.info;
272
+ return { ...entry.info, mtimeMs: stat.mtimeMs, size: stat.size };
217
273
  }
218
274
  function safeFileStat(filePath) {
219
275
  try {
@@ -232,48 +288,192 @@ function safeDirMtimeMs(dir) {
232
288
  }
233
289
  }
234
290
  function discoveryCacheStillFresh(entry) {
235
- return entry.dirMtimes.every((cached) => safeDirMtimeMs(cached.dir) === cached.mtimeMs);
291
+ if (!entry.dirMtimes.every((cached) => safeDirMtimeMs(cached.dir) === cached.mtimeMs)) {
292
+ return false;
293
+ }
294
+ // #2483 round 2. When no config was found anywhere, `bearingDirMtimes`'s
295
+ // no-`info` branch scopes `dirMtimes` to `startDir` alone, so the `.every`
296
+ // above can only ever see churn AT `startDir` — a config file created at
297
+ // any ancestor above it moves nothing this entry tracks, and the entry
298
+ // would then read as fresh for the rest of the process (no production
299
+ // caller resets it, and a same-`startDir` load never rediscovers). The
300
+ // issue's acceptance line rules that out ("a new config file created in
301
+ // the chain IS noticed (bounded staleness stated)"), so a no-`info` entry
302
+ // is force-expired at most once per `FRESHNESS_CADENCE_MS` regardless of
303
+ // what `dirMtimes` says — the same negative-cache cadence bound
304
+ // `file-utils.ts`'s `patternsForDir` already applies to its own no-match
305
+ // case, reused rather than a second cadence mechanism invented here. The
306
+ // forced re-walk that follows re-stats the FULL ancestor chain (see
307
+ // `discoverPiLensProjectConfig`), so a config created anywhere in it is
308
+ // found — bounded to at most one cadence window of staleness, not
309
+ // unbounded.
310
+ if (entry.info === undefined &&
311
+ Date.now() - entry.checkedAtMs >= FRESHNESS_CADENCE_MS) {
312
+ return false;
313
+ }
314
+ // A legacy document can be EDITED in place without its directory's mtime
315
+ // moving, and its top-level key set is what `legacyRecords` describes — so
316
+ // those files carry their own freshness axis, the same (mtime, size) pair
317
+ // `CacheEntry` uses.
318
+ return entry.legacySources.every((source) => {
319
+ const stat = safeFileStat(source.path);
320
+ return (stat?.isFile() === true &&
321
+ stat.mtimeMs === source.mtimeMs &&
322
+ stat.size === source.size);
323
+ });
236
324
  }
325
+ const LEGACY_PROJECT_LOCATIONS = PROJECT_CONFIG_LOCATIONS.filter((location) => location.legacy);
237
326
  function discoverPiLensProjectConfig(startDir) {
238
327
  const dirMtimes = [];
239
- for (const dir of walkUpDirs(startDir)) {
328
+ const legacyDocuments = [];
329
+ const legacySources = [];
330
+ let info;
331
+ // #2426: ceiling-bounded, same rule and same shared primitive as the LSP
332
+ // loader and as `findNestedProjectMutationValue` below. This walk ran to the
333
+ // filesystem root before, so a `.pi-lens.json` in `$HOME` (or at `C:\`) was
334
+ // adopted by every project on the machine — the #622/#625 class.
335
+ //
336
+ // It no longer STOPS at the first bearing directory (#2426 review round 4,
337
+ // F4): the first hit still decides `info`, and therefore still decides every
338
+ // value this loader returns, but the walk continues to the ceiling so the
339
+ // legacy documents a user is being asked to migrate are enumerated wherever
340
+ // they sit — beside the winning file or above it.
341
+ for (const dir of configSearchDirs(startDir)) {
240
342
  dirMtimes.push({ dir, mtimeMs: safeDirMtimeMs(dir) });
241
- for (const name of PROJECT_CONFIG_BASENAMES) {
242
- const candidate = path.join(dir, name);
243
- const stat = safeFileStat(candidate);
244
- if (stat?.isFile()) {
245
- return {
246
- info: {
343
+ if (!info) {
344
+ for (const name of PROJECT_CONFIG_BASENAMES) {
345
+ const candidate = path.join(dir, name);
346
+ const stat = safeFileStat(candidate);
347
+ if (stat?.isFile()) {
348
+ info = {
247
349
  path: candidate,
248
350
  dir,
249
351
  mtimeMs: stat.mtimeMs,
250
352
  size: stat.size,
251
- },
252
- dirMtimes,
253
- };
353
+ };
354
+ break;
355
+ }
254
356
  }
255
357
  }
358
+ for (const location of LEGACY_PROJECT_LOCATIONS) {
359
+ const file = path.join(dir, location.relativePath);
360
+ const stat = safeFileStat(file);
361
+ if (!stat?.isFile())
362
+ continue;
363
+ legacySources.push({
364
+ path: file,
365
+ mtimeMs: stat.mtimeMs,
366
+ size: stat.size,
367
+ });
368
+ const outcome = readConfigDocument(file);
369
+ if (outcome.status === "error") {
370
+ reportConfigReadFailure({
371
+ file,
372
+ location,
373
+ tier: "project",
374
+ error: outcome.error,
375
+ sourceText: outcome.sourceText,
376
+ });
377
+ continue;
378
+ }
379
+ if (outcome.status !== "ok")
380
+ continue;
381
+ legacyDocuments.push({
382
+ tier: "project",
383
+ file,
384
+ location,
385
+ value: outcome.value,
386
+ });
387
+ }
256
388
  }
257
- return { info: undefined, dirMtimes };
389
+ return {
390
+ info,
391
+ dirMtimes: bearingDirMtimes(dirMtimes, info),
392
+ legacyRecords: legacyDeprecationRecords(legacyDocuments),
393
+ legacySources,
394
+ checkedAtMs: Date.now(),
395
+ };
258
396
  }
259
- function warnInvalidConfigOnce(configPath, reason) {
260
- const key = `${configPath}:${reason}`;
261
- if (warnedInvalidConfigs.has(key))
262
- return;
263
- warnedInvalidConfigs.add(key);
264
- const message = `ignoring invalid project config ${configPath}: ${reason}`;
265
- logExtension({
266
- subsystem: "project-lens-config",
267
- level: "warn",
268
- message,
269
- metadata: { configPath, reason },
270
- });
271
- // HUMAN-audience too: the user's own `.pi-lens.json` is being ignored.
272
- notifyUserDegradation(`pi-lens: ${message}`);
397
+ /**
398
+ * The legacy documents' deprecation records, BOUNDED per document (#2426
399
+ * review round 5, F-A/F-B).
400
+ *
401
+ * This path shipped its records raw. A legacy file's recognized top-level key
402
+ * count is user input, so one 28-key `pi-lens.json` produced 28 notifications
403
+ * through this loader while the LSP loader — which finalizes the same records —
404
+ * produced the bound plus a count for the identical file, and five nested
405
+ * legacy files produced 145 with no suppression record anywhere. Every producer
406
+ * now goes through the one \`finalizeRecords\`.
407
+ *
408
+ * Per DOCUMENT rather than per walk, because these are not one resolution: each
409
+ * file is separately actionable ("move these keys out of THIS file"), and a
410
+ * per-walk bound would silently drop whole files' advice while naming only the
411
+ * last one. Per document also makes the two loaders agree on the count for a
412
+ * given file, which is the parity `config-notice-ownership` asserts.
413
+ */
414
+ function legacyDeprecationRecords(documents) {
415
+ const home = os.homedir();
416
+ return documents.flatMap((document) => finalizeRecords(deprecationRecords([document], home), {
417
+ file: document.file,
418
+ tier: document.tier,
419
+ }));
420
+ }
421
+ /**
422
+ * The directory mtimes this entry's freshness is keyed on: the BEARING chain —
423
+ * `startDir` up to and including the directory that supplied `info` (#2426
424
+ * review round 5, F-C).
425
+ *
426
+ * The walk itself no longer stops at the first bearing directory, because the
427
+ * legacy documents a user is asked to migrate can sit above it. Keying
428
+ * freshness on every directory it VISITS is a different decision, and a bad
429
+ * one: the walk runs to just below `$HOME`, so the entry was invalidated by any
430
+ * churn in an ancestor like `~/Desktop` — a full re-walk, re-read and re-parse
431
+ * of every legacy document, on a hot path, triggered by an unrelated directory.
432
+ *
433
+ * Scoping it back to the bearing chain restores the pre-#2426 invalidation
434
+ * surface exactly. Nothing that decides a VALUE is outside that chain, and an
435
+ * already-discovered legacy document above it carries its own (mtime, size)
436
+ * stamp in `legacySources`, so an EDIT to one is still noticed. What is given
437
+ * up is noticing a legacy file NEWLY CREATED above the bearing directory
438
+ * mid-process — records-only, and strictly more than the pre-#2426 loader saw,
439
+ * since it never looked above that directory at all.
440
+ *
441
+ * When NO config file is found anywhere (#2483), `info` is undefined and there
442
+ * is no bearing directory to stop at — but the majority of projects have no
443
+ * config at all, so this is the COMMON case, not an edge one. Falling back to
444
+ * the full chain here (the pre-fix behavior) keyed freshness on every
445
+ * directory up to just below `$HOME`: unrelated churn in `~/Desktop` or
446
+ * `~/projects` invalidated the entry and forced a full re-walk on the very
447
+ * next load, every load, forever.
448
+ *
449
+ * The nearest directory a config COULD have appeared in is `startDir` itself
450
+ * — `dirMtimes[0]`, first in the innermost-first walk order (`configSearchDirs`)
451
+ * — so that is what per-call freshness is scoped to, the same bearing-directory
452
+ * principle as the presence case (a directory always includes itself as the
453
+ * degenerate bearing chain). A config file created directly in `startDir`
454
+ * bumps `startDir`'s own mtime and is found on the next load.
455
+ *
456
+ * A config file created further up the chain does not move `startDir`'s own
457
+ * mtime, so it is invisible to THIS check alone (round 1 of #2483 stopped
458
+ * here, leaving staleness unbounded — no production caller ever resets the
459
+ * cache and a same-`startDir` load never rediscovers). `discoveryCacheStillFresh`
460
+ * closes that gap: a no-`info` entry is force-expired at most once per
461
+ * `FRESHNESS_CADENCE_MS`, independent of what `dirMtimes` says, which sends
462
+ * the NEXT load through a full re-walk of the ancestor chain regardless — so
463
+ * a config created anywhere in it is noticed within one cadence window.
464
+ * Bounded staleness, and strictly narrower than the pre-fix "any ancestor up
465
+ * to $HOME invalidates on every load".
466
+ */
467
+ function bearingDirMtimes(dirMtimes, info) {
468
+ if (info) {
469
+ const bearing = dirMtimes.findIndex((entry) => entry.dir === info.dir);
470
+ return bearing < 0 ? [...dirMtimes] : dirMtimes.slice(0, bearing + 1);
471
+ }
472
+ return dirMtimes.slice(0, 1);
273
473
  }
274
- function parseRulePolicyList(configPath, ruleId, key, value) {
474
+ function parseRulePolicyList(note, ruleId, key, value) {
275
475
  if (!Array.isArray(value)) {
276
- warnInvalidConfigOnce(configPath, `rules.${ruleId}.${key} must be an array of strings`);
476
+ note(`rules.${ruleId}.${key} must be an array of strings`);
277
477
  return { list: [], invalid: true };
278
478
  }
279
479
  const list = [];
@@ -290,34 +490,78 @@ function parseRulePolicyList(configPath, ruleId, key, value) {
290
490
  // but none were usable strings (all blank / non-string), which is a real
291
491
  // authoring mistake that must not fail silently.
292
492
  if (value.length > 0) {
293
- warnInvalidConfigOnce(configPath, `rules.${ruleId}.${key} must contain at least one non-empty string`);
493
+ note(`rules.${ruleId}.${key} must contain at least one non-empty string`);
294
494
  return { list: [], invalid: true };
295
495
  }
296
496
  return { list: [], invalid: false };
297
497
  }
298
498
  return { list, invalid: false };
299
499
  }
500
+ const NO_RECORDS = [];
300
501
  function parseConfigFile(configPath) {
301
- let raw;
302
- try {
303
- const text = fs.readFileSync(configPath, "utf-8");
304
- raw = JSON.parse(text);
502
+ const { note, records: ignoredRecords } = ignoredRecordCollector(configPath, "project");
503
+ const outcome = readConfigDocument(configPath);
504
+ if (outcome.status === "error") {
505
+ // `{ parseError }`, not a pre-stringified message (#2431): the raw error
506
+ // reaches one seam, which decides how much of a `JSON.parse`
507
+ // `SyntaxError#message` (which embeds a snippet of the file being parsed)
508
+ // survives into the sinks.
509
+ note({ parseError: outcome.error, sourceText: outcome.sourceText });
510
+ return {
511
+ config: EMPTY_PROJECT_CONFIG,
512
+ records: NO_RECORDS,
513
+ ignored: ignoredRecords(),
514
+ };
305
515
  }
306
- catch (error) {
307
- warnInvalidConfigOnce(configPath, error instanceof Error ? error.message : "failed to parse JSON");
308
- return EMPTY_PROJECT_CONFIG;
516
+ // The file was stat'd as present by the discovery above, so `missing` here is
517
+ // a delete that raced the read. Nothing to warn about — the next call
518
+ // re-discovers and finds no config at all.
519
+ if (outcome.status === "missing") {
520
+ return {
521
+ config: EMPTY_PROJECT_CONFIG,
522
+ records: NO_RECORDS,
523
+ ignored: NO_RECORDS,
524
+ };
309
525
  }
310
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
311
- warnInvalidConfigOnce(configPath, "top-level value must be an object");
312
- return EMPTY_PROJECT_CONFIG;
526
+ if (!outcome.value ||
527
+ typeof outcome.value !== "object" ||
528
+ Array.isArray(outcome.value)) {
529
+ note("top-level value must be an object");
530
+ return {
531
+ config: EMPTY_PROJECT_CONFIG,
532
+ records: NO_RECORDS,
533
+ ignored: ignoredRecords(),
534
+ };
313
535
  }
314
- const obj = raw;
536
+ // Through the #2425 core (#2426): the value is validated against the
537
+ // canonical schema before this loader's own field parsing sees it, so the
538
+ // depth bound, the prototype-key policy and the declared `lsp.*` types apply
539
+ // here exactly as they do in the LSP loader. `raw` is the VALIDATED value
540
+ // rather than the parse output, which is the one place the field's
541
+ // documented "the parsed JSON as-is" narrows: a `__proto__` key or a
542
+ // 4000-deep blob no longer rides into `.raw`'s consumers (`trivy-client.ts`,
543
+ // the helm-render runner).
544
+ const resolved = resolveOnePiLensConfigDocument({
545
+ tier: "project",
546
+ file: configPath,
547
+ location: projectLocationFor(configPath),
548
+ value: outcome.value,
549
+ });
550
+ // The records are RETURNED, not reported here (#2426 review round 3, F2):
551
+ // this function's result is what the caller caches, and a cache HIT must
552
+ // still report them every session — a call this function never makes on a
553
+ // hit. Reporting here would fire only on a cache MISS (session 1), and
554
+ // `resetProjectLensConfigCache` has no production caller, so every session
555
+ // after the first silently lost the `config-deprecated` ledger row despite
556
+ // the file still being the exact deprecated one on disk.
557
+ const raw = resolved.value;
558
+ const obj = resolved.value;
315
559
  const ignore = Array.isArray(obj.ignore)
316
560
  ? obj.ignore.filter((p) => typeof p === "string")
317
561
  : [];
318
562
  const mutations = {};
319
563
  for (const spec of PROJECT_SCOPED_LENS_FLAGS) {
320
- assignFlagConfigSection(obj, mutations, spec.configKey, (reason) => warnInvalidConfigOnce(configPath, reason));
564
+ assignFlagConfigSection(obj, mutations, spec.configKey, note);
321
565
  }
322
566
  const rules = {};
323
567
  if (obj.rules && typeof obj.rules === "object" && !Array.isArray(obj.rules)) {
@@ -327,7 +571,7 @@ function parseConfigFile(configPath) {
327
571
  // disable`), which lands here as an array and would otherwise be
328
572
  // dropped without a word — the one shape a user is most likely to try.
329
573
  if (!ruleCfg || typeof ruleCfg !== "object" || Array.isArray(ruleCfg)) {
330
- warnInvalidConfigOnce(configPath, `rules.${ruleId} must be an object with threshold, disable, or select; ignored`);
574
+ note(`rules.${ruleId} must be an object with threshold, disable, or select; ignored`);
331
575
  continue;
332
576
  }
333
577
  const r = ruleCfg;
@@ -338,17 +582,17 @@ function parseConfigFile(configPath) {
338
582
  entry.threshold = r.threshold;
339
583
  }
340
584
  else if ("threshold" in r) {
341
- warnInvalidConfigOnce(configPath, `rules.${ruleId}.threshold must be a positive finite number`);
585
+ note(`rules.${ruleId}.threshold must be a positive finite number`);
342
586
  }
343
587
  if ("disable" in r) {
344
- const parsed = parseRulePolicyList(configPath, ruleId, "disable", r.disable);
588
+ const parsed = parseRulePolicyList(note, ruleId, "disable", r.disable);
345
589
  // #1087: an explicitly empty list is valid-but-empty (no warning);
346
590
  // don't store a pointless no-op entry for it.
347
591
  if (!parsed.invalid && parsed.list.length > 0)
348
592
  entry.disable = parsed.list;
349
593
  }
350
594
  if ("select" in r) {
351
- const parsed = parseRulePolicyList(configPath, ruleId, "select", r.select);
595
+ const parsed = parseRulePolicyList(note, ruleId, "select", r.select);
352
596
  if (!parsed.invalid && parsed.list.length > 0)
353
597
  entry.select = parsed.list;
354
598
  }
@@ -361,7 +605,7 @@ function parseConfigFile(configPath) {
361
605
  rules[ruleId] = entry;
362
606
  }
363
607
  else if (!("threshold" in r) && !("disable" in r) && !("select" in r)) {
364
- warnInvalidConfigOnce(configPath, `rules.${ruleId} has no recognized setting (threshold, disable, select); ignored`);
608
+ note(`rules.${ruleId} has no recognized setting (threshold, disable, select); ignored`);
365
609
  }
366
610
  }
367
611
  }
@@ -373,7 +617,7 @@ function parseConfigFile(configPath) {
373
617
  maxProjectFiles = obj.maxProjectFiles;
374
618
  }
375
619
  else {
376
- warnInvalidConfigOnce(configPath, "maxProjectFiles must be a positive finite number");
620
+ note("maxProjectFiles must be a positive finite number");
377
621
  }
378
622
  }
379
623
  let reviewGraph;
@@ -381,7 +625,7 @@ function parseConfigFile(configPath) {
381
625
  if (!obj.reviewGraph ||
382
626
  typeof obj.reviewGraph !== "object" ||
383
627
  Array.isArray(obj.reviewGraph)) {
384
- warnInvalidConfigOnce(configPath, "reviewGraph must be an object");
628
+ note("reviewGraph must be an object");
385
629
  }
386
630
  else {
387
631
  const rg = obj.reviewGraph;
@@ -392,7 +636,44 @@ function parseConfigFile(configPath) {
392
636
  reviewGraph = { maxFiles: clamped };
393
637
  }
394
638
  else {
395
- warnInvalidConfigOnce(configPath, "reviewGraph.maxFiles must be a positive finite number");
639
+ note("reviewGraph.maxFiles must be a positive finite number");
640
+ }
641
+ }
642
+ }
643
+ }
644
+ let startup;
645
+ if (obj.startup !== undefined) {
646
+ if (!obj.startup ||
647
+ typeof obj.startup !== "object" ||
648
+ Array.isArray(obj.startup)) {
649
+ note("startup must be an object");
650
+ }
651
+ else {
652
+ const section = obj.startup;
653
+ if (section.mode === "quick" ||
654
+ section.mode === "full" ||
655
+ section.mode === "minimal") {
656
+ startup = { mode: section.mode };
657
+ }
658
+ else if ("mode" in section) {
659
+ note('startup.mode must be "quick", "full", or "minimal"');
660
+ }
661
+ if (section.scans !== undefined) {
662
+ if (!section.scans ||
663
+ typeof section.scans !== "object" ||
664
+ Array.isArray(section.scans)) {
665
+ note("startup.scans must be an object");
666
+ }
667
+ else if (typeof section.scans.enabled ===
668
+ "boolean") {
669
+ startup ??= {};
670
+ startup.scans = {
671
+ enabled: section.scans
672
+ .enabled,
673
+ };
674
+ }
675
+ else if ("enabled" in section.scans) {
676
+ note("startup.scans.enabled must be a boolean");
396
677
  }
397
678
  }
398
679
  }
@@ -407,7 +688,7 @@ function parseConfigFile(configPath) {
407
688
  // than being lumped in with typos — docs previously called this "silently
408
689
  // ignored".
409
690
  const knownProjectKeys = new Set([
410
- ...PROJECT_OWN_CONFIG_KEYS,
691
+ ...PROJECT_NON_FLAG_CONFIG_SECTIONS,
411
692
  ...flagConfigSectionKeys(PROJECT_SCOPED_LENS_FLAGS),
412
693
  ...PROJECT_FOREIGN_CONFIG_NAMESPACES,
413
694
  ]);
@@ -415,23 +696,58 @@ function parseConfigFile(configPath) {
415
696
  ...flagConfigSectionKeys(LENS_FLAGS),
416
697
  ...GLOBAL_NON_FLAG_CONFIG_SECTIONS,
417
698
  ].filter((key) => !knownProjectKeys.has(key)));
699
+ // The full DOTTED config keys of the flags a project file cannot set, for the
700
+ // sub-key scan below. Section-level keys are too coarse for a namespace that
701
+ // mixes scopes: `lsp` holds four project-scoped settings AND the global-only
702
+ // `lsp.enabled`.
703
+ const projectScoped = new Set(PROJECT_SCOPED_LENS_FLAGS.map((spec) => spec.configKey));
704
+ const globalScopeOnlyFlagKeys = LENS_FLAGS.map((spec) => spec.configKey).filter((configKey) => !projectScoped.has(configKey));
705
+ // The literal path is derived, not hand-typed (#2426 review round 3, S1):
706
+ // under `PI_LENS_CONFIG_PATH` the canonical global config is NOT
707
+ // `~/.pi-lens/config.json`, and a hardcoded string in this notice would name
708
+ // a file the resolver never reads.
709
+ const globalConfigLabel = homeRelativePath(getPiLensGlobalConfigPath(), os.homedir());
710
+ const warnUnhonoredKey = (label, globalOnly) => {
711
+ note(globalOnly
712
+ ? `"${label}" is a global-only pi-lens setting and is not honored in a project .pi-lens.json (set it in ${globalConfigLabel} or pass the matching CLI flag); ignored`
713
+ : `unknown key "${label}" is not a recognized pi-lens setting (check for a typo); ignored`);
714
+ };
418
715
  for (const key of Object.keys(obj)) {
419
716
  if (knownProjectKeys.has(key))
420
717
  continue;
421
- if (globalScopeOnlyKeys.has(key)) {
422
- warnInvalidConfigOnce(configPath, `"${key}" is a global-only pi-lens setting and is not honored in a project .pi-lens.json (set it in ~/.pi-lens/config.json or pass the matching CLI flag); ignored`);
718
+ warnUnhonoredKey(key, globalScopeOnlyKeys.has(key));
719
+ }
720
+ // A tolerated foreign namespace is tolerated KEY BY KEY (#2426 review round
721
+ // 2, F3). `lsp` is read out of this file by the LSP loader, but only four of
722
+ // its keys are project-scoped; `lsp.enabled` is the `--no-lsp` flag, which is
723
+ // `scope: "global"`. Blanket namespace tolerance swallowed it silently, which
724
+ // `docs/configuration.md`'s "nothing is ignored silently" forbids — so every
725
+ // other sub-key gets the same global-only-vs-typo signal a top-level key does,
726
+ // named with its DOTTED path so the notice says which setting is meant.
727
+ for (const [namespace, honored] of PROJECT_FOREIGN_NAMESPACE_HONORED_KEYS) {
728
+ const section = obj[namespace];
729
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
730
+ continue;
423
731
  }
424
- else {
425
- warnInvalidConfigOnce(configPath, `unknown key "${key}" is not a recognized pi-lens setting (check for a typo); ignored`);
732
+ for (const key of Object.keys(section)) {
733
+ if (honored.includes(key))
734
+ continue;
735
+ const dotted = `${namespace}.${key}`;
736
+ warnUnhonoredKey(dotted, globalScopeOnlyFlagKeys.some((configKey) => configKey === dotted || configKey.startsWith(`${dotted}.`)));
426
737
  }
427
738
  }
428
739
  return {
429
- ignore,
430
- rules,
431
- ...mutations,
432
- maxProjectFiles,
433
- reviewGraph,
434
- raw,
435
- configPath,
740
+ config: {
741
+ ...(startup === undefined ? {} : { startup }),
742
+ ignore,
743
+ rules,
744
+ ...mutations,
745
+ maxProjectFiles,
746
+ reviewGraph,
747
+ raw,
748
+ configPath,
749
+ },
750
+ records: resolved.records,
751
+ ignored: ignoredRecords(),
436
752
  };
437
753
  }