pi-lens 3.8.73 → 4.0.0

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 (346) hide show
  1. package/CHANGELOG.md +2614 -12
  2. package/README.md +50 -21
  3. package/dist/clients/actionable-warnings.js +73 -18
  4. package/dist/clients/agent-behavior-client.js +21 -4
  5. package/dist/clients/ast-grep-client.js +68 -16
  6. package/dist/clients/ast-grep-types.js +0 -1
  7. package/dist/clients/ast-grep-yaml-synth.js +37 -10
  8. package/dist/clients/atomic-write-staging.js +63 -0
  9. package/dist/clients/atomic-write.js +116 -16
  10. package/dist/clients/bash-file-access.js +193 -23
  11. package/dist/clients/biome-client.js +29 -25
  12. package/dist/clients/bootstrap.js +11 -2
  13. package/dist/clients/bounded-cache.js +34 -0
  14. package/dist/clients/bounded-pid-file-lock.js +192 -0
  15. package/dist/clients/bus-events-logger.js +5 -1
  16. package/dist/clients/bus-publish.js +10 -3
  17. package/dist/clients/cache/rule-cache.js +89 -6
  18. package/dist/clients/cache-manager.js +97 -9
  19. package/dist/clients/cache-observability.js +498 -0
  20. package/dist/clients/call-graph.js +460 -108
  21. package/dist/clients/cascade-logger.js +5 -1
  22. package/dist/clients/child-unref.js +85 -0
  23. package/dist/clients/codebase-model.js +52 -23
  24. package/dist/clients/collateral-test-role.js +40 -0
  25. package/dist/clients/complexity-client.js +6 -8
  26. package/dist/clients/console-guard-install.js +14 -0
  27. package/dist/clients/cooperative-budget.js +42 -0
  28. package/dist/clients/dead-code-client.js +2 -1
  29. package/dist/clients/debug-handles.js +206 -0
  30. package/dist/clients/debug-heap.js +167 -0
  31. package/dist/clients/degradation-ledger.js +153 -0
  32. package/dist/clients/dependency-checker.js +466 -63
  33. package/dist/clients/diagnostic-dispositions.js +83 -13
  34. package/dist/clients/diagnostics-publish.js +10 -3
  35. package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
  36. package/dist/clients/dispatch/dispatcher.js +71 -13
  37. package/dist/clients/dispatch/fact-store.js +11 -0
  38. package/dist/clients/dispatch/facts/function-facts.js +37 -7
  39. package/dist/clients/dispatch/facts/import-facts.js +108 -119
  40. package/dist/clients/dispatch/facts/tree-sitter-facts.js +12 -12
  41. package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
  42. package/dist/clients/dispatch/indent-detect.js +35 -0
  43. package/dist/clients/dispatch/inline-suppressions.js +17 -8
  44. package/dist/clients/dispatch/integration.js +1094 -611
  45. package/dist/clients/dispatch/lazy.js +14 -0
  46. package/dist/clients/dispatch/plan.js +11 -1
  47. package/dist/clients/dispatch/rule-id-normalize.js +50 -0
  48. package/dist/clients/dispatch/rule-policy.js +135 -0
  49. package/dist/clients/dispatch/rules/async-noise.js +1 -0
  50. package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
  51. package/dist/clients/dispatch/runners/ast-grep-napi.js +47 -6
  52. package/dist/clients/dispatch/runners/biome-check.js +5 -9
  53. package/dist/clients/dispatch/runners/detekt.js +3 -2
  54. package/dist/clients/dispatch/runners/hadolint.js +2 -1
  55. package/dist/clients/dispatch/runners/helm-lint.js +154 -0
  56. package/dist/clients/dispatch/runners/htmlhint.js +3 -2
  57. package/dist/clients/dispatch/runners/index.js +5 -1
  58. package/dist/clients/dispatch/runners/lsp.js +14 -6
  59. package/dist/clients/dispatch/runners/markdownlint.js +4 -9
  60. package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
  61. package/dist/clients/dispatch/runners/pyright.js +9 -5
  62. package/dist/clients/dispatch/runners/ruff.js +4 -8
  63. package/dist/clients/dispatch/runners/shellcheck.js +2 -3
  64. package/dist/clients/dispatch/runners/shfmt.js +2 -3
  65. package/dist/clients/dispatch/runners/spotbugs.js +2 -1
  66. package/dist/clients/dispatch/runners/taplo.js +2 -1
  67. package/dist/clients/dispatch/runners/terragrunt.js +163 -0
  68. package/dist/clients/dispatch/runners/tflint.js +37 -5
  69. package/dist/clients/dispatch/runners/tree-sitter.js +155 -166
  70. package/dist/clients/dispatch/runners/trivy-config.js +13 -7
  71. package/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  72. package/dist/clients/dispatch/runners/utils/runner-helpers.js +284 -34
  73. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  74. package/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  75. package/dist/clients/disposition-publish.js +10 -3
  76. package/dist/clients/durable-store.js +97 -0
  77. package/dist/clients/event-loop-monitor.js +94 -3
  78. package/dist/clients/extension-log.js +139 -0
  79. package/dist/clients/extension-mode.js +73 -0
  80. package/dist/clients/feature-hints.js +53 -7
  81. package/dist/clients/file-kinds.js +143 -31
  82. package/dist/clients/file-role.js +18 -3
  83. package/dist/clients/file-utils.js +56 -34
  84. package/dist/clients/fix-worklog.js +14 -2
  85. package/dist/clients/format-events-publish.js +16 -3
  86. package/dist/clients/format-service.js +24 -8
  87. package/dist/clients/formatters-lazy.js +8 -0
  88. package/dist/clients/formatters.js +358 -61
  89. package/dist/clients/generated-artifacts.js +169 -15
  90. package/dist/clients/git-guard.js +681 -11
  91. package/dist/clients/git-tracked-ignore.js +4 -0
  92. package/dist/clients/go-client.js +2 -1
  93. package/dist/clients/govulncheck-client.js +8 -0
  94. package/dist/clients/grammar-source.js +12 -4
  95. package/dist/clients/gzip-stage-write.js +102 -0
  96. package/dist/clients/host-ports.js +31 -0
  97. package/dist/clients/installer/index.js +733 -213
  98. package/dist/clients/instance-reaper.js +142 -172
  99. package/dist/clients/jscpd-client.js +31 -54
  100. package/dist/clients/knip-client.js +78 -36
  101. package/dist/clients/language-policy.js +14 -2
  102. package/dist/clients/language-profile.js +72 -77
  103. package/dist/clients/latency-logger.js +28 -16
  104. package/dist/clients/lens-config.js +140 -166
  105. package/dist/clients/lens-engine.js +124 -21
  106. package/dist/clients/lens-events.js +27 -4
  107. package/dist/clients/lens-flag-registry.js +293 -0
  108. package/dist/clients/lens-map.js +96 -26
  109. package/dist/clients/live-bus-emitter.js +30 -0
  110. package/dist/clients/log-cleanup.js +8 -18
  111. package/dist/clients/lsp/cascade-tier.js +23 -3
  112. package/dist/clients/lsp/client.js +566 -57
  113. package/dist/clients/lsp/config.js +74 -10
  114. package/dist/clients/lsp/diagnostic-binding.js +86 -0
  115. package/dist/clients/lsp/edits.js +1012 -121
  116. package/dist/clients/lsp/index.js +1840 -803
  117. package/dist/clients/lsp/language.js +4 -1
  118. package/dist/clients/lsp/launch.js +40 -32
  119. package/dist/clients/lsp/path-utils.js +1 -1
  120. package/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  121. package/dist/clients/lsp/server.js +279 -60
  122. package/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  123. package/dist/clients/lsp-document-symbols.js +112 -0
  124. package/dist/clients/lsp-lazy.js +8 -0
  125. package/dist/clients/lsp-mutation.js +248 -0
  126. package/dist/clients/mcp/analyze.js +128 -36
  127. package/dist/clients/mcp/ipc.js +157 -24
  128. package/dist/clients/mcp/review.js +36 -3
  129. package/dist/clients/mcp/session.js +216 -8
  130. package/dist/clients/memory-sampler.js +115 -0
  131. package/dist/clients/metrics-client.js +2 -1
  132. package/dist/clients/metrics-history.js +2 -1
  133. package/dist/clients/module-report-lsp.js +5 -0
  134. package/dist/clients/module-report.js +267 -82
  135. package/dist/clients/ndjson-logger.js +334 -103
  136. package/dist/clients/opengrep-client.js +1 -1
  137. package/dist/clients/partial-edit-apply.js +120 -5
  138. package/dist/clients/path-keyed-map.js +76 -0
  139. package/dist/clients/path-utils.js +141 -5
  140. package/dist/clients/performance-report.js +327 -0
  141. package/dist/clients/pipeline.js +54 -13
  142. package/dist/clients/print-mode.js +21 -0
  143. package/dist/clients/project-changes.js +117 -4
  144. package/dist/clients/project-diagnostics/cache.js +3 -2
  145. package/dist/clients/project-diagnostics/extractors.js +26 -115
  146. package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
  147. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +117 -0
  148. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
  149. package/dist/clients/project-diagnostics/scanner.js +268 -128
  150. package/dist/clients/project-lens-config.js +138 -34
  151. package/dist/clients/project-report.js +64 -25
  152. package/dist/clients/project-scale.js +8 -10
  153. package/dist/clients/project-snapshot-persist-worker.js +6 -0
  154. package/dist/clients/project-snapshot.js +767 -8
  155. package/dist/clients/project-trust.js +181 -0
  156. package/dist/clients/quiet-window.js +27 -5
  157. package/dist/clients/read-bridge.js +147 -0
  158. package/dist/clients/read-expansion.js +22 -12
  159. package/dist/clients/read-guard-logger.js +159 -1
  160. package/dist/clients/read-guard-tool-lines.js +150 -29
  161. package/dist/clients/read-guard.js +262 -3
  162. package/dist/clients/redact/secrets.js +449 -0
  163. package/dist/clients/resource-sampler.js +144 -39
  164. package/dist/clients/reverse-deps.js +48 -0
  165. package/dist/clients/review-graph/builder.js +2570 -326
  166. package/dist/clients/review-graph/git-identity.js +2 -1
  167. package/dist/clients/review-graph/import-resolvers.js +21 -10
  168. package/dist/clients/review-graph/persist-worker.js +10 -0
  169. package/dist/clients/review-graph/query.js +5 -0
  170. package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
  171. package/dist/clients/review-graph/symbol-id.js +46 -0
  172. package/dist/clients/review-graph/tsconfig-paths.js +57 -6
  173. package/dist/clients/review-graph/workspace-modules.js +51 -3
  174. package/dist/clients/review-graph-logger.js +53 -0
  175. package/dist/clients/ruff-client.js +27 -44
  176. package/dist/clients/runtime-agent-end.js +111 -75
  177. package/dist/clients/runtime-context.js +36 -12
  178. package/dist/clients/runtime-coordinator.js +76 -8
  179. package/dist/clients/runtime-session.js +799 -137
  180. package/dist/clients/runtime-tool-call.js +117 -62
  181. package/dist/clients/runtime-tool-result.js +218 -23
  182. package/dist/clients/runtime-turn.js +284 -40
  183. package/dist/clients/rust-client.js +2 -1
  184. package/dist/clients/safe-spawn.js +873 -71
  185. package/dist/clients/security-scan-client.js +2 -1
  186. package/dist/clients/session-start-observability.js +15 -0
  187. package/dist/clients/session-state-store.js +5 -3
  188. package/dist/clients/sessionstart-logger.js +18 -0
  189. package/dist/clients/sg-runner.js +284 -159
  190. package/dist/clients/slow-fs.js +2 -1
  191. package/dist/clients/smells-rollup.js +224 -0
  192. package/dist/clients/source-filter.js +231 -43
  193. package/dist/clients/source-walker.js +66 -10
  194. package/dist/clients/startup-scan.js +15 -16
  195. package/dist/clients/startup-timing.js +9 -0
  196. package/dist/clients/test-runner-client.js +38 -8
  197. package/dist/clients/todo-scanner.js +30 -3
  198. package/dist/clients/tool-event.js +35 -0
  199. package/dist/clients/tool-policy.js +388 -39
  200. package/dist/clients/tool-render.js +155 -0
  201. package/dist/clients/tree-sitter-cache.js +153 -140
  202. package/dist/clients/tree-sitter-client.js +1722 -80
  203. package/dist/clients/tree-sitter-logger.js +64 -1
  204. package/dist/clients/tree-sitter-query-loader.js +117 -12
  205. package/dist/clients/tree-sitter-shared.js +48 -19
  206. package/dist/clients/tree-sitter-symbol-extractor.js +115 -11
  207. package/dist/clients/user-notify.js +51 -0
  208. package/dist/clients/vanished-instance-marker.js +57 -0
  209. package/dist/clients/warm-attach.js +33 -13
  210. package/dist/clients/widget-state.js +342 -49
  211. package/dist/clients/word-index-logger.js +44 -0
  212. package/dist/clients/word-index.js +612 -68
  213. package/dist/clients/workspace-topology.js +100 -7
  214. package/dist/index.js +65923 -47161
  215. package/dist/mcp/analyze-cli.js +185 -29
  216. package/dist/mcp/cli.js +110 -0
  217. package/dist/mcp/server.js +272 -50
  218. package/dist/tools/ast-grep-search.js +93 -27
  219. package/dist/tools/lens-diagnostics.js +247 -41
  220. package/dist/tools/lsp-diagnostics.js +153 -40
  221. package/dist/tools/lsp-navigation.js +51 -31
  222. package/dist/tools/module-report.js +9 -0
  223. package/dist/tools/symbol-search.js +21 -2
  224. package/docs/agent-guide.md +247 -0
  225. package/docs/agent-tools.md +21 -10
  226. package/docs/analysisall.md +192 -0
  227. package/docs/api-ports-inventory.md +165 -0
  228. package/docs/ast-grep_rules_catalog.md +5 -2
  229. package/docs/astplayground.md +3 -4
  230. package/docs/audit1.md +5 -5
  231. package/docs/durable-store-audit-1202.md +32 -0
  232. package/docs/environment-variables.md +197 -11
  233. package/docs/fable.md +27 -0
  234. package/docs/features.md +35 -5
  235. package/docs/globalconfig.md +101 -5
  236. package/docs/language-coverage.md +6 -5
  237. package/docs/lsp-capability-matrix.md +2 -2
  238. package/docs/mcp.md +54 -20
  239. package/docs/servercapabilities.md +7 -4
  240. package/docs/settings.md +192 -0
  241. package/docs/tools.md +18 -1
  242. package/docs/tree-sitter_rules_catalog.md +11 -31
  243. package/docs/usage.md +31 -0
  244. package/package.json +139 -138
  245. package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
  246. package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
  247. package/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  248. package/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  249. package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
  250. package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
  251. package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
  252. package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
  253. package/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  254. package/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  255. package/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  256. package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
  257. package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
  258. package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
  259. package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
  260. package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
  261. package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
  262. package/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  263. package/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  264. package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
  265. package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
  266. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
  267. package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
  268. package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
  269. package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
  270. package/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  271. package/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  272. package/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  273. package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
  274. package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
  275. package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
  276. package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
  277. package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
  278. package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
  279. package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
  280. package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
  281. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
  282. package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
  283. package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
  284. package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
  285. package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
  286. package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
  287. package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
  288. package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
  289. package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
  290. package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
  291. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
  292. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
  293. package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
  294. package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
  295. package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
  296. package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
  297. package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
  298. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
  299. package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
  300. package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
  301. package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
  302. package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
  303. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
  304. package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
  305. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
  306. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
  307. package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
  308. package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
  309. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
  310. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
  311. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  312. package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
  313. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
  314. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
  315. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
  316. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
  317. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
  318. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
  319. package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
  320. package/rules/typos/_typos.toml +17 -0
  321. package/scripts/analyze-pi-lens-logs.mjs +55 -2
  322. package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
  323. package/dist/clients/ast-grep-parser.js +0 -86
  324. package/dist/clients/config-enabled-shape.js +0 -39
  325. package/dist/clients/lsp/interactive-install.js +0 -367
  326. package/dist/clients/lsp/lsp-index.js +0 -10
  327. package/dist/clients/lsp/server-strategies.js +0 -5
  328. package/dist/clients/project-metadata.js +0 -690
  329. package/dist/clients/source-groups.js +0 -96
  330. package/dist/clients/types.js +0 -11
  331. package/docs/env_variables.md +0 -10
  332. package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
  333. package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
  334. package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
  335. package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
  336. package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
  337. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
  338. package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
  339. package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
  340. package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
  341. package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
  342. package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
  343. package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
  344. package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
  345. package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
  346. package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
@@ -57,14 +57,34 @@
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
62
  import * as fs from "node:fs";
61
63
  import * as os from "node:os";
62
64
  import * as path from "node:path";
63
- import { parseEnabledShape } from "./config-enabled-shape.js";
64
65
  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";
65
67
  import { isAtOrAboveHomeDir, walkUpDirs } from "./path-utils.js";
66
68
  import { findPiLensConfigMarkerInDir } from "./workspace-topology.js";
67
69
  const PROJECT_CONFIG_BASENAMES = [".pi-lens.json", "pi-lens.json"];
70
+ /**
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.
80
+ */
81
+ const PROJECT_OWN_CONFIG_KEYS = [
82
+ "ignore",
83
+ "rules",
84
+ "maxProjectFiles",
85
+ "reviewGraph",
86
+ "trivy",
87
+ ];
68
88
  /** Clamp bounds for `reviewGraph.maxFiles` — see the field's doc comment above. */
69
89
  const REVIEW_GRAPH_MAX_FILES_MIN = 100;
70
90
  const REVIEW_GRAPH_MAX_FILES_MAX = 20_000;
@@ -89,11 +109,17 @@ export function loadPiLensProjectConfig(startDir, preloadedInfo = findPiLensProj
89
109
  if (!configInfo)
90
110
  return EMPTY_PROJECT_CONFIG;
91
111
  const cached = configCache.get(configInfo.path);
92
- if (cached && cached.mtimeMs === configInfo.mtimeMs) {
112
+ if (cached &&
113
+ cached.mtimeMs === configInfo.mtimeMs &&
114
+ cached.size === configInfo.size) {
93
115
  return cached.config;
94
116
  }
95
117
  const config = parseConfigFile(configInfo.path);
96
- configCache.set(configInfo.path, { mtimeMs: configInfo.mtimeMs, config });
118
+ configCache.set(configInfo.path, {
119
+ mtimeMs: configInfo.mtimeMs,
120
+ size: configInfo.size,
121
+ config,
122
+ });
97
123
  return config;
98
124
  }
99
125
  /** For tests + callers that need to force a re-read (e.g. config-watcher hooks). */
@@ -119,14 +145,19 @@ export function findPiLensConfigInDir(dir) {
119
145
  const marker = findPiLensConfigMarkerInDir(dir);
120
146
  if (!marker)
121
147
  return undefined;
122
- return { path: marker.path, dir: marker.dir, mtimeMs: marker.mtimeMs };
148
+ return {
149
+ path: marker.path,
150
+ dir: marker.dir,
151
+ mtimeMs: marker.mtimeMs,
152
+ size: marker.size,
153
+ };
123
154
  }
124
155
  /**
125
156
  * Find the closest config, between an edited file and the project root, that
126
157
  * explicitly defines one mutation flag. The walk uses the shared primitive
127
158
  * and refuses to inspect HOME or any ancestor of HOME.
128
159
  */
129
- export function findNestedProjectMutationValue(name, editedFilePath, projectRoot, homeDir = os.homedir()) {
160
+ export function findNestedProjectMutationValue(spec, editedFilePath, projectRoot, homeDir = os.homedir()) {
130
161
  const root = path.resolve(projectRoot);
131
162
  const start = path.dirname(path.resolve(editedFilePath));
132
163
  for (const dir of walkUpDirs(start)) {
@@ -136,11 +167,7 @@ export function findNestedProjectMutationValue(name, editedFilePath, projectRoot
136
167
  if (rel.startsWith("..") || path.isAbsolute(rel))
137
168
  break;
138
169
  const config = loadPiLensConfigInDir(dir);
139
- const enabled = name === "no-autoformat"
140
- ? config.format?.enabled
141
- : name === "no-autofix"
142
- ? config.autofix?.enabled
143
- : config.actionableWarnings?.autoFix?.enabled;
170
+ const enabled = readFlagConfigValue(config, spec.configKey);
144
171
  if (enabled !== undefined)
145
172
  return { value: enabled, dir };
146
173
  if (dir === root)
@@ -160,11 +187,11 @@ export function loadPiLensConfigInDir(dir) {
160
187
  if (!info)
161
188
  return EMPTY_PROJECT_CONFIG;
162
189
  const cached = configCache.get(info.path);
163
- if (cached && cached.mtimeMs === info.mtimeMs) {
190
+ if (cached && cached.mtimeMs === info.mtimeMs && cached.size === info.size) {
164
191
  return cached.config;
165
192
  }
166
193
  const config = parseConfigFile(info.path);
167
- configCache.set(info.path, { mtimeMs: info.mtimeMs, config });
194
+ configCache.set(info.path, { mtimeMs: info.mtimeMs, size: info.size, config });
168
195
  return config;
169
196
  }
170
197
  export function findPiLensProjectConfig(startDir) {
@@ -175,7 +202,7 @@ export function findPiLensProjectConfig(startDir) {
175
202
  return undefined;
176
203
  const stat = safeFileStat(cached.info.path);
177
204
  if (stat?.isFile())
178
- return { ...cached.info, mtimeMs: stat.mtimeMs };
205
+ return { ...cached.info, mtimeMs: stat.mtimeMs, size: stat.size };
179
206
  }
180
207
  const discovered = discoverPiLensProjectConfig(cacheKey);
181
208
  discoveryCache.set(cacheKey, discovered);
@@ -209,7 +236,7 @@ function discoverPiLensProjectConfig(startDir) {
209
236
  const stat = safeFileStat(candidate);
210
237
  if (stat?.isFile()) {
211
238
  return {
212
- info: { path: candidate, dir, mtimeMs: stat.mtimeMs },
239
+ info: { path: candidate, dir, mtimeMs: stat.mtimeMs, size: stat.size },
213
240
  dirMtimes,
214
241
  };
215
242
  }
@@ -222,10 +249,41 @@ function warnInvalidConfigOnce(configPath, reason) {
222
249
  if (warnedInvalidConfigs.has(key))
223
250
  return;
224
251
  warnedInvalidConfigs.add(key);
225
- console.error(`[pi-lens] ignoring invalid project config ${configPath}: ${reason}`);
252
+ const message = `ignoring invalid project config ${configPath}: ${reason}`;
253
+ logExtension({
254
+ subsystem: "project-lens-config",
255
+ level: "warn",
256
+ message,
257
+ metadata: { configPath, reason },
258
+ });
259
+ // HUMAN-audience too: the user's own `.pi-lens.json` is being ignored.
260
+ notifyUserDegradation(`pi-lens: ${message}`);
226
261
  }
227
- function parseEnabledConfig(configPath, fieldPath, value) {
228
- return parseEnabledShape(value, fieldPath, (reason) => warnInvalidConfigOnce(configPath, reason));
262
+ function parseRulePolicyList(configPath, ruleId, key, value) {
263
+ if (!Array.isArray(value)) {
264
+ warnInvalidConfigOnce(configPath, `rules.${ruleId}.${key} must be an array of strings`);
265
+ return { list: [], invalid: true };
266
+ }
267
+ const list = [];
268
+ for (const entry of value) {
269
+ if (typeof entry !== "string")
270
+ continue;
271
+ const trimmed = entry.trim();
272
+ if (trimmed.length > 0)
273
+ list.push(trimmed);
274
+ }
275
+ if (list.length === 0) {
276
+ // #1087: an explicitly empty array (`"disable": []`) is a well-formed
277
+ // no-op, not an error — don't warn. Only warn when the array HAD entries
278
+ // but none were usable strings (all blank / non-string), which is a real
279
+ // authoring mistake that must not fail silently.
280
+ if (value.length > 0) {
281
+ warnInvalidConfigOnce(configPath, `rules.${ruleId}.${key} must contain at least one non-empty string`);
282
+ return { list: [], invalid: true };
283
+ }
284
+ return { list: [], invalid: false };
285
+ }
286
+ return { list, invalid: false };
229
287
  }
230
288
  function parseConfigFile(configPath) {
231
289
  let raw;
@@ -245,36 +303,56 @@ function parseConfigFile(configPath) {
245
303
  const ignore = Array.isArray(obj.ignore)
246
304
  ? obj.ignore.filter((p) => typeof p === "string")
247
305
  : [];
248
- const format = parseEnabledConfig(configPath, "format", obj.format);
249
- const autofix = parseEnabledConfig(configPath, "autofix", obj.autofix);
250
- const actionableWarningsRaw = obj.actionableWarnings &&
251
- typeof obj.actionableWarnings === "object" &&
252
- !Array.isArray(obj.actionableWarnings)
253
- ? obj.actionableWarnings
254
- : undefined;
255
- if (obj.actionableWarnings !== undefined && !actionableWarningsRaw) {
256
- warnInvalidConfigOnce(configPath, "actionableWarnings must be an object");
306
+ const mutations = {};
307
+ for (const spec of PROJECT_SCOPED_LENS_FLAGS) {
308
+ assignFlagConfigSection(obj, mutations, spec.configKey, (reason) => warnInvalidConfigOnce(configPath, reason));
257
309
  }
258
- const actionableWarningsAutoFix = parseEnabledConfig(configPath, "actionableWarnings.autoFix", actionableWarningsRaw?.autoFix);
259
- const actionableWarnings = actionableWarningsRaw
260
- ? { autoFix: actionableWarningsAutoFix }
261
- : undefined;
262
310
  const rules = {};
263
311
  if (obj.rules && typeof obj.rules === "object" && !Array.isArray(obj.rules)) {
264
312
  const rawRules = obj.rules;
265
313
  for (const [ruleId, ruleCfg] of Object.entries(rawRules)) {
314
+ // #444's own example writes the lists directly under `rules` (`rules.
315
+ // disable`), which lands here as an array and would otherwise be
316
+ // dropped without a word — the one shape a user is most likely to try.
266
317
  if (!ruleCfg || typeof ruleCfg !== "object" || Array.isArray(ruleCfg)) {
318
+ warnInvalidConfigOnce(configPath, `rules.${ruleId} must be an object with threshold, disable, or select; ignored`);
267
319
  continue;
268
320
  }
269
321
  const r = ruleCfg;
322
+ const entry = {};
270
323
  if (typeof r.threshold === "number" &&
271
324
  Number.isFinite(r.threshold) &&
272
325
  r.threshold > 0) {
273
- rules[ruleId] = { threshold: r.threshold };
326
+ entry.threshold = r.threshold;
274
327
  }
275
328
  else if ("threshold" in r) {
276
329
  warnInvalidConfigOnce(configPath, `rules.${ruleId}.threshold must be a positive finite number`);
277
330
  }
331
+ if ("disable" in r) {
332
+ const parsed = parseRulePolicyList(configPath, ruleId, "disable", r.disable);
333
+ // #1087: an explicitly empty list is valid-but-empty (no warning);
334
+ // don't store a pointless no-op entry for it.
335
+ if (!parsed.invalid && parsed.list.length > 0)
336
+ entry.disable = parsed.list;
337
+ }
338
+ if ("select" in r) {
339
+ const parsed = parseRulePolicyList(configPath, ruleId, "select", r.select);
340
+ if (!parsed.invalid && parsed.list.length > 0)
341
+ entry.select = parsed.list;
342
+ }
343
+ // Honor both threshold-only and policy-only entries; only drop if
344
+ // the entry had no recognized fields at all (e.g. { unrelated: true }).
345
+ // A recognized-but-malformed field already warned above, so only warn
346
+ // here when nothing recognized was spelled at all — #444 proposed
347
+ // `only` rather than `select`, and that typo must not fail silent.
348
+ if (entry.threshold !== undefined || entry.disable || entry.select) {
349
+ rules[ruleId] = entry;
350
+ }
351
+ else if (!("threshold" in r) &&
352
+ !("disable" in r) &&
353
+ !("select" in r)) {
354
+ warnInvalidConfigOnce(configPath, `rules.${ruleId} has no recognized setting (threshold, disable, select); ignored`);
355
+ }
278
356
  }
279
357
  }
280
358
  let maxProjectFiles;
@@ -309,12 +387,38 @@ function parseConfigFile(configPath) {
309
387
  }
310
388
  }
311
389
  }
390
+ // #533 hygiene: mirror the global loader's unknown-key warn so a typo in a
391
+ // shared `.pi-lens.json` (e.g. `maxProjectFile`, `lps`) produces a signal
392
+ // instead of silently doing nothing. The recognized set is single-sourced
393
+ // (#883): the project loader's own keys + the project-scoped flag sections
394
+ // (registry-derived) + the foreign namespaces the LSP loader reads from this
395
+ // same file. A key recognized ONLY at global scope (e.g. `lsp`, `tests`,
396
+ // `delta`) gets a distinct, honest signal that it does nothing here rather
397
+ // than being lumped in with typos — docs previously called this "silently
398
+ // ignored".
399
+ const knownProjectKeys = new Set([
400
+ ...PROJECT_OWN_CONFIG_KEYS,
401
+ ...flagConfigSectionKeys(PROJECT_SCOPED_LENS_FLAGS),
402
+ ...PROJECT_FOREIGN_CONFIG_NAMESPACES,
403
+ ]);
404
+ const globalScopeOnlyKeys = new Set([
405
+ ...flagConfigSectionKeys(LENS_FLAGS),
406
+ ...GLOBAL_NON_FLAG_CONFIG_SECTIONS,
407
+ ].filter((key) => !knownProjectKeys.has(key)));
408
+ for (const key of Object.keys(obj)) {
409
+ if (knownProjectKeys.has(key))
410
+ continue;
411
+ if (globalScopeOnlyKeys.has(key)) {
412
+ 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`);
413
+ }
414
+ else {
415
+ warnInvalidConfigOnce(configPath, `unknown key "${key}" is not a recognized pi-lens setting (check for a typo); ignored`);
416
+ }
417
+ }
312
418
  return {
313
419
  ignore,
314
420
  rules,
315
- format,
316
- autofix,
317
- actionableWarnings,
421
+ ...mutations,
318
422
  maxProjectFiles,
319
423
  reviewGraph,
320
424
  raw,
@@ -34,9 +34,8 @@
34
34
  * into symbol-node metadata so this path can read it for free.
35
35
  */
36
36
  import * as path from "node:path";
37
- import { normalizeMapKey } from "./path-utils.js";
38
- import { isAtOrAboveHomeDir } from "./path-utils.js";
39
- import { loadProjectSnapshot } from "./project-snapshot.js";
37
+ import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
38
+ import { loadProjectSnapshotWithoutWordIndex } from "./project-snapshot.js";
40
39
  const DEFAULT_LIMIT = 10;
41
40
  const STALE_THRESHOLD_MS = 15 * 60_000; // 15 minutes
42
41
  const LOW_COVERAGE_THRESHOLD = 0.8;
@@ -48,14 +47,19 @@ function clampLimit(limit) {
48
47
  }
49
48
  // Same display-path convention as module-report.ts's toDisplayPath: cwd-relative
50
49
  // + forward-slashed under the project root, else the absolute (slash-normalized)
51
- // path.
50
+ // path. Delegates to the shape-aware toProjectRelativePath (#1163/#1194) instead
51
+ // of hand-rolling with host-default path.isAbsolute/path.relative, which
52
+ // mis-parses a Windows-shaped path (e.g. a persisted graph symbol-key path) on
53
+ // Linux CI: path.isAbsolute("C:\\repo\\x.ts") is false there, short-circuiting
54
+ // to the raw absolute path instead of relativizing (#1024 divergence class).
52
55
  function toDisplayPath(p, projectRoot) {
53
- if (!path.isAbsolute(p))
54
- return p.replace(/\\/g, "/");
55
- const rel = path.relative(projectRoot, p);
56
- return rel && !rel.startsWith("..")
57
- ? rel.replace(/\\/g, "/")
58
- : p.replace(/\\/g, "/");
56
+ return toProjectRelativePath(p, projectRoot);
57
+ }
58
+ /** Test-only export (refs #1194) so the shape-aware delegation can be verified
59
+ * directly without standing up a full review graph — mirrors the
60
+ * `_reset*ForTests` underscore convention used elsewhere in this file. */
61
+ export function _toDisplayPathForTests(p, projectRoot) {
62
+ return toDisplayPath(p, projectRoot);
59
63
  }
60
64
  function suggestedNext(displayPath) {
61
65
  return { tool: "module_report", path: displayPath };
@@ -111,9 +115,10 @@ function buildFileDegrees(graph) {
111
115
  // --- section 1: trust header --------------------------------------------------
112
116
  function computeTrust(graph, cwd) {
113
117
  const filesCovered = graph.fileNodes.size;
114
- const snapshot = loadProjectSnapshot(cwd);
118
+ const snapshot = loadProjectSnapshotWithoutWordIndex(cwd);
115
119
  const snapshotFileCount = snapshot ? Object.keys(snapshot.files).length : 0;
116
- const filesTotal = Math.max(filesCovered, snapshotFileCount);
120
+ const persistedTotal = graph.persistCoverage?.totalFiles ?? 0;
121
+ const filesTotal = Math.max(filesCovered, snapshotFileCount, persistedTotal);
117
122
  const coverage = filesTotal > 0 ? filesCovered / filesTotal : 1;
118
123
  let exact = 0;
119
124
  let imp = 0;
@@ -143,14 +148,28 @@ function computeTrust(graph, cwd) {
143
148
  const frac = (n) => (sampleSize > 0 ? n / sampleSize : 0);
144
149
  const ageMs = Date.now() - Date.parse(graph.builtAt);
145
150
  const stale = Number.isFinite(ageMs) && ageMs > STALE_THRESHOLD_MS;
146
- const lowCoverage = coverage < LOW_COVERAGE_THRESHOLD;
151
+ const lowCoverage = coverage < LOW_COVERAGE_THRESHOLD ||
152
+ graph.persistCoverage?.partial === true;
147
153
  const notes = [];
148
154
  if (stale) {
149
155
  const ageMin = Math.round(ageMs / 60_000);
150
156
  notes.push(`Graph is stale: built ${ageMin}m ago. Sections below may miss recent edits — run pilens_rebuild or re-analyze to refresh.`);
151
157
  }
152
158
  if (lowCoverage) {
153
- notes.push(`Low coverage: only ${filesCovered}/${filesTotal} project files (${Math.round(coverage * 100)}%) are in the graph — whole subsystems may be invisible below.`);
159
+ if (graph.persistCoverage?.partial) {
160
+ const p = graph.persistCoverage;
161
+ const totalFiles = p.totalFiles ?? filesCovered;
162
+ const persistedFiles = p.persistedFiles ?? filesCovered;
163
+ if (p.sourceFilesTruncated) {
164
+ notes.push(`Partial source walk: at least ${totalFiles} source files were represented before the visited-entry budget stopped enumeration; ${persistedFiles} files are in this graph.`);
165
+ }
166
+ else {
167
+ notes.push(`Partial persisted graph: ${p.persistedNodes}/${p.totalNodes} nodes, ${p.persistedEdges}/${p.totalEdges} edges, and ${persistedFiles}/${totalFiles} files were retained under the ${p.cap}-element cap — whole subsystems may be invisible below.`);
168
+ }
169
+ }
170
+ else {
171
+ notes.push(`Low coverage: only ${filesCovered}/${filesTotal} project files (${Math.round(coverage * 100)}%) are in the graph — whole subsystems may be invisible below.`);
172
+ }
154
173
  }
155
174
  return {
156
175
  graphBuiltAt: graph.builtAt,
@@ -166,6 +185,9 @@ function computeTrust(graph, cwd) {
166
185
  },
167
186
  stale,
168
187
  lowCoverage,
188
+ persistCoverage: graph.persistCoverage?.partial
189
+ ? graph.persistCoverage
190
+ : undefined,
169
191
  notes,
170
192
  };
171
193
  }
@@ -473,10 +495,8 @@ export function _resetProjectReportBuildGuardForTests() {
473
495
  }
474
496
  function triggerBackgroundGraphBuild(cwd) {
475
497
  const key = normalizeMapKey(path.resolve(cwd));
476
- if (isAtOrAboveHomeDir(key))
477
- return;
478
498
  if (inFlightGraphBuilds.has(key))
479
- return;
499
+ return false;
480
500
  inFlightGraphBuilds.add(key);
481
501
  void (async () => {
482
502
  try {
@@ -485,12 +505,13 @@ function triggerBackgroundGraphBuild(cwd) {
485
505
  await buildOrUpdateGraph(key, [], new FactStore());
486
506
  }
487
507
  catch {
488
- // Best-effort warmth, not a request the caller is waiting on.
508
+ // buildOrUpdateGraph records the durable failure and surfaced status.
489
509
  }
490
510
  finally {
491
511
  inFlightGraphBuilds.delete(key);
492
512
  }
493
513
  })();
514
+ return true;
494
515
  }
495
516
  // --- entry point ---------------------------------------------------------------
496
517
  /**
@@ -502,7 +523,7 @@ export async function projectReport(cwd, options) {
502
523
  const limit = clampLimit(options?.limit);
503
524
  const focusTerms = normalizeFocus(options?.focus);
504
525
  const view = options?.view;
505
- const { getCachedReviewGraph, getReviewGraphSizeSkipVerdict } = await import("./review-graph/builder.js");
526
+ const { getCachedReviewGraph, getReviewGraphSizeSkipVerdict, getLastReviewGraphBuildAttempt, } = await import("./review-graph/builder.js");
506
527
  let graph;
507
528
  try {
508
529
  graph = getCachedReviewGraph(cwd);
@@ -523,19 +544,29 @@ export async function projectReport(cwd, options) {
523
544
  sizeSkip = undefined;
524
545
  }
525
546
  if (sizeSkip) {
547
+ const lastBuildAttempt = getLastReviewGraphBuildAttempt(cwd);
526
548
  return {
527
549
  available: false,
528
- hint: `review graph disabled: project has ${sizeSkip.sourceFileCount} files, ` +
529
- `cap is ${sizeSkip.maxFileCount} — raise maxProjectFiles in .pi-lens.json ` +
530
- "or set PI_LENS_REVIEW_GRAPH_MAX_FILES",
550
+ hint: `review graph disabled: project has more than ${sizeSkip.maxFileCount} files ` +
551
+ `(cap ${sizeSkip.maxFileCount}) — raise maxProjectFiles in .pi-lens.json ` +
552
+ "or set PI_LENS_REVIEW_GRAPH_MAX_FILES; for CI/cron, run " +
553
+ "npx pi-lens build-graph after configuring the cap",
554
+ ...(lastBuildAttempt ? { lastBuildAttempt } : {}),
531
555
  ...(view ? { view } : {}),
532
556
  };
533
557
  }
534
- triggerBackgroundGraphBuild(cwd);
558
+ const previousAttempt = getLastReviewGraphBuildAttempt(cwd);
559
+ const kickedOff = triggerBackgroundGraphBuild(cwd);
560
+ const lastBuildAttempt = previousAttempt ?? getLastReviewGraphBuildAttempt(cwd);
535
561
  return {
536
562
  available: false,
537
- hint: "No review graph cached for this workspace yet — a build was kicked " +
538
- "off in the background; retry this call shortly.",
563
+ hint: lastBuildAttempt?.outcome === "failed" ||
564
+ lastBuildAttempt?.outcome === "skipped"
565
+ ? `Review graph unavailable: ${lastBuildAttempt.reason ?? lastBuildAttempt.outcome}. A retry was ${kickedOff ? "started" : "not started"}.`
566
+ : kickedOff
567
+ ? "No review graph cached for this workspace yet — a build was kicked off in the background; retry this call shortly."
568
+ : "No review graph cached for this workspace yet — the background build is still running; retry this call shortly.",
569
+ ...(lastBuildAttempt ? { lastBuildAttempt } : {}),
539
570
  ...(view ? { view } : {}),
540
571
  };
541
572
  }
@@ -546,8 +577,10 @@ export async function projectReport(cwd, options) {
546
577
  const subsystems = computeSubsystems(graph, cwd, limit);
547
578
  const riskHotspots = computeRiskHotspots(graph, degrees, cwd, limit, focusTerms);
548
579
  const deadWeight = computeDeadWeight(graph, degrees, entryPointFiles, cwd, limit);
580
+ const lastBuildAttempt = getLastReviewGraphBuildAttempt(cwd);
549
581
  return {
550
582
  available: true,
583
+ ...(lastBuildAttempt ? { lastBuildAttempt } : {}),
551
584
  ...(view ? { view } : {}),
552
585
  trust,
553
586
  hubs,
@@ -571,6 +604,12 @@ export function renderCompactProjectReport(report) {
571
604
  return `project_report — unavailable${report.hint ? `: ${report.hint}` : ""}`;
572
605
  }
573
606
  const lines = [];
607
+ // #919: an available graph whose persist failed serves this process fine
608
+ // but leaves the NEXT session cold — compact view must say so, not only
609
+ // the JSON view.
610
+ if (report.lastBuildAttempt?.reason) {
611
+ lines.push(`! build: ${report.lastBuildAttempt.reason}`);
612
+ }
574
613
  const t = report.trust;
575
614
  if (t) {
576
615
  lines.push(`TRUST: built ${t.graphBuiltAt} — ${t.filesCovered}/${t.filesTotal} files (${fmtPct(t.coverage)} coverage)` +
@@ -151,19 +151,17 @@ export function getProjectDiagnosticsScannerMaxFiles(cwd) {
151
151
  *
152
152
  * {@link REVIEW_GRAPH_HARD_CEILING} (5,000) is grounded FIRST by the
153
153
  * strongest real constraint on this number — `review-graph/builder.ts`'s
154
- * persist circuit-breaker (`GRAPH_PERSIST_MAX_ELEMENTS_DEFAULT`, 200,000
154
+ * persist circuit-breaker (`GRAPH_PERSIST_MAX_ELEMENTS_DEFAULT`, 500,000
155
155
  * graph elements: file/symbol nodes + cross-file edges). Above that element
156
- * count the graph is NEVER persisted to disk (logged + skipped, #260's OOM
157
- * guard) — not "slow", but PERMANENTLY uncached, so every session pays a
158
- * full cold build instead of the ~20 ms cached path (see the CHANGELOG
159
- * timing note below) engaging exactly once. For symbol-dense repos, element
156
+ * count only a centrality-ranked, honestly-marked partial graph is persisted
157
+ * (#936). Read-only orientation can use it, but the build tier rejects it as
158
+ * a complete incremental base. For symbol-dense repos, element
160
159
  * count runs on the order of ~30/file (file node + per-symbol nodes +
161
160
  * import/call/reference edges), so a hard ceiling must leave real headroom
162
- * below 200,000 elements, not creep up to it: 5,000 files x ~30 ⇒ ~150,000
163
- * elements, a ~25% margin. The earlier 6,000-file candidate (~180,000
164
- * elements) left too little margin — a slightly denser-than-average repo at
165
- * exactly the ceiling could tip over and silently lose persistence forever,
166
- * the opposite of this issue's "no silent caps" goal. 5,000 is also still
161
+ * below 500,000 elements. The file ceiling remains conservative because it
162
+ * also bounds cold-build latency; the persist cap has independent measured
163
+ * headroom through a roughly 420,000-element / 10,000-file graph (#936).
164
+ * 5,000 is also still
167
165
  * coherent with the jscpd/wordIndex 3x-ratio precedent (6,000 at the
168
166
  * default base) as "the ceiling sits just below that established top
169
167
  * tier" rather than an exact match, so it isn't a fresh unrelated
@@ -0,0 +1,6 @@
1
+ import { serveGzipStageWorker, } from "./gzip-stage-write.js";
2
+ serveGzipStageWorker((request) => ({
3
+ id: request.id,
4
+ generation: request.generation,
5
+ stagePath: request.stagePath,
6
+ }));