@herbertgao/pi-extensions 2026.9.0 → 2026.9.1

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