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
@@ -4,12 +4,17 @@
4
4
  * Builds a bidirectional function-level call graph by resolving symbol
5
5
  * references across files. Provides BFS impact analysis with severity tiers.
6
6
  * Uses the Symbol/SymbolRef data produced by TreeSitterSymbolExtractor and
7
- * persists the result with per-file mtime tracking for incremental sessions.
7
+ * persists the result under the canonical review-graph identity; freshness is
8
+ * owned exclusively by the review-graph snapshot.
8
9
  */
9
10
  import * as fs from "node:fs";
10
11
  import * as path from "node:path";
11
12
  import { getProjectDataDir } from "./file-utils.js";
12
- const CACHE_VERSION = 3;
13
+ import { writeFileAtomic } from "./atomic-write.js";
14
+ import { parseSymbolKey as parseCanonicalSymbolKey } from "./review-graph/symbol-id.js";
15
+ import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
16
+ /** Persisted call-graph cache format version. Bump on breaking format changes. */
17
+ export const CALL_GRAPH_CACHE_VERSION = 5;
13
18
  function severityForDepth(depth) {
14
19
  if (depth === 1)
15
20
  return "WillBreak";
@@ -65,6 +70,19 @@ export function impact(graph, startKey, maxDepth = 3, minWeight = 0.1) {
65
70
  // Sort by depth then symbolKey for stable output
66
71
  return results.sort((a, b) => a.depth - b.depth || a.symbolKey.localeCompare(b.symbolKey));
67
72
  }
73
+ /**
74
+ * Splits a `SymbolKey` into its file and symbol-name parts. Two shapes exist
75
+ * (see `buildCallGraph` above): the normal `"filePath:symbolName"` form, and
76
+ * the file-level fallback `"file:filePath"` used when no enclosing symbol was
77
+ * found for a call site (module-level code). Splitting at the LAST colon
78
+ * (rather than a naive `split(":")`) means a Windows drive letter in
79
+ * `filePath` (`C:\...`) is never mistaken for the name/file separator; the
80
+ * `"file:"` fallback is matched as a literal prefix first so it isn't
81
+ * misparsed by the same last-colon rule.
82
+ */
83
+ export function parseSymbolKey(key, knownFilePath) {
84
+ return parseCanonicalSymbolKey(key, knownFilePath);
85
+ }
68
86
  /**
69
87
  * Format an impact result set as a compact human-readable summary.
70
88
  * Example: "handleToolResult (WillBreak) → handleAgentEnd (MayBreak) → 3 Review callers"
@@ -77,12 +95,9 @@ export function formatImpact(results, projectRoot) {
77
95
  const review = results.filter((r) => r.severity === "Review");
78
96
  const parts = [];
79
97
  const label = (r) => {
80
- const name = r.symbolKey.includes(":")
81
- ? r.symbolKey.split(":").pop() ?? r.symbolKey
82
- : r.symbolKey;
83
- const file = r.symbolKey.includes(":")
84
- ? r.symbolKey.split(":").slice(0, -1).join(":").replace(projectRoot, "").replace(/^[/\\]/, "")
85
- : "";
98
+ const parsed = parseSymbolKey(r.symbolKey);
99
+ const name = parsed.symbolName ?? r.symbolKey;
100
+ const file = toProjectRelativePath(parsed.filePath, projectRoot);
86
101
  return file ? `${name} (${file})` : name;
87
102
  };
88
103
  if (willBreak.length > 0) {
@@ -113,15 +128,15 @@ const STDLIB_NAMES = new Set([
113
128
  "open", "isinstance", "issubclass", "type", "super", "hasattr", "getattr",
114
129
  "setattr", "enumerate", "zip", "map", "filter", "sorted", "reversed",
115
130
  // Go
116
- "fmt", "log", "os", "io", "err", "make", "append", "len", "cap", "copy",
131
+ "fmt", "log", "os", "io", "err", "make", "append", "cap", "copy",
117
132
  "close", "delete", "panic", "recover", "new",
118
133
  // Rust
119
- "println", "eprintln", "print", "eprint", "vec", "Some", "None", "Ok", "Err",
120
- "Box", "Rc", "Arc", "String", "Vec", "HashMap", "HashSet", "format",
134
+ "eprintln", "eprint", "vec", "Some", "None", "Ok", "Err",
135
+ "Box", "Rc", "Arc", "Vec", "HashMap", "HashSet", "format",
121
136
  // Java/Kotlin
122
- "System", "println", "toString", "equals", "hashCode", "Objects",
137
+ "System", "toString", "equals", "hashCode", "Objects",
123
138
  // Generic
124
- "new", "this", "self", "super", "nil", "null", "undefined", "true", "false",
139
+ "this", "self", "nil", "null", "undefined", "true", "false",
125
140
  ]);
126
141
  // ── Core resolution ────────────────────────────────────────────────────────────
127
142
  /**
@@ -130,29 +145,34 @@ const STDLIB_NAMES = new Set([
130
145
  * discounts edges when many files define the same name.
131
146
  */
132
147
  function buildDefIndex(allSymbols) {
133
- const index = new Map();
148
+ const byName = new Map();
149
+ const byId = new Map();
134
150
  for (const [, symbols] of allSymbols) {
135
151
  for (const sym of symbols) {
136
- if (!sym.name)
152
+ if (!sym.name || !sym.id)
137
153
  continue;
138
- const key = `${sym.filePath}:${sym.name}`;
139
- const existing = index.get(sym.name) ?? [];
154
+ const key = sym.id;
155
+ byId.set(key, sym);
156
+ const existing = byName.get(sym.name) ?? [];
140
157
  if (!existing.includes(key))
141
158
  existing.push(key);
142
- index.set(sym.name, existing);
159
+ byName.set(sym.name, existing);
143
160
  }
144
161
  }
145
- return index;
162
+ return { byName, byId };
146
163
  }
147
164
  /**
148
- * Find the enclosing function/method for a ref at `refLine` using a
149
- * "last start-line before ref" heuristic. Returns the symbol whose
150
- * start line is closest to (and not after) the ref's line.
165
+ * Find the enclosing function/method for a ref at `refLine`. A symbol with
166
+ * an end line is eligible only while the reference is inside its inclusive
167
+ * source range; symbols without an end line retain the conservative start-line
168
+ * fallback. Returns undefined when no symbol encloses the reference so callers
169
+ * can use the file-level module key.
151
170
  */
152
171
  function findEnclosingSymbol(symbols, refLine) {
153
172
  let best;
154
173
  for (const sym of symbols) {
155
174
  if (sym.line <= refLine &&
175
+ (sym.endLine === undefined || refLine <= sym.endLine) &&
156
176
  (sym.kind === "function" || sym.kind === "method")) {
157
177
  if (!best || sym.line > best.line)
158
178
  best = sym;
@@ -161,74 +181,224 @@ function findEnclosingSymbol(symbols, refLine) {
161
181
  return best;
162
182
  }
163
183
  /**
164
- * Build the function-level call graph from extracted symbols and refs.
184
+ * Build the function-level call graph from normalized review-graph evidence.
165
185
  *
166
- * Two passes:
167
- * 1. Index all defs by name across all files.
168
- * 2. For each ref, resolve to cross-file defs; find enclosing caller.
186
+ * The adapter supplies canonical target ids for graph evidence. Such evidence
187
+ * is accepted only when it points at a concrete symbol and has a non-ambiguous
188
+ * resolution. The old name-only path remains for legacy SymbolRef callers, but
189
+ * it is deliberately not used by the review-graph adapter (#1070).
169
190
  */
170
- export function buildCallGraph(allSymbols, allRefs) {
171
- const defIndex = buildDefIndex(allSymbols);
191
+ export function buildCallGraph(
192
+ /** Keys are normalizeMapKey-canonical file paths; Symbol.filePath values retain their display spelling. */
193
+ allSymbols,
194
+ /** Keys are normalizeMapKey-canonical caller file paths; SymbolRef.filePath retains its display spelling. */
195
+ allRefs, inputCoverage) {
196
+ // Keep the lookup index canonical even for legacy direct callers that still
197
+ // construct these maps from display paths; persisted/review-graph callers
198
+ // already satisfy the canonical-key contract documented above. A caller that
199
+ // supplies TWO raw spellings of the same file gets last-writer-wins here —
200
+ // raw compatibility is best-effort tolerance, not a merge contract.
201
+ const canonicalSymbols = new Map();
202
+ for (const [filePath, symbols] of allSymbols)
203
+ canonicalSymbols.set(normalizeMapKey(filePath), symbols);
204
+ const defIndex = buildDefIndex(canonicalSymbols);
172
205
  const callees = new Map();
173
206
  const callers = new Map();
174
207
  const inDegree = new Map();
175
- const edges = [];
208
+ const edgeByPair = new Map();
176
209
  let unresolvedRefs = 0;
177
210
  let totalRefs = 0;
211
+ const hasInputCoverage = inputCoverage !== undefined;
212
+ const coverage = inputCoverage
213
+ ? { ...inputCoverage }
214
+ : {
215
+ totalEvidence: 0,
216
+ callsEvidence: 0,
217
+ referencesEvidence: 0,
218
+ eligibleEvidence: 0,
219
+ resolvedEvidence: 0,
220
+ unresolvedEvidence: 0,
221
+ typeOnlyEvidence: 0,
222
+ unsupportedEvidence: 0,
223
+ sameFileEvidence: 0,
224
+ duplicateEvidence: 0,
225
+ complete: false,
226
+ };
178
227
  for (const [callerFile, refs] of allRefs) {
179
- const callerSymbols = allSymbols.get(callerFile) ?? [];
228
+ const callerFileKey = normalizeMapKey(callerFile);
229
+ const callerSymbols = canonicalSymbols.get(callerFileKey) ?? [];
180
230
  for (const ref of refs) {
181
231
  totalRefs++;
182
- // ref.symbolId is "filePath:name" from the extractor; we only need the name.
183
- const refName = ref.symbolId.split(":").pop() ?? ref.symbolId;
184
- if (STDLIB_NAMES.has(refName) || !refName)
232
+ if (!hasInputCoverage) {
233
+ // Legacy callers do not carry an evidenceKind, but each ref is still
234
+ // raw call-graph evidence and must contribute to coverage totals.
235
+ coverage.totalEvidence++;
236
+ if (ref.evidenceKind === "references" || ref.referenceKind === "type")
237
+ coverage.referencesEvidence++;
238
+ else
239
+ coverage.callsEvidence++;
240
+ }
241
+ if (ref.referenceKind === "type") {
242
+ if (!hasInputCoverage)
243
+ coverage.typeOnlyEvidence++;
185
244
  continue;
186
- const defs = defIndex.get(refName);
187
- if (!defs || defs.length === 0) {
188
- unresolvedRefs++;
245
+ }
246
+ if (ref.evidenceKind && ref.referenceKind !== "call") {
247
+ if (!hasInputCoverage)
248
+ coverage.unsupportedEvidence++;
189
249
  continue;
190
250
  }
191
- // Only cross-file refs are interesting for the call graph.
192
- const crossFileDefs = defs.filter((d) => !d.startsWith(`${callerFile}:`));
193
- if (crossFileDefs.length === 0)
251
+ const candidates = [];
252
+ if (ref.targetId) {
253
+ // Canonical graph identity is authoritative. Never recover a name by
254
+ // splitting an id: symbol ids contain kind and line components.
255
+ const callee = defIndex.byId.get(ref.targetId);
256
+ if (!callee || ref.resolution === "name-only" || ref.resolution === "unresolved") {
257
+ if (!hasInputCoverage) {
258
+ unresolvedRefs++;
259
+ coverage.unresolvedEvidence++;
260
+ }
261
+ continue;
262
+ }
263
+ candidates.push({
264
+ callee,
265
+ resolution: ref.resolution ?? "exact",
266
+ candidateCount: 1,
267
+ });
268
+ }
269
+ else {
270
+ // Compatibility for pre-#1070 callers that supplied only name refs.
271
+ // There is no typed/import evidence to select one definition, so retain
272
+ // every cross-file candidate and discount each edge. Picking [0] made
273
+ // graph iteration order decide which caller was reported.
274
+ const refName = ref.symbolName ?? parseSymbolKey(ref.symbolId, callerFile).symbolName ?? "";
275
+ if (STDLIB_NAMES.has(refName) || !refName) {
276
+ if (!hasInputCoverage)
277
+ coverage.unsupportedEvidence++;
278
+ continue;
279
+ }
280
+ const defs = defIndex.byName.get(refName);
281
+ if (!defs || defs.length === 0) {
282
+ if (!hasInputCoverage) {
283
+ unresolvedRefs++;
284
+ coverage.unresolvedEvidence++;
285
+ }
286
+ continue;
287
+ }
288
+ const crossFileDefs = defs
289
+ .map((id) => defIndex.byId.get(id))
290
+ .filter((candidate) => candidate !== undefined && normalizeMapKey(candidate.filePath) !== callerFileKey);
291
+ if (crossFileDefs.length === 0) {
292
+ if (!hasInputCoverage)
293
+ coverage.unsupportedEvidence++;
294
+ continue;
295
+ }
296
+ const resolution = crossFileDefs.length === 1 ? "exact" : "name-only";
297
+ for (const callee of crossFileDefs) {
298
+ candidates.push({ callee, resolution, candidateCount: crossFileDefs.length });
299
+ }
300
+ }
301
+ if (ref.evidenceKind && candidates.some(({ resolution }) => resolution === "name-only")) {
302
+ if (!hasInputCoverage) {
303
+ unresolvedRefs++;
304
+ coverage.unresolvedEvidence++;
305
+ }
194
306
  continue;
195
- const weight = 1.0 / crossFileDefs.length;
196
- // Enclosing function is the caller; fall back to file-level key.
197
- const enclosing = findEnclosingSymbol(callerSymbols, ref.line);
198
- const callerKey = enclosing
199
- ? `${callerFile}:${enclosing.name}`
200
- : `file:${callerFile}`;
201
- for (const calleeKey of crossFileDefs) {
202
- const calleeFile = calleeKey.split(":").slice(0, -1).join(":");
203
- const calleeSymbol = calleeKey.split(":").pop() ?? calleeKey;
204
- // Bidirectional maps (deduplicated by Set).
205
- const callerCallees = callees.get(callerKey) ?? new Set();
206
- callerCallees.add(calleeKey);
207
- callees.set(callerKey, callerCallees);
208
- const calleeCallers = callers.get(calleeKey) ?? new Set();
209
- calleeCallers.add(callerKey);
210
- callers.set(calleeKey, calleeCallers);
211
- // Weighted in-degree accumulation.
212
- inDegree.set(calleeKey, (inDegree.get(calleeKey) ?? 0) + weight);
213
- edges.push({
307
+ }
308
+ const caller = ref.callerSymbolId
309
+ ? defIndex.byId.get(ref.callerSymbolId)
310
+ : undefined;
311
+ const enclosing = caller ?? findEnclosingSymbol(callerSymbols, ref.line);
312
+ const callerKey = enclosing?.id ?? `file:${callerFile}`;
313
+ let producedEdge = false;
314
+ let duplicateRef = false;
315
+ for (const { callee, resolution, candidateCount } of candidates) {
316
+ if (normalizeMapKey(callee.filePath) === callerFileKey) {
317
+ // This projection is intentionally cross-file-only. A canonical
318
+ // same-file target is resolved by the source graph, but is represented
319
+ // by an explicit coverage category rather than as unsupported evidence.
320
+ // That keeps the projection contract while allowing complete coverage
321
+ // to gate user-facing cross-file impact honestly.
322
+ coverage.sameFileEvidence++;
323
+ if (hasInputCoverage) {
324
+ coverage.eligibleEvidence--;
325
+ coverage.resolvedEvidence--;
326
+ }
327
+ continue;
328
+ }
329
+ producedEdge = true;
330
+ const pairKey = `${callerKey}\u0000${callee.id}`;
331
+ const existing = edgeByPair.get(pairKey);
332
+ if (existing) {
333
+ existing.evidenceCount = (existing.evidenceCount ?? 1) + 1;
334
+ existing.evidenceKind = existing.evidenceKind === ref.evidenceKind
335
+ ? existing.evidenceKind
336
+ : "mixed";
337
+ // Duplicate evidence is a property of the logical pair, not of
338
+ // whether the producer happened to tag the record. Keep the edge,
339
+ // coverage, and centrality views consistent for legacy refs too.
340
+ duplicateRef = true;
341
+ continue;
342
+ }
343
+ const weight = ref.evidenceKind ? 1 : 1 / Math.max(1, candidateCount);
344
+ const edge = {
214
345
  callerFile,
215
346
  callerSymbol: enclosing?.name,
216
347
  callerKey,
217
- calleeFile,
218
- calleeSymbol,
219
- calleeKey,
348
+ callerKind: enclosing?.kind,
349
+ callerLine: enclosing?.line,
350
+ callerColumn: enclosing?.column,
351
+ calleeFile: callee.filePath,
352
+ calleeSymbol: callee.name,
353
+ calleeKey: callee.id,
354
+ calleeKind: callee.kind,
355
+ calleeLine: callee.line,
356
+ calleeColumn: callee.column,
357
+ evidenceKind: ref.evidenceKind,
358
+ resolution,
359
+ evidenceCount: 1,
220
360
  weight,
221
- });
361
+ };
362
+ edgeByPair.set(pairKey, edge);
363
+ const callerCallees = callees.get(callerKey) ?? new Set();
364
+ callerCallees.add(callee.id);
365
+ callees.set(callerKey, callerCallees);
366
+ const calleeCallers = callers.get(callee.id) ?? new Set();
367
+ calleeCallers.add(callerKey);
368
+ callers.set(callee.id, calleeCallers);
369
+ inDegree.set(callee.id, (inDegree.get(callee.id) ?? 0) + weight);
370
+ }
371
+ if (!hasInputCoverage && producedEdge) {
372
+ // Legacy name-only ambiguity can fan one raw record out to several
373
+ // weighted edges. Coverage remains a count of raw records, so account
374
+ // for eligibility and duplicates once per reference, not once per
375
+ // candidate edge.
376
+ coverage.eligibleEvidence++;
377
+ coverage.resolvedEvidence++;
378
+ if (duplicateRef)
379
+ coverage.duplicateEvidence++;
222
380
  }
381
+ if (hasInputCoverage && producedEdge && duplicateRef)
382
+ coverage.duplicateEvidence++;
223
383
  }
224
384
  }
385
+ // With adapter-supplied coverage, totalRefs is the raw evidence count. Some
386
+ // raw records (for example an external node with no caller file) cannot be
387
+ // represented in allRefs, but they must not disappear from accounting.
388
+ if (hasInputCoverage)
389
+ totalRefs = coverage.totalEvidence;
390
+ const languagesKnownComplete = Object.values(coverage.languages ?? {}).every((status) => status === "complete");
391
+ coverage.complete = coverage.complete && hasInputCoverage &&
392
+ coverage.unsupportedEvidence === 0 && languagesKnownComplete &&
393
+ (allSymbols.size > 0 || coverage.totalEvidence > 0);
225
394
  return {
226
395
  callees,
227
396
  callers,
228
397
  inDegree,
229
- edges,
230
- unresolvedRefs,
398
+ edges: [...edgeByPair.values()],
399
+ unresolvedRefs: hasInputCoverage ? coverage.unresolvedEvidence : unresolvedRefs,
231
400
  totalRefs,
401
+ coverage,
232
402
  builtAt: new Date().toISOString(),
233
403
  };
234
404
  }
@@ -240,39 +410,252 @@ function metaFilePath(cwd) {
240
410
  return path.join(getProjectDataDir(cwd), "cache", "call-graph.meta.json");
241
411
  }
242
412
  /**
243
- * Persist the call graph to disk with per-file mtime tracking.
244
- * On the next session-start, stale files can be identified without a full rebuild.
413
+ * Persist a call-graph projection. Current callers pass the canonical
414
+ * review-graph identity. There is deliberately no independent file/mtime
415
+ * freshness argument: callers must use the review graph's canonical identity.
245
416
  */
246
- export function saveCallGraph(cwd, graph, fileMtimes) {
417
+ export function saveCallGraph(cwd, graph, identity) {
247
418
  const cacheFile = cacheFilePath(cwd);
248
419
  const metaFile = metaFilePath(cwd);
249
420
  try {
250
421
  fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
251
422
  const persisted = {
252
- version: CACHE_VERSION,
423
+ version: CALL_GRAPH_CACHE_VERSION,
253
424
  builtAt: graph.builtAt,
254
- fileMtimes: Object.fromEntries(fileMtimes),
425
+ reviewGraphVersion: identity.reviewGraphVersion,
426
+ reviewGraphSignature: identity.reviewGraphSignature,
255
427
  edges: graph.edges,
256
428
  callees: [...graph.callees.entries()].map(([k, v]) => [k, [...v]]),
257
429
  callers: [...graph.callers.entries()].map(([k, v]) => [k, [...v]]),
258
430
  inDegree: [...graph.inDegree.entries()],
431
+ totalRefs: graph.totalRefs,
432
+ unresolvedRefs: graph.unresolvedRefs,
433
+ coverage: graph.coverage,
259
434
  };
260
- fs.writeFileSync(cacheFile, JSON.stringify(persisted), "utf-8");
261
- fs.writeFileSync(metaFile, JSON.stringify({ savedAt: new Date().toISOString(), edgeCount: graph.edges.length }), "utf-8");
435
+ writeFileAtomic(cacheFile, JSON.stringify(persisted));
436
+ writeFileAtomic(metaFile, JSON.stringify({ savedAt: new Date().toISOString(), edgeCount: graph.edges.length }));
262
437
  }
263
438
  catch {
264
439
  // Non-fatal — next session rebuilds from scratch.
265
440
  }
266
441
  }
442
+ function inferLegacyCoverage(edges) {
443
+ let weightedTotalEvidence = 0;
444
+ let weightedReferencesEvidence = 0;
445
+ let weightedDuplicateEvidence = 0;
446
+ for (const edge of edges) {
447
+ const count = Number.isFinite(edge.evidenceCount) && (edge.evidenceCount ?? 0) > 0
448
+ ? Math.floor(edge.evidenceCount)
449
+ : 1;
450
+ const weight = Number.isFinite(edge.weight) && edge.weight >= 0 ? edge.weight : 1;
451
+ weightedTotalEvidence += count * weight;
452
+ weightedDuplicateEvidence += Math.max(0, count - 1) * weight;
453
+ if (edge.evidenceKind === "references")
454
+ weightedReferencesEvidence += count * weight;
455
+ }
456
+ const totalEvidence = Math.max(0, Math.round(weightedTotalEvidence));
457
+ const referencesEvidence = Math.max(0, Math.round(weightedReferencesEvidence));
458
+ const duplicateEvidence = Math.max(0, Math.round(weightedDuplicateEvidence));
459
+ return {
460
+ totalEvidence,
461
+ callsEvidence: totalEvidence - referencesEvidence,
462
+ referencesEvidence,
463
+ eligibleEvidence: totalEvidence,
464
+ resolvedEvidence: totalEvidence,
465
+ unresolvedEvidence: 0,
466
+ typeOnlyEvidence: 0,
467
+ unsupportedEvidence: 0,
468
+ sameFileEvidence: 0,
469
+ duplicateEvidence,
470
+ // A legacy graph can still be queried, but it cannot prove that the old
471
+ // extractor covered every source/reference kind.
472
+ complete: false,
473
+ };
474
+ }
475
+ function loadCoverage(rawCoverage, edges) {
476
+ const inferred = inferLegacyCoverage(edges);
477
+ return rawCoverage
478
+ ? {
479
+ ...inferred,
480
+ ...rawCoverage,
481
+ }
482
+ : inferred;
483
+ }
484
+ function finiteCount(value) {
485
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
486
+ }
487
+ function countsMatch(left, right) {
488
+ return Math.abs(left - right) <= 1e-9 * Math.max(1, Math.abs(left), Math.abs(right));
489
+ }
490
+ function validatePersistedCallGraph(raw, coverage) {
491
+ const nonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
492
+ const enumValue = (value, values) => typeof value === "string" && values.includes(value);
493
+ const edgeKinds = ["calls", "references", "mixed"];
494
+ const resolutions = ["exact", "import", "receiver-type", "name-only", "unresolved"];
495
+ if (!raw || raw.version !== CALL_GRAPH_CACHE_VERSION || !nonEmptyString(raw.builtAt))
496
+ return false;
497
+ if (!nonEmptyString(raw.reviewGraphVersion) || !nonEmptyString(raw.reviewGraphSignature))
498
+ return false;
499
+ if (raw.coverage !== undefined &&
500
+ (!raw.coverage || typeof raw.coverage !== "object" || Array.isArray(raw.coverage)))
501
+ return false;
502
+ if (!Array.isArray(raw.edges) || !Array.isArray(raw.callees) || !Array.isArray(raw.callers) || !Array.isArray(raw.inDegree))
503
+ return false;
504
+ const coverageValues = [
505
+ coverage.totalEvidence,
506
+ coverage.callsEvidence,
507
+ coverage.referencesEvidence,
508
+ coverage.eligibleEvidence,
509
+ coverage.resolvedEvidence,
510
+ coverage.unresolvedEvidence,
511
+ coverage.typeOnlyEvidence,
512
+ coverage.unsupportedEvidence,
513
+ coverage.sameFileEvidence,
514
+ coverage.duplicateEvidence,
515
+ ];
516
+ if (typeof coverage.complete !== "boolean" || coverageValues.some((value) => !finiteCount(value)))
517
+ return false;
518
+ if (coverage.languages !== undefined &&
519
+ (!coverage.languages || typeof coverage.languages !== "object" || Array.isArray(coverage.languages) ||
520
+ Object.entries(coverage.languages).some(([language, status]) => !nonEmptyString(language) || !enumValue(status, ["complete", "partial", "unavailable"]))))
521
+ return false;
522
+ if (coverage.callsEvidence + coverage.referencesEvidence !== coverage.totalEvidence)
523
+ return false;
524
+ if (coverage.resolvedEvidence > coverage.eligibleEvidence)
525
+ return false;
526
+ if (coverage.eligibleEvidence !== coverage.resolvedEvidence)
527
+ return false;
528
+ if (coverage.resolvedEvidence + coverage.unresolvedEvidence + coverage.typeOnlyEvidence + coverage.unsupportedEvidence + coverage.sameFileEvidence !== coverage.totalEvidence)
529
+ return false;
530
+ if (coverage.complete && coverage.unsupportedEvidence > 0)
531
+ return false;
532
+ if (coverage.complete && Object.values(coverage.languages ?? {}).some((status) => status !== "complete"))
533
+ return false;
534
+ const expectedCallees = new Map();
535
+ const expectedCallers = new Map();
536
+ const expectedInDegree = new Map();
537
+ const pairKeys = new Set();
538
+ let weightedEdgeEvidence = 0;
539
+ let weightedDuplicateEvidence = 0;
540
+ for (const edge of raw.edges) {
541
+ if (!edge || typeof edge !== "object" || !nonEmptyString(edge.callerKey) || !nonEmptyString(edge.calleeKey) ||
542
+ !nonEmptyString(edge.callerFile) || !nonEmptyString(edge.calleeFile) ||
543
+ !nonEmptyString(edge.calleeSymbol) || typeof edge.weight !== "number" || !Number.isFinite(edge.weight) || edge.weight <= 0 || edge.weight > 1)
544
+ return false;
545
+ if (edge.callerSymbol !== undefined && !nonEmptyString(edge.callerSymbol))
546
+ return false;
547
+ if (edge.callerKind !== undefined && !nonEmptyString(edge.callerKind))
548
+ return false;
549
+ if (edge.calleeKind !== undefined && !nonEmptyString(edge.calleeKind))
550
+ return false;
551
+ if (edge.evidenceKind !== undefined && !enumValue(edge.evidenceKind, edgeKinds))
552
+ return false;
553
+ if (edge.resolution !== undefined && !enumValue(edge.resolution, resolutions))
554
+ return false;
555
+ if (edge.evidenceCount !== undefined && (!finiteCount(edge.evidenceCount) || edge.evidenceCount < 1))
556
+ return false;
557
+ const evidenceCount = edge.evidenceCount ?? 1;
558
+ const callerIdentity = parseCanonicalSymbolKey(edge.callerKey, edge.callerFile);
559
+ const calleeIdentity = parseCanonicalSymbolKey(edge.calleeKey, edge.calleeFile);
560
+ if (normalizeMapKey(callerIdentity.filePath) !== normalizeMapKey(edge.callerFile) ||
561
+ normalizeMapKey(calleeIdentity.filePath) !== normalizeMapKey(edge.calleeFile) ||
562
+ (calleeIdentity.symbolName !== undefined && calleeIdentity.symbolName !== edge.calleeSymbol) ||
563
+ (callerIdentity.symbolName !== undefined && edge.callerSymbol !== undefined && callerIdentity.symbolName !== edge.callerSymbol))
564
+ return false;
565
+ const pair = `${edge.callerKey}\u0000${edge.calleeKey}`;
566
+ if (pairKeys.has(pair))
567
+ return false;
568
+ pairKeys.add(pair);
569
+ // Ambiguous legacy name-only evidence may fan one raw record out to
570
+ // multiple weighted edges. Weight the persisted evidence back to raw
571
+ // records so semantic validation agrees with coverage accounting.
572
+ weightedEdgeEvidence += evidenceCount * edge.weight;
573
+ weightedDuplicateEvidence += (evidenceCount - 1) * edge.weight;
574
+ const callees = expectedCallees.get(edge.callerKey) ?? new Set();
575
+ callees.add(edge.calleeKey);
576
+ expectedCallees.set(edge.callerKey, callees);
577
+ const callers = expectedCallers.get(edge.calleeKey) ?? new Set();
578
+ callers.add(edge.callerKey);
579
+ expectedCallers.set(edge.calleeKey, callers);
580
+ expectedInDegree.set(edge.calleeKey, (expectedInDegree.get(edge.calleeKey) ?? 0) + edge.weight);
581
+ }
582
+ if (!countsMatch(weightedEdgeEvidence, coverage.resolvedEvidence))
583
+ return false;
584
+ if (!countsMatch(weightedDuplicateEvidence, coverage.duplicateEvidence))
585
+ return false;
586
+ if (!countsMatch(coverage.totalEvidence, weightedEdgeEvidence + coverage.unresolvedEvidence + coverage.typeOnlyEvidence + coverage.unsupportedEvidence + coverage.sameFileEvidence))
587
+ return false;
588
+ if (raw.totalRefs !== undefined && !finiteCount(raw.totalRefs))
589
+ return false;
590
+ if (raw.unresolvedRefs !== undefined && !finiteCount(raw.unresolvedRefs))
591
+ return false;
592
+ if (raw.totalRefs !== undefined && raw.totalRefs !== coverage.totalEvidence)
593
+ return false;
594
+ if (raw.unresolvedRefs !== undefined && raw.unresolvedRefs !== coverage.unresolvedEvidence)
595
+ return false;
596
+ const readAdjacency = (entries) => {
597
+ const result = new Map();
598
+ if (!Array.isArray(entries))
599
+ return undefined;
600
+ for (const entry of entries) {
601
+ if (!Array.isArray(entry) || !nonEmptyString(entry[0]) || !Array.isArray(entry[1]) || entry[1].some((key) => !nonEmptyString(key)))
602
+ return undefined;
603
+ const keys = entry[1];
604
+ if (new Set(keys).size !== keys.length || result.has(entry[0]))
605
+ return undefined;
606
+ result.set(entry[0], new Set(keys));
607
+ }
608
+ return result;
609
+ };
610
+ const actualCallees = readAdjacency(raw.callees);
611
+ const actualCallers = readAdjacency(raw.callers);
612
+ if (!actualCallees || !actualCallers || actualCallees.size !== expectedCallees.size || actualCallers.size !== expectedCallers.size)
613
+ return false;
614
+ const sameSets = (actual, expected) => {
615
+ for (const [key, values] of expected) {
616
+ const got = actual.get(key);
617
+ if (!got || got.size !== values.size || [...values].some((value) => !got.has(value)))
618
+ return false;
619
+ }
620
+ return true;
621
+ };
622
+ if (!sameSets(actualCallees, expectedCallees) || !sameSets(actualCallers, expectedCallers))
623
+ return false;
624
+ const actualInDegree = new Map();
625
+ for (const entry of raw.inDegree) {
626
+ if (!Array.isArray(entry) || !nonEmptyString(entry[0]) || typeof entry[1] !== "number" || !Number.isFinite(entry[1]) || entry[1] < 0 || actualInDegree.has(entry[0]))
627
+ return false;
628
+ actualInDegree.set(entry[0], entry[1]);
629
+ }
630
+ if (actualInDegree.size !== expectedInDegree.size)
631
+ return false;
632
+ for (const [key, value] of expectedInDegree) {
633
+ if (actualInDegree.get(key) !== value)
634
+ return false;
635
+ }
636
+ return true;
637
+ }
267
638
  /**
268
639
  * Load the persisted call graph from disk.
269
640
  * Returns undefined if the cache is missing, version-mismatched, or corrupt.
270
641
  */
271
- export function loadCallGraph(cwd) {
642
+ export function loadCallGraph(cwd, expectedIdentity) {
272
643
  const cacheFile = cacheFilePath(cwd);
273
644
  try {
274
645
  const raw = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
275
- if (raw.version !== CACHE_VERSION)
646
+ if (raw.version !== CALL_GRAPH_CACHE_VERSION)
647
+ return undefined;
648
+ if (typeof raw.reviewGraphVersion !== "string" || typeof raw.reviewGraphSignature !== "string")
649
+ return undefined;
650
+ const identity = {
651
+ reviewGraphVersion: raw.reviewGraphVersion,
652
+ reviewGraphSignature: raw.reviewGraphSignature,
653
+ };
654
+ if (expectedIdentity && (identity.reviewGraphVersion !== expectedIdentity.reviewGraphVersion ||
655
+ identity.reviewGraphSignature !== expectedIdentity.reviewGraphSignature))
656
+ return undefined;
657
+ const coverage = loadCoverage(raw.coverage, raw.edges);
658
+ if (!validatePersistedCallGraph(raw, coverage))
276
659
  return undefined;
277
660
  return {
278
661
  graph: {
@@ -280,46 +663,15 @@ export function loadCallGraph(cwd) {
280
663
  callers: new Map(raw.callers.map(([k, v]) => [k, new Set(v)])),
281
664
  inDegree: new Map(raw.inDegree),
282
665
  edges: raw.edges,
283
- unresolvedRefs: 0,
284
- totalRefs: 0,
666
+ unresolvedRefs: raw.unresolvedRefs ?? coverage.unresolvedEvidence,
667
+ totalRefs: raw.totalRefs ?? coverage.totalEvidence,
668
+ coverage,
285
669
  builtAt: raw.builtAt,
286
670
  },
287
- fileMtimes: new Map(Object.entries(raw.fileMtimes)),
671
+ identity,
288
672
  };
289
673
  }
290
674
  catch {
291
675
  return undefined;
292
676
  }
293
677
  }
294
- /**
295
- * Returns the set of file paths whose mtime has changed since the cache was saved.
296
- * Files not in the mtime map are treated as new (stale).
297
- */
298
- export function staleFiles(fileMtimes, currentFiles) {
299
- return currentFiles.filter((f) => {
300
- const cached = fileMtimes.get(f);
301
- if (cached === undefined)
302
- return true; // new file
303
- try {
304
- return fs.statSync(f).mtimeMs !== cached;
305
- }
306
- catch {
307
- return true; // deleted or unreadable
308
- }
309
- });
310
- }
311
- /**
312
- * Read current mtimes for a set of files.
313
- */
314
- export function readMtimes(files) {
315
- const mtimes = new Map();
316
- for (const f of files) {
317
- try {
318
- mtimes.set(f, fs.statSync(f).mtimeMs);
319
- }
320
- catch {
321
- // skip
322
- }
323
- }
324
- return mtimes;
325
- }