@herbertgao/pi-extensions 2026.8.4 → 2026.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  26. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  36. package/node_modules/@narumitw/pi-btw/README.md +32 -24
  37. package/node_modules/@narumitw/pi-btw/package.json +6 -6
  38. package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
  39. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
  40. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  41. package/node_modules/pi-lens/CHANGELOG.md +2535 -954
  42. package/node_modules/pi-lens/README.md +13 -0
  43. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  44. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
  45. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  46. package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
  47. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  48. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  49. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  50. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  51. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  52. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  53. package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
  54. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  55. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  56. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  57. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  58. package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
  59. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  60. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  61. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  62. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  63. package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
  64. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  65. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  66. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  67. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
  69. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  70. package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
  71. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  72. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  73. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  74. package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
  75. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
  76. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
  77. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
  78. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  79. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  80. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  81. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  82. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  83. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
  84. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  87. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  114. package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
  115. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  116. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  117. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  118. package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
  119. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  120. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  121. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  122. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  123. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  124. package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
  125. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  126. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  127. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  128. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  129. package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
  130. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  131. package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  134. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  135. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  136. package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
  137. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  138. package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
  139. package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
  140. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  141. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  142. package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
  143. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  144. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  145. package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
  146. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
  147. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  148. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
  149. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  150. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  151. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
  152. package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
  153. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  154. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
  155. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  156. package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
  157. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  158. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
  161. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
  162. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  163. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
  164. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  165. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  167. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
  168. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  171. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  172. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  173. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  174. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  175. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  177. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  178. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  179. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  180. package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
  181. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  182. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  183. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  184. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
  185. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  186. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  187. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  188. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  189. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  190. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  191. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  192. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  193. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  194. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  195. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  196. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  197. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  198. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  199. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  201. package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
  202. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  203. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  204. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  205. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  206. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  207. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  208. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
  209. package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
  210. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
  211. package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
  212. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  213. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
  214. package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
  215. package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
  216. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  217. package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
  218. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  219. package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
  220. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  221. package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
  222. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  223. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  224. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  225. package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
  226. package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
  227. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  228. package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
  229. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  230. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  231. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  232. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  233. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  234. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  237. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  238. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  239. package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
  240. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  241. package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
  242. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  243. package/node_modules/pi-lens/dist/index.js +51785 -36118
  244. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  245. package/node_modules/pi-lens/dist/mcp/server.js +219 -46
  246. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  247. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  248. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  249. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  250. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
  251. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
  252. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  253. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  254. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  255. package/node_modules/pi-lens/docs/agent-guide.md +40 -15
  256. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  257. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  258. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  259. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
  260. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  261. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  262. package/node_modules/pi-lens/docs/fable.md +27 -0
  263. package/node_modules/pi-lens/docs/features.md +45 -3
  264. package/node_modules/pi-lens/docs/globalconfig.md +56 -1
  265. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  266. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  267. package/node_modules/pi-lens/docs/mcp.md +37 -4
  268. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  269. package/node_modules/pi-lens/docs/settings.md +18 -1
  270. package/node_modules/pi-lens/docs/tools.md +4 -1
  271. package/node_modules/pi-lens/docs/usage.md +41 -5
  272. package/node_modules/pi-lens/docs/word-index.md +35 -0
  273. package/node_modules/pi-lens/package.json +10 -10
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  300. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  301. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  302. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
  303. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
  304. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  305. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
  306. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
  307. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  308. package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
  309. package/node_modules/pi-mcp-adapter/README.md +38 -4
  310. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  311. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  312. package/node_modules/pi-mcp-adapter/cli.js +25 -4
  313. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  314. package/node_modules/pi-mcp-adapter/config.ts +4 -4
  315. package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
  316. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  317. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  318. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  319. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  320. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  321. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  322. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  323. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  324. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  325. package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
  326. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  327. package/node_modules/pi-mcp-adapter/package.json +4 -3
  328. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  329. package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
  330. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  331. package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
  332. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  333. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  334. package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
  335. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  336. package/node_modules/pi-mcp-adapter/types.ts +176 -27
  337. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  338. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  339. package/node_modules/pi-web-access/README.md +18 -12
  340. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  341. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  342. package/node_modules/pi-web-access/curator-page.ts +5 -3
  343. package/node_modules/pi-web-access/curator-server.ts +2 -1
  344. package/node_modules/pi-web-access/extract.ts +106 -34
  345. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  346. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  347. package/node_modules/pi-web-access/gemini-search.ts +18 -4
  348. package/node_modules/pi-web-access/index.ts +120 -48
  349. package/node_modules/pi-web-access/package.json +2 -2
  350. package/node_modules/pi-web-access/summary-review.ts +11 -5
  351. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  352. package/package.json +12 -13
  353. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  354. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  355. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  356. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  357. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  358. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  359. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  360. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -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";
@@ -75,15 +80,8 @@ export function impact(graph, startKey, maxDepth = 3, minWeight = 0.1) {
75
80
  * `"file:"` fallback is matched as a literal prefix first so it isn't
76
81
  * misparsed by the same last-colon rule.
77
82
  */
78
- export function parseSymbolKey(key) {
79
- if (key.startsWith("file:")) {
80
- return { filePath: key.slice("file:".length) };
81
- }
82
- const idx = key.lastIndexOf(":");
83
- if (idx === -1)
84
- return { filePath: key };
85
- const symbolName = key.slice(idx + 1);
86
- return { filePath: key.slice(0, idx), symbolName: symbolName || undefined };
83
+ export function parseSymbolKey(key, knownFilePath) {
84
+ return parseCanonicalSymbolKey(key, knownFilePath);
87
85
  }
88
86
  /**
89
87
  * Format an impact result set as a compact human-readable summary.
@@ -97,12 +95,9 @@ export function formatImpact(results, projectRoot) {
97
95
  const review = results.filter((r) => r.severity === "Review");
98
96
  const parts = [];
99
97
  const label = (r) => {
100
- const name = r.symbolKey.includes(":")
101
- ? r.symbolKey.split(":").pop() ?? r.symbolKey
102
- : r.symbolKey;
103
- const file = r.symbolKey.includes(":")
104
- ? r.symbolKey.split(":").slice(0, -1).join(":").replace(projectRoot, "").replace(/^[/\\]/, "")
105
- : "";
98
+ const parsed = parseSymbolKey(r.symbolKey);
99
+ const name = parsed.symbolName ?? r.symbolKey;
100
+ const file = toProjectRelativePath(parsed.filePath, projectRoot);
106
101
  return file ? `${name} (${file})` : name;
107
102
  };
108
103
  if (willBreak.length > 0) {
@@ -133,15 +128,15 @@ const STDLIB_NAMES = new Set([
133
128
  "open", "isinstance", "issubclass", "type", "super", "hasattr", "getattr",
134
129
  "setattr", "enumerate", "zip", "map", "filter", "sorted", "reversed",
135
130
  // Go
136
- "fmt", "log", "os", "io", "err", "make", "append", "len", "cap", "copy",
131
+ "fmt", "log", "os", "io", "err", "make", "append", "cap", "copy",
137
132
  "close", "delete", "panic", "recover", "new",
138
133
  // Rust
139
- "println", "eprintln", "print", "eprint", "vec", "Some", "None", "Ok", "Err",
140
- "Box", "Rc", "Arc", "String", "Vec", "HashMap", "HashSet", "format",
134
+ "eprintln", "eprint", "vec", "Some", "None", "Ok", "Err",
135
+ "Box", "Rc", "Arc", "Vec", "HashMap", "HashSet", "format",
141
136
  // Java/Kotlin
142
- "System", "println", "toString", "equals", "hashCode", "Objects",
137
+ "System", "toString", "equals", "hashCode", "Objects",
143
138
  // Generic
144
- "new", "this", "self", "super", "nil", "null", "undefined", "true", "false",
139
+ "this", "self", "nil", "null", "undefined", "true", "false",
145
140
  ]);
146
141
  // ── Core resolution ────────────────────────────────────────────────────────────
147
142
  /**
@@ -150,29 +145,34 @@ const STDLIB_NAMES = new Set([
150
145
  * discounts edges when many files define the same name.
151
146
  */
152
147
  function buildDefIndex(allSymbols) {
153
- const index = new Map();
148
+ const byName = new Map();
149
+ const byId = new Map();
154
150
  for (const [, symbols] of allSymbols) {
155
151
  for (const sym of symbols) {
156
- if (!sym.name)
152
+ if (!sym.name || !sym.id)
157
153
  continue;
158
- const key = `${sym.filePath}:${sym.name}`;
159
- const existing = index.get(sym.name) ?? [];
154
+ const key = sym.id;
155
+ byId.set(key, sym);
156
+ const existing = byName.get(sym.name) ?? [];
160
157
  if (!existing.includes(key))
161
158
  existing.push(key);
162
- index.set(sym.name, existing);
159
+ byName.set(sym.name, existing);
163
160
  }
164
161
  }
165
- return index;
162
+ return { byName, byId };
166
163
  }
167
164
  /**
168
- * Find the enclosing function/method for a ref at `refLine` using a
169
- * "last start-line before ref" heuristic. Returns the symbol whose
170
- * 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.
171
170
  */
172
171
  function findEnclosingSymbol(symbols, refLine) {
173
172
  let best;
174
173
  for (const sym of symbols) {
175
174
  if (sym.line <= refLine &&
175
+ (sym.endLine === undefined || refLine <= sym.endLine) &&
176
176
  (sym.kind === "function" || sym.kind === "method")) {
177
177
  if (!best || sym.line > best.line)
178
178
  best = sym;
@@ -181,74 +181,224 @@ function findEnclosingSymbol(symbols, refLine) {
181
181
  return best;
182
182
  }
183
183
  /**
184
- * Build the function-level call graph from extracted symbols and refs.
184
+ * Build the function-level call graph from normalized review-graph evidence.
185
185
  *
186
- * Two passes:
187
- * 1. Index all defs by name across all files.
188
- * 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).
189
190
  */
190
- export function buildCallGraph(allSymbols, allRefs) {
191
- 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);
192
205
  const callees = new Map();
193
206
  const callers = new Map();
194
207
  const inDegree = new Map();
195
- const edges = [];
208
+ const edgeByPair = new Map();
196
209
  let unresolvedRefs = 0;
197
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
+ };
198
227
  for (const [callerFile, refs] of allRefs) {
199
- const callerSymbols = allSymbols.get(callerFile) ?? [];
228
+ const callerFileKey = normalizeMapKey(callerFile);
229
+ const callerSymbols = canonicalSymbols.get(callerFileKey) ?? [];
200
230
  for (const ref of refs) {
201
231
  totalRefs++;
202
- // ref.symbolId is "filePath:name" from the extractor; we only need the name.
203
- const refName = ref.symbolId.split(":").pop() ?? ref.symbolId;
204
- 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++;
205
244
  continue;
206
- const defs = defIndex.get(refName);
207
- if (!defs || defs.length === 0) {
208
- unresolvedRefs++;
245
+ }
246
+ if (ref.evidenceKind && ref.referenceKind !== "call") {
247
+ if (!hasInputCoverage)
248
+ coverage.unsupportedEvidence++;
209
249
  continue;
210
250
  }
211
- // Only cross-file refs are interesting for the call graph.
212
- const crossFileDefs = defs.filter((d) => !d.startsWith(`${callerFile}:`));
213
- 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
+ }
214
306
  continue;
215
- const weight = 1.0 / crossFileDefs.length;
216
- // Enclosing function is the caller; fall back to file-level key.
217
- const enclosing = findEnclosingSymbol(callerSymbols, ref.line);
218
- const callerKey = enclosing
219
- ? `${callerFile}:${enclosing.name}`
220
- : `file:${callerFile}`;
221
- for (const calleeKey of crossFileDefs) {
222
- const calleeFile = calleeKey.split(":").slice(0, -1).join(":");
223
- const calleeSymbol = calleeKey.split(":").pop() ?? calleeKey;
224
- // Bidirectional maps (deduplicated by Set).
225
- const callerCallees = callees.get(callerKey) ?? new Set();
226
- callerCallees.add(calleeKey);
227
- callees.set(callerKey, callerCallees);
228
- const calleeCallers = callers.get(calleeKey) ?? new Set();
229
- calleeCallers.add(callerKey);
230
- callers.set(calleeKey, calleeCallers);
231
- // Weighted in-degree accumulation.
232
- inDegree.set(calleeKey, (inDegree.get(calleeKey) ?? 0) + weight);
233
- 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 = {
234
345
  callerFile,
235
346
  callerSymbol: enclosing?.name,
236
347
  callerKey,
237
- calleeFile,
238
- calleeSymbol,
239
- 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,
240
360
  weight,
241
- });
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);
242
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++;
380
+ }
381
+ if (hasInputCoverage && producedEdge && duplicateRef)
382
+ coverage.duplicateEvidence++;
243
383
  }
244
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);
245
394
  return {
246
395
  callees,
247
396
  callers,
248
397
  inDegree,
249
- edges,
250
- unresolvedRefs,
398
+ edges: [...edgeByPair.values()],
399
+ unresolvedRefs: hasInputCoverage ? coverage.unresolvedEvidence : unresolvedRefs,
251
400
  totalRefs,
401
+ coverage,
252
402
  builtAt: new Date().toISOString(),
253
403
  };
254
404
  }
@@ -260,39 +410,252 @@ function metaFilePath(cwd) {
260
410
  return path.join(getProjectDataDir(cwd), "cache", "call-graph.meta.json");
261
411
  }
262
412
  /**
263
- * Persist the call graph to disk with per-file mtime tracking.
264
- * 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.
265
416
  */
266
- export function saveCallGraph(cwd, graph, fileMtimes) {
417
+ export function saveCallGraph(cwd, graph, identity) {
267
418
  const cacheFile = cacheFilePath(cwd);
268
419
  const metaFile = metaFilePath(cwd);
269
420
  try {
270
421
  fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
271
422
  const persisted = {
272
- version: CACHE_VERSION,
423
+ version: CALL_GRAPH_CACHE_VERSION,
273
424
  builtAt: graph.builtAt,
274
- fileMtimes: Object.fromEntries(fileMtimes),
425
+ reviewGraphVersion: identity.reviewGraphVersion,
426
+ reviewGraphSignature: identity.reviewGraphSignature,
275
427
  edges: graph.edges,
276
428
  callees: [...graph.callees.entries()].map(([k, v]) => [k, [...v]]),
277
429
  callers: [...graph.callers.entries()].map(([k, v]) => [k, [...v]]),
278
430
  inDegree: [...graph.inDegree.entries()],
431
+ totalRefs: graph.totalRefs,
432
+ unresolvedRefs: graph.unresolvedRefs,
433
+ coverage: graph.coverage,
279
434
  };
280
- fs.writeFileSync(cacheFile, JSON.stringify(persisted), "utf-8");
281
- 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 }));
282
437
  }
283
438
  catch {
284
439
  // Non-fatal — next session rebuilds from scratch.
285
440
  }
286
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
+ }
287
638
  /**
288
639
  * Load the persisted call graph from disk.
289
640
  * Returns undefined if the cache is missing, version-mismatched, or corrupt.
290
641
  */
291
- export function loadCallGraph(cwd) {
642
+ export function loadCallGraph(cwd, expectedIdentity) {
292
643
  const cacheFile = cacheFilePath(cwd);
293
644
  try {
294
645
  const raw = JSON.parse(fs.readFileSync(cacheFile, "utf-8"));
295
- 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))
296
659
  return undefined;
297
660
  return {
298
661
  graph: {
@@ -300,46 +663,15 @@ export function loadCallGraph(cwd) {
300
663
  callers: new Map(raw.callers.map(([k, v]) => [k, new Set(v)])),
301
664
  inDegree: new Map(raw.inDegree),
302
665
  edges: raw.edges,
303
- unresolvedRefs: 0,
304
- totalRefs: 0,
666
+ unresolvedRefs: raw.unresolvedRefs ?? coverage.unresolvedEvidence,
667
+ totalRefs: raw.totalRefs ?? coverage.totalEvidence,
668
+ coverage,
305
669
  builtAt: raw.builtAt,
306
670
  },
307
- fileMtimes: new Map(Object.entries(raw.fileMtimes)),
671
+ identity,
308
672
  };
309
673
  }
310
674
  catch {
311
675
  return undefined;
312
676
  }
313
677
  }
314
- /**
315
- * Returns the set of file paths whose mtime has changed since the cache was saved.
316
- * Files not in the mtime map are treated as new (stale).
317
- */
318
- export function staleFiles(fileMtimes, currentFiles) {
319
- return currentFiles.filter((f) => {
320
- const cached = fileMtimes.get(f);
321
- if (cached === undefined)
322
- return true; // new file
323
- try {
324
- return fs.statSync(f).mtimeMs !== cached;
325
- }
326
- catch {
327
- return true; // deleted or unreadable
328
- }
329
- });
330
- }
331
- /**
332
- * Read current mtimes for a set of files.
333
- */
334
- export function readMtimes(files) {
335
- const mtimes = new Map();
336
- for (const f of files) {
337
- try {
338
- mtimes.set(f, fs.statSync(f).mtimeMs);
339
- }
340
- catch {
341
- // skip
342
- }
343
- }
344
- return mtimes;
345
- }