@herbertgao/pi-extensions 2026.10.0 → 2026.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +52 -242
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
- package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
- package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1064 -0
- package/node_modules/pi-lens/README.md +115 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
- package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
- package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
- package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
- package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
- package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
- package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
- package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
- package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
- package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
- package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
- package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
- package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
- package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
- package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
- package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
- package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
- package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
- package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
- package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
- package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
- package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
- package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
- package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
- package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
- package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
- package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
- package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
- package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
- package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
- package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
- package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
- package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
- package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
- package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
- package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
- package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
- package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
- package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
- package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
- package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
- package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
- package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
- package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
- package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
- package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
- package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
- package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
- package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
- package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
- package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
- package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
- package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
- package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
- package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
- package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
- package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
- package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
- package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
- package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
- package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
- package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
- package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
- package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
- package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
- package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
- package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
- package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
- package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
- package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
- package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
- package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
- package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
- package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
- package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
- package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
- package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
- package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
- package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
- package/node_modules/pi-lens/dist/index.js +16456 -8335
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
- package/node_modules/pi-lens/docs/configuration.md +20 -1
- package/node_modules/pi-lens/docs/environment-variables.md +52 -2
- package/node_modules/pi-lens/docs/features.md +8 -4
- package/node_modules/pi-lens/docs/globalconfig.md +1 -0
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/node_modules/pi-typesafe/README.md +1 -1
- package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
- package/node_modules/pi-typesafe/dist/backends.js +18 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
- package/node_modules/pi-typesafe/dist/client.js +22 -7
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
- package/node_modules/pi-typesafe/dist/usage.js +147 -42
- package/node_modules/pi-typesafe/package.json +1 -1
- package/node_modules/pi-web-access/CHANGELOG.md +48 -0
- package/node_modules/pi-web-access/README.md +57 -11
- package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
- package/node_modules/pi-web-access/curator-page.ts +5 -1
- package/node_modules/pi-web-access/curator-server.ts +2 -0
- package/node_modules/pi-web-access/declared-web-links.ts +2 -2
- package/node_modules/pi-web-access/degoog.ts +323 -0
- package/node_modules/pi-web-access/dist/index.js +3483 -2745
- package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
- package/node_modules/pi-web-access/exa.ts +5 -1
- package/node_modules/pi-web-access/extract.ts +41 -3
- package/node_modules/pi-web-access/firecrawl.ts +35 -4
- package/node_modules/pi-web-access/gemini-search.ts +34 -11
- package/node_modules/pi-web-access/gemini-web.ts +14 -9
- package/node_modules/pi-web-access/index.ts +153 -1202
- package/node_modules/pi-web-access/keenable.ts +221 -0
- package/node_modules/pi-web-access/mcp-cli.ts +40 -0
- package/node_modules/pi-web-access/mcp-server.ts +81 -0
- package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
- package/node_modules/pi-web-access/openai-search.ts +34 -1
- package/node_modules/pi-web-access/package.json +23 -5
- package/node_modules/pi-web-access/perplexity.ts +64 -56
- package/node_modules/pi-web-access/tool-arguments.ts +20 -0
- package/node_modules/pi-web-access/video-extract.ts +1 -1
- package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
- package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
- package/node_modules/pi-web-access/youtube-extract.ts +2 -5
- package/package.json +17 -12
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
|
@@ -1,2377 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dispatch integration helpers
|
|
3
|
-
*
|
|
4
|
-
* Provides utilities for integrating the declarative dispatch system
|
|
5
|
-
* with the existing index.ts tool_result handler.
|
|
6
|
-
*/
|
|
7
|
-
import { CASCADE_DIAGNOSTICS_TTL_MS } from "../cascade-types.js";
|
|
8
|
-
import { getDiagnosticLogger } from "../diagnostic-logger.js";
|
|
9
|
-
import { detectFileKind } from "../file-kinds.js";
|
|
10
|
-
import { getLspCapableKinds, getPrimaryDispatchGroup, } from "../language-policy.js";
|
|
11
|
-
import { formatSlopScoreSummary, } from "../session-summary.js";
|
|
12
|
-
import { clearCoverageNoticeState, clearLatencyReports, createDispatchContext, dispatchForFile, formatLatencyReport, getLatencyReports, RunnerRegistry, } from "./dispatcher.js";
|
|
13
|
-
import { recordDegradationOnce } from "../degradation-ledger.js";
|
|
14
|
-
import { FactStore, setFactStoreEvictionReporter } from "./fact-store.js";
|
|
15
|
-
import { TOOL_PLANS } from "./plan.js";
|
|
16
|
-
// Re-export latency tracking types and functions
|
|
17
|
-
export { clearLatencyReports, formatLatencyReport, getLatencyReports };
|
|
18
|
-
import * as nodeFs from "node:fs";
|
|
19
|
-
import * as nodePath from "node:path";
|
|
20
|
-
import { fileURLToPath } from "node:url";
|
|
21
|
-
import { CASCADE_NEIGHBOUR_BUDGET, deriveCascadeNeighbourBudget, } from "../cascade-budget.js";
|
|
22
|
-
import { applyCascadeDisplayPolicy, formatCascadeNeighborDiagnostics, recordCascadeFindingPolicy, } from "../cascade-format.js";
|
|
23
|
-
import { logCascade } from "../cascade-logger.js";
|
|
24
|
-
import { getDiagnosticTracker } from "../diagnostic-tracker.js";
|
|
25
|
-
import { classifyCascadeWaitTier, isTierAwareCascadeEnabled, recordOutstandingCascadeTouch, } from "../lsp/cascade-tier.js";
|
|
26
|
-
import { bindingStateLabel, touchCoverageGap, } from "../lsp/diagnostic-binding.js";
|
|
27
|
-
import { getServersForFileWithConfig } from "../lsp/config.js";
|
|
28
|
-
import { getLSPService } from "../lsp/index.js";
|
|
29
|
-
import { isExternalOrVendorFile, normalizeMapKey } from "../path-utils.js";
|
|
30
|
-
import { getProjectIgnoreMatcher } from "../file-utils.js";
|
|
31
|
-
import { resetAstGrepNapiLoadState, resetAstGrepUnsupportedLanguageLog, } from "./runners/ast-grep-napi.js";
|
|
32
|
-
import { resetTreeSitterClientLoadState } from "../tree-sitter-shared.js";
|
|
33
|
-
import { isTestRoleCollateral } from "../collateral-test-role.js";
|
|
34
|
-
import { clearReviewGraphWorkspaceCache, getGraphBuildInfoForGraph, getGraphImportChanges, graphBuildInfoIsTrustworthy, } from "../review-graph/builder.js";
|
|
35
|
-
import { buildReverseDependencyIndexFromGraph, getAffectedFilesFromIndex, patchReverseDependencyIndex, writeReverseDependencyIndexToSnapshot, } from "../reverse-deps.js";
|
|
36
|
-
import { buildOrUpdateGraph, computeImpactCascade, computeTransitiveImpact, formatImpactCascade, } from "../review-graph/service.js";
|
|
37
|
-
import { clearModuleGraphCache } from "../review-graph/workspace-modules.js";
|
|
38
|
-
import { releaseWorkspaceTopologyIdleTimers } from "../workspace-topology.js";
|
|
39
|
-
import { RUNTIME_CONFIG } from "../runtime-config.js";
|
|
40
|
-
import { findCompiledClassesDir, hasJavaBuildDescriptor, } from "../tool-policy.js";
|
|
41
|
-
import { removeWordIndexDocumentAsync, updateWordIndexDocumentForEdit, WORD_INDEX_MAX_BYTES, } from "../word-index.js";
|
|
42
|
-
import { reconcileCascadeNeighborLspErrors } from "../widget-state.js";
|
|
43
|
-
import { findAuxiliaryProfileForSource } from "./auxiliary-lsp.js";
|
|
44
|
-
// Register fact providers. All register eagerly here (the dispatch entry) — the
|
|
45
|
-
// tree-sitter-backed providers included, since the parsing stack loads
|
|
46
|
-
// `web-tree-sitter` lazily inside client.init(), not at module import, so it
|
|
47
|
-
// stays out of the eager graph and degrades there rather than crashing at load.
|
|
48
|
-
import { registerProvider, runProviders } from "./fact-runner.js";
|
|
49
|
-
import { commentFactProvider } from "./facts/comment-facts.js";
|
|
50
|
-
import { fileContentProvider } from "./facts/file-content.js";
|
|
51
|
-
import { functionFactProvider } from "./facts/function-facts.js";
|
|
52
|
-
import { importFactProvider } from "./facts/import-facts.js";
|
|
53
|
-
import { tryCatchFactProvider } from "./facts/try-catch-facts.js";
|
|
54
|
-
import { resolveRunnerPath, toRunnerDisplayPath } from "./runner-context.js";
|
|
55
|
-
import { registerDefaultRunners } from "./runners/index.js";
|
|
56
|
-
import { convertLspDiagnostics } from "./utils/lsp-diagnostics.js";
|
|
57
|
-
registerProvider(fileContentProvider);
|
|
58
|
-
registerProvider(tryCatchFactProvider);
|
|
59
|
-
registerProvider(functionFactProvider);
|
|
60
|
-
registerProvider(commentFactProvider);
|
|
61
|
-
registerProvider(importFactProvider);
|
|
62
|
-
// Register fact rules
|
|
63
|
-
import { registerRule } from "./fact-rule-runner.js";
|
|
64
|
-
import { asyncNoiseRule } from "./rules/async-noise.js";
|
|
65
|
-
import { asyncUnnecessaryWrapperRule } from "./rules/async-unnecessary-wrapper.js";
|
|
66
|
-
import { corsWildcardRule } from "./rules/cors-wildcard.js";
|
|
67
|
-
import { errorObscuringRule } from "./rules/error-obscuring.js";
|
|
68
|
-
import { errorSwallowingRule } from "./rules/error-swallowing.js";
|
|
69
|
-
import { highComplexityRule } from "./rules/high-complexity.js";
|
|
70
|
-
import { highFanOutRule } from "./rules/high-fan-out.js";
|
|
71
|
-
import { highImportCouplingRule } from "./rules/high-import-coupling.js";
|
|
72
|
-
import { missingErrorPropagationRule } from "./rules/missing-error-propagation.js";
|
|
73
|
-
import { commentedCredentialsRule } from "./rules/no-commented-credentials.js";
|
|
74
|
-
import { passThroughWrappersRule } from "./rules/pass-through-wrappers.js";
|
|
75
|
-
import { placeholderCommentsRule } from "./rules/placeholder-comments.js";
|
|
76
|
-
import { unsafeBoundaryRule } from "./rules/unsafe-boundary.js";
|
|
77
|
-
import { loadPiLensProjectConfig, } from "../project-lens-config.js";
|
|
78
|
-
registerRule(errorObscuringRule);
|
|
79
|
-
registerRule(errorSwallowingRule);
|
|
80
|
-
registerRule(asyncNoiseRule);
|
|
81
|
-
registerRule(passThroughWrappersRule);
|
|
82
|
-
registerRule(placeholderCommentsRule);
|
|
83
|
-
registerRule(highComplexityRule);
|
|
84
|
-
registerRule(unsafeBoundaryRule);
|
|
85
|
-
registerRule(asyncUnnecessaryWrapperRule);
|
|
86
|
-
registerRule(missingErrorPropagationRule);
|
|
87
|
-
registerRule(highFanOutRule);
|
|
88
|
-
registerRule(highImportCouplingRule);
|
|
89
|
-
registerRule(corsWildcardRule);
|
|
90
|
-
registerRule(commentedCredentialsRule);
|
|
91
|
-
/**
|
|
92
|
-
* Load a project's `.pi-lens.json` config.
|
|
93
|
-
*
|
|
94
|
-
* Rule thresholds are consumed from `DispatchContext.projectConfig` during rule
|
|
95
|
-
* evaluation, not applied to process-global module state. Keeping this helper as
|
|
96
|
-
* a thin loader preserves the existing integration seam while avoiding
|
|
97
|
-
* cross-workspace threshold bleed when multiple dispatches overlap.
|
|
98
|
-
*/
|
|
99
|
-
export function applyProjectLensConfig(cwd) {
|
|
100
|
-
return loadPiLensProjectConfig(cwd);
|
|
101
|
-
}
|
|
102
|
-
// "dispatch" labels this store's eviction telemetry (#2243 review round 3,
|
|
103
|
-
// F1) — five other production FactStore instances exist (mcp/analyze.ts,
|
|
104
|
-
// lens-map.ts, project-diagnostics/scanner.ts, runtime-session.ts's
|
|
105
|
-
// session-start call-graph task, mcp/cli.ts); a shared constant subject let
|
|
106
|
-
// runtime-session's session-start review-graph walk consume the once-per-
|
|
107
|
-
// session ledger slot before any dispatch ran, misattributing the record.
|
|
108
|
-
const sessionFacts = new FactStore("dispatch");
|
|
109
|
-
// #2243 item 4: wire the fact-store's capacity-eviction telemetry to the
|
|
110
|
-
// ledger from HERE, not from fact-store.ts. fact-store stays an import leaf so
|
|
111
|
-
// it cannot re-enter the safe-spawn ↔ degradation-ledger cycle (see
|
|
112
|
-
// `setFactStoreEvictionReporter`). The reporter forwards each store's own
|
|
113
|
-
// subject, so the ledger's per-kind+subject dedupe emits exactly ONE record
|
|
114
|
-
// per session PER STORE, re-arming when the ledger resets at session_start.
|
|
115
|
-
//
|
|
116
|
-
// #2247 review F1: the axis was previously dropped here, so
|
|
117
|
-
// `recordDegradationOnce`'s `${kind}\0${subject}` dedupe collapsed a store's
|
|
118
|
-
// count-axis and byte-axis evictions onto the SAME key — whichever axis
|
|
119
|
-
// tripped first (always count, since 1024 typical files land far under 64
|
|
120
|
-
// MiB) silently consumed the once-per-session slot and a later byte-axis
|
|
121
|
-
// eviction on that same store recorded nothing. Fold the axis into the
|
|
122
|
-
// subject (`<store>:<axis>`), matching this ledger's own `<store>:<file>`
|
|
123
|
-
// convention (see `demoted-finding-retired`'s doc comment) rather than
|
|
124
|
-
// growing the `kind` union per axis. `pinned-over-budget` (#2247 review F2)
|
|
125
|
-
// is a distinct condition, not an eviction, so it gets its own kind instead
|
|
126
|
-
// of sharing the eviction kind's subject convention.
|
|
127
|
-
setFactStoreEvictionReporter((subject, axis, reason) => {
|
|
128
|
-
if (axis === "pinned-over-budget") {
|
|
129
|
-
recordDegradationOnce({
|
|
130
|
-
kind: "fact-store-pinned-over-budget",
|
|
131
|
-
subject,
|
|
132
|
-
reason,
|
|
133
|
-
});
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
recordDegradationOnce({
|
|
137
|
-
kind: "fact-store-capacity-eviction",
|
|
138
|
-
subject: `${subject}:${axis}`,
|
|
139
|
-
reason,
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
const sessionRunnerRegistry = new RunnerRegistry();
|
|
143
|
-
registerDefaultRunners(sessionRunnerRegistry);
|
|
144
|
-
const LSP_CAPABLE_KINDS = new Set(getLspCapableKinds());
|
|
145
|
-
const FACT_RULE_IDS = new Set([
|
|
146
|
-
"error-obscuring",
|
|
147
|
-
"error-swallowing",
|
|
148
|
-
"async-noise",
|
|
149
|
-
"pass-through-wrappers",
|
|
150
|
-
"placeholder-comments",
|
|
151
|
-
"high-complexity",
|
|
152
|
-
"unsafe-boundary",
|
|
153
|
-
"async-unnecessary-wrapper",
|
|
154
|
-
"missing-error-propagation",
|
|
155
|
-
"high-fan-out",
|
|
156
|
-
"commented-out-code",
|
|
157
|
-
"duplicate-string-literal",
|
|
158
|
-
"function-in-loop",
|
|
159
|
-
"cors-wildcard",
|
|
160
|
-
"dynamic-regexp",
|
|
161
|
-
"max-switch-cases",
|
|
162
|
-
"no-commented-credentials",
|
|
163
|
-
"no-boolean-params",
|
|
164
|
-
"high-import-coupling",
|
|
165
|
-
"no-complex-conditionals",
|
|
166
|
-
]);
|
|
167
|
-
const sessionSlopRuleCounts = new Map();
|
|
168
|
-
let sessionSlopDiagnosticCount = 0;
|
|
169
|
-
let sessionWrittenLineCount = 0;
|
|
170
|
-
// Debounced ast-grep warning scan — fires 2s after the last write to a jsts file.
|
|
171
|
-
// Runs warning-tier rules that are too expensive to include in the blocking write path,
|
|
172
|
-
// logs all diagnostics for history without surfacing anything to the agent.
|
|
173
|
-
const astGrepWarnDebounceTimers = new Map();
|
|
174
|
-
const AST_GREP_WARN_DEBOUNCE_MS = 2000;
|
|
175
|
-
function scheduleAstGrepWarningScan(filePath, cwd, pi, logContext) {
|
|
176
|
-
const existing = astGrepWarnDebounceTimers.get(filePath);
|
|
177
|
-
if (existing)
|
|
178
|
-
clearTimeout(existing);
|
|
179
|
-
const timer = setTimeout(() => {
|
|
180
|
-
astGrepWarnDebounceTimers.delete(filePath);
|
|
181
|
-
void runAstGrepWarningScan(filePath, cwd, pi, logContext).catch(() => {
|
|
182
|
-
// Non-critical background scan — swallow errors silently
|
|
183
|
-
});
|
|
184
|
-
}, AST_GREP_WARN_DEBOUNCE_MS);
|
|
185
|
-
astGrepWarnDebounceTimers.set(filePath, timer);
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Run the debounced ast-grep warning scan for one jsts file.
|
|
189
|
-
*
|
|
190
|
-
* #2243 item 1: this is a dispatch entry point like dispatchLint*, so it must
|
|
191
|
-
* follow the same discipline. It fires ~2 s after the last write, and in that
|
|
192
|
-
* window the review-graph project walk (`buildOrUpdateGraph`) seeds facts for
|
|
193
|
-
* every file into the SAME `sessionFacts`, which can evict this file's
|
|
194
|
-
* `file.content`. `dispatcher.ts` reads that fact back with `?? ""` for inline
|
|
195
|
-
* suppressions, so a stale or evicted read silently stops applying
|
|
196
|
-
* `pi-lens-ignore`. Two guards, matching the other dispatch callers:
|
|
197
|
-
* 1. `beginDispatchFor` pins the file for the dispatch (released in the
|
|
198
|
-
* finally), so a concurrent walk cannot evict it mid-dispatch. This
|
|
199
|
-
* does NOT clear the file's existing facts the way `clearFileFactsFor`
|
|
200
|
-
* does for the other dispatch callers (#2243 review round 3, F5): this
|
|
201
|
-
* scan fires ~2s after the SAME edit's inline dispatch already
|
|
202
|
-
* re-derived every fact for this file, so a second clear+re-derive
|
|
203
|
-
* here bought nothing but ~51ms of redundant re-parsing across all
|
|
204
|
-
* five providers (measured; a warm-cache read is ~1ms).
|
|
205
|
-
* 2. `runProviders` still re-derives `file.content` when it's actually
|
|
206
|
-
* missing (a genuine miss — evicted, or never computed), so the read
|
|
207
|
-
* never depends on staleness.
|
|
208
|
-
* `facts` is injected only for tests; production always uses `sessionFacts`.
|
|
209
|
-
*/
|
|
210
|
-
export async function runAstGrepWarningScan(filePath, cwd, pi, logContext, facts = sessionFacts) {
|
|
211
|
-
const ctx = createDispatchContext(filePath, cwd, pi, facts, false);
|
|
212
|
-
if (ctx.kind !== "jsts")
|
|
213
|
-
return;
|
|
214
|
-
facts.beginDispatchFor(ctx.filePath);
|
|
215
|
-
try {
|
|
216
|
-
// Single-runner group: ast-grep only, warning mode (blockingOnly=false)
|
|
217
|
-
const group = {
|
|
218
|
-
mode: "all",
|
|
219
|
-
runnerIds: ["ast-grep"],
|
|
220
|
-
filterKinds: ["jsts"],
|
|
221
|
-
};
|
|
222
|
-
await runProviders(ctx);
|
|
223
|
-
const result = await dispatchForFile(ctx, [group], sessionRunnerRegistry);
|
|
224
|
-
if (result.diagnostics.length === 0)
|
|
225
|
-
return;
|
|
226
|
-
const logger = getDiagnosticLogger();
|
|
227
|
-
for (const d of result.diagnostics) {
|
|
228
|
-
logger.logCaught(d, logContext, false);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
finally {
|
|
232
|
-
facts.endDispatchFor(ctx.filePath);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
function resetSessionSlopScore() {
|
|
236
|
-
sessionSlopRuleCounts.clear();
|
|
237
|
-
sessionSlopDiagnosticCount = 0;
|
|
238
|
-
sessionWrittenLineCount = 0;
|
|
239
|
-
}
|
|
240
|
-
function detectFactRuleId(diagnostic) {
|
|
241
|
-
if (diagnostic.rule && FACT_RULE_IDS.has(diagnostic.rule)) {
|
|
242
|
-
return diagnostic.rule;
|
|
243
|
-
}
|
|
244
|
-
if (diagnostic.tool && FACT_RULE_IDS.has(diagnostic.tool)) {
|
|
245
|
-
return diagnostic.tool;
|
|
246
|
-
}
|
|
247
|
-
if (diagnostic.id) {
|
|
248
|
-
const prefix = diagnostic.id.split(":", 1)[0];
|
|
249
|
-
if (FACT_RULE_IDS.has(prefix)) {
|
|
250
|
-
return prefix;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return undefined;
|
|
254
|
-
}
|
|
255
|
-
function trackSessionSlopStats(ctx, diagnostics) {
|
|
256
|
-
const lineCount = ctx.facts.getFileFact(ctx.filePath, "file.lineCount");
|
|
257
|
-
if (typeof lineCount === "number" &&
|
|
258
|
-
Number.isFinite(lineCount) &&
|
|
259
|
-
lineCount > 0) {
|
|
260
|
-
sessionWrittenLineCount += lineCount;
|
|
261
|
-
}
|
|
262
|
-
for (const diagnostic of diagnostics) {
|
|
263
|
-
const ruleId = detectFactRuleId(diagnostic);
|
|
264
|
-
if (!ruleId)
|
|
265
|
-
continue;
|
|
266
|
-
sessionSlopDiagnosticCount += 1;
|
|
267
|
-
sessionSlopRuleCounts.set(ruleId, (sessionSlopRuleCounts.get(ruleId) ?? 0) + 1);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
export function getDispatchSlopScoreSummary() {
|
|
271
|
-
if (sessionSlopDiagnosticCount === 0 || sessionWrittenLineCount <= 0) {
|
|
272
|
-
return undefined;
|
|
273
|
-
}
|
|
274
|
-
const totalKlocWritten = sessionWrittenLineCount / 1000;
|
|
275
|
-
const ruleCounts = [...sessionSlopRuleCounts.entries()]
|
|
276
|
-
.map(([ruleId, count]) => ({ ruleId, count }))
|
|
277
|
-
.sort((a, b) => b.count - a.count || a.ruleId.localeCompare(b.ruleId));
|
|
278
|
-
return {
|
|
279
|
-
totalRuleDiagnostics: sessionSlopDiagnosticCount,
|
|
280
|
-
totalKlocWritten,
|
|
281
|
-
scorePerKloc: sessionSlopDiagnosticCount / totalKlocWritten,
|
|
282
|
-
ruleCounts,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
export function getDispatchSlopScoreLine() {
|
|
286
|
-
const summary = getDispatchSlopScoreSummary();
|
|
287
|
-
if (!summary)
|
|
288
|
-
return "";
|
|
289
|
-
return formatSlopScoreSummary(summary);
|
|
290
|
-
}
|
|
291
|
-
// SpotBugs analyzes JVM bytecode, so it applies to java + kotlin. Opt-in
|
|
292
|
-
// (lens-spotbugs flag) and only when a Java build descriptor + compiled .class
|
|
293
|
-
// dir exist — the runner itself mtime-caches so it doesn't re-run per keystroke. #133
|
|
294
|
-
const SPOTBUGS_SUPPORTED_KINDS = new Set(["java", "kotlin"]);
|
|
295
|
-
function withSpotbugsGroup(kind, groups, ctx) {
|
|
296
|
-
if (!SPOTBUGS_SUPPORTED_KINDS.has(kind))
|
|
297
|
-
return groups;
|
|
298
|
-
if (!ctx.pi.getFlag("lens-spotbugs"))
|
|
299
|
-
return groups;
|
|
300
|
-
if (!hasJavaBuildDescriptor(ctx.cwd) || !findCompiledClassesDir(ctx.cwd)) {
|
|
301
|
-
return groups;
|
|
302
|
-
}
|
|
303
|
-
if (groups.some((group) => group.runnerIds.includes("spotbugs")))
|
|
304
|
-
return groups;
|
|
305
|
-
return [
|
|
306
|
-
...groups,
|
|
307
|
-
{
|
|
308
|
-
mode: "all",
|
|
309
|
-
runnerIds: ["spotbugs"],
|
|
310
|
-
filterKinds: [kind],
|
|
311
|
-
semantic: "warning",
|
|
312
|
-
},
|
|
313
|
-
];
|
|
314
|
-
}
|
|
315
|
-
function withPrimaryPolicyGroup(kind, groups, pi) {
|
|
316
|
-
const lspEnabled = !pi.getFlag("no-lsp");
|
|
317
|
-
const normalizedGroups = lspEnabled
|
|
318
|
-
? groups
|
|
319
|
-
: groups
|
|
320
|
-
.map((group) => {
|
|
321
|
-
const runnerIds = group.runnerIds.filter((id) => id !== "lsp");
|
|
322
|
-
if (runnerIds.length === 0)
|
|
323
|
-
return null;
|
|
324
|
-
return {
|
|
325
|
-
...group,
|
|
326
|
-
runnerIds,
|
|
327
|
-
};
|
|
328
|
-
})
|
|
329
|
-
.filter((group) => group !== null);
|
|
330
|
-
const primary = getPrimaryDispatchGroup(kind, lspEnabled);
|
|
331
|
-
if (!primary)
|
|
332
|
-
return normalizedGroups;
|
|
333
|
-
const alreadyHasPrimary = normalizedGroups.some((group) => {
|
|
334
|
-
if (group.mode !== primary.mode)
|
|
335
|
-
return false;
|
|
336
|
-
if (group.runnerIds.length !== primary.runnerIds.length)
|
|
337
|
-
return false;
|
|
338
|
-
return group.runnerIds.every((id, index) => primary.runnerIds[index] === id);
|
|
339
|
-
});
|
|
340
|
-
if (alreadyHasPrimary)
|
|
341
|
-
return normalizedGroups;
|
|
342
|
-
return [primary, ...normalizedGroups];
|
|
343
|
-
}
|
|
344
|
-
export function getDispatchGroupsForKind(kind, pi) {
|
|
345
|
-
const plan = TOOL_PLANS[kind];
|
|
346
|
-
if (!plan) {
|
|
347
|
-
const lspEnabled = !pi.getFlag("no-lsp");
|
|
348
|
-
const policyGroup = getPrimaryDispatchGroup(kind, lspEnabled);
|
|
349
|
-
if (policyGroup)
|
|
350
|
-
return [policyGroup];
|
|
351
|
-
if (lspEnabled && LSP_CAPABLE_KINDS.has(kind)) {
|
|
352
|
-
return [
|
|
353
|
-
{ mode: "all", runnerIds: ["lsp"], filterKinds: [kind] },
|
|
354
|
-
];
|
|
355
|
-
}
|
|
356
|
-
return [];
|
|
357
|
-
}
|
|
358
|
-
return withPrimaryPolicyGroup(kind, plan.groups, pi);
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Reset baselines — call on session_start so a new session
|
|
362
|
-
* starts with a clean slate.
|
|
363
|
-
*
|
|
364
|
-
* Pass `cwd` to also re-apply the project's `.pi-lens.json` rule thresholds
|
|
365
|
-
* (a no-op when the file is absent or unchanged, since the loader is
|
|
366
|
-
* mtime-cached). Optional for backward compatibility with tests that don't
|
|
367
|
-
* care about per-project thresholds.
|
|
368
|
-
*/
|
|
369
|
-
export function resetDispatchBaselines(cwd) {
|
|
370
|
-
if (cwd)
|
|
371
|
-
applyProjectLensConfig(cwd);
|
|
372
|
-
resetAstGrepUnsupportedLanguageLog();
|
|
373
|
-
// #1567: a genuine-failure verdict or a live transient cooldown on the
|
|
374
|
-
// napi load latch is session-scoped state, not process-lifetime state.
|
|
375
|
-
resetAstGrepNapiLoadState();
|
|
376
|
-
// #1592: same shape for the web-tree-sitter load latch — an evaluation-
|
|
377
|
-
// shaped rejection is session-scoped state, not process-lifetime state.
|
|
378
|
-
resetTreeSitterClientLoadState();
|
|
379
|
-
sessionFacts.clearAll();
|
|
380
|
-
resetSessionSlopScore();
|
|
381
|
-
clearCoverageNoticeState();
|
|
382
|
-
clearReviewGraphWorkspaceCache();
|
|
383
|
-
clearReverseDepsIndexCache();
|
|
384
|
-
clearModuleGraphCache();
|
|
385
|
-
recentlyCleanNeighborCache.clear();
|
|
386
|
-
primaryFilesThisTurn.clear();
|
|
387
|
-
cascadeSessionStats = {
|
|
388
|
-
runs: 0,
|
|
389
|
-
diagnosticsSurfaced: 0,
|
|
390
|
-
coldSnapshotTouches: 0,
|
|
391
|
-
};
|
|
392
|
-
for (const timer of astGrepWarnDebounceTimers.values())
|
|
393
|
-
clearTimeout(timer);
|
|
394
|
-
astGrepWarnDebounceTimers.clear();
|
|
395
|
-
}
|
|
396
|
-
let cascadeSessionStats = {
|
|
397
|
-
runs: 0,
|
|
398
|
-
diagnosticsSurfaced: 0,
|
|
399
|
-
coldSnapshotTouches: 0,
|
|
400
|
-
};
|
|
401
|
-
export function getCascadeSessionStats() {
|
|
402
|
-
return { ...cascadeSessionStats };
|
|
403
|
-
}
|
|
404
|
-
const recentlyCleanNeighborCache = new Map();
|
|
405
|
-
/** O(1) entry count of this module's turn-bounded cache (#1123 item 2
|
|
406
|
-
* memory attribution) — a `Map.size` read, never iterated.
|
|
407
|
-
* `sessionFactEntries` (#2282) is the dispatch `FactStore`'s own
|
|
408
|
-
* `sessionFacts` footprint: the fixed-vocabulary map (small, unbounded by
|
|
409
|
-
* design) plus the bounded per-file map (capped at `MAX_SESSION_FACT_RECORDS`
|
|
410
|
-
* — see `fact-store.ts`), so this number is now itself bounded rather than
|
|
411
|
-
* invisible growth. */
|
|
412
|
-
export function getDispatchCascadeCacheStats() {
|
|
413
|
-
return {
|
|
414
|
-
recentlyCleanNeighborCacheSize: recentlyCleanNeighborCache.size,
|
|
415
|
-
sessionFactEntries: sessionFacts.getSessionFactEntryCount(),
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
const RECENTLY_CLEAN_TTL_TURNS = 5;
|
|
419
|
-
// B10: tracks files that were the *primary* edited file this turn.
|
|
420
|
-
// These are excluded from cascade neighbor results — their own pipeline run
|
|
421
|
-
// already reported their diagnostics authoritatively.
|
|
422
|
-
let cascadeTurnScope = 0;
|
|
423
|
-
const primaryFilesThisTurn = new Set();
|
|
424
|
-
function ensureCascadeTurnScope(turnSeq) {
|
|
425
|
-
if (turnSeq === cascadeTurnScope)
|
|
426
|
-
return;
|
|
427
|
-
cascadeTurnScope = turnSeq;
|
|
428
|
-
primaryFilesThisTurn.clear();
|
|
429
|
-
for (const [key, entry] of recentlyCleanNeighborCache) {
|
|
430
|
-
if (turnSeq - entry.turnSeq > RECENTLY_CLEAN_TTL_TURNS) {
|
|
431
|
-
recentlyCleanNeighborCache.delete(key);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
const MAX_PER_FILE = RUNTIME_CONFIG.pipeline.cascadeMaxDiagnosticsPerFile;
|
|
436
|
-
const MAX_FILES = RUNTIME_CONFIG.pipeline.cascadeMaxFiles;
|
|
437
|
-
/**
|
|
438
|
-
* How many of ONE neighbour's ERRORs the display policy may look at (#3157
|
|
439
|
-
* round 2, F1). `MAX_PER_FILE` is the DISPLAY cap and now applies to the
|
|
440
|
-
* survivors, so this is the separate, deliberately generous bound on the
|
|
441
|
-
* policy's INPUT — four full display caps of headroom, so a neighbour would
|
|
442
|
-
* need 80 suppressed findings before a genuine one could fall off the end
|
|
443
|
-
* again.
|
|
444
|
-
*
|
|
445
|
-
* It exists because the policy's cost is linear in the finding count once a
|
|
446
|
-
* project has ANY disposition mark (the zero-I/O hoist in
|
|
447
|
-
* `finding-policy.ts` stops firing and every finding expands into its
|
|
448
|
-
* candidate identities): measured on 40 real neighbours, 1.31 ms/neighbour at
|
|
449
|
-
* 20 findings, 4.74 ms at 80, and 28.29 ms at 500 — that last one is 1131
|
|
450
|
-
* ms/edit across a full 40-neighbour walk, a quarter of the `cascadeSettleWaitMs`
|
|
451
|
-
* window, for a neighbour nobody can read anyway. Truncation here is COUNTED
|
|
452
|
-
* (`inputTruncated` on the `cascade_finding_policy` row), because an input
|
|
453
|
-
* bound whose loss is invisible is exactly the defect F1 found.
|
|
454
|
-
*/
|
|
455
|
-
const MAX_POLICY_INPUT_PER_FILE = MAX_PER_FILE * 4;
|
|
456
|
-
/**
|
|
457
|
-
* The genuine language-server ERROR diagnostics from a cascade neighbor
|
|
458
|
-
* re-check, ready to reconcile into the footer widget (#1093).
|
|
459
|
-
*
|
|
460
|
-
* Drops any diagnostic whose `source` matches an auxiliary-LSP profile
|
|
461
|
-
* (opengrep/ast-grep/zizmor/typos — `findAuxiliaryProfileForSource`). The
|
|
462
|
-
* cascade's `convertLspDiagnostics` tags everything `tool: "lsp"`,
|
|
463
|
-
* `semantic: "blocking"` and — unlike every OTHER widget writer
|
|
464
|
-
* (`runners/lsp.ts`, `tools/lsp-diagnostics.ts`, `tools/lens-diagnostics.ts`) —
|
|
465
|
-
* never runs `retagAuxiliaryDiagnostics`. Since `getAllDiagnostics` /
|
|
466
|
-
* `touchFile({clientScope:"all"})` include the auxiliary servers' findings,
|
|
467
|
-
* writing them here would (a) DOUBLE-COUNT a neighbor's own correctly-tagged aux
|
|
468
|
-
* entry and (b) ESCALATE an advisory/suppressed aux finding into a blocking
|
|
469
|
-
* `tool:"lsp"` error, bypassing aux policy (semantic downgrade, native
|
|
470
|
-
* `# nosemgrep`/`zizmor:ignore` suppression, `skipTestFiles`). Aux findings are
|
|
471
|
-
* per-file lint/security signals the neighbor's OWN per-edit runners own; they
|
|
472
|
-
* are not cross-file impact, so the cascade simply excludes them and the merge
|
|
473
|
-
* (`reconcileCascadeNeighborLspErrors`) preserves the neighbor's existing aux
|
|
474
|
-
* entries untouched. This keeps `isLspErrorEntry`'s "tool === 'lsp' uniquely
|
|
475
|
-
* identifies a genuine language-server entry" contract true for what we write.
|
|
476
|
-
*
|
|
477
|
-
* The DISPLAY list is no longer "left as-is" as it was for #1093: since #3157
|
|
478
|
-
* every display site runs `cascadeDisplay` → `applyCascadeDisplayPolicy`, which
|
|
479
|
-
* applies the finding-policy stack AND keeps the auxiliary retag's own drop set
|
|
480
|
-
* — so the display drops natively-suppressed aux findings while still RENDERING
|
|
481
|
-
* the aux findings this widget writer excludes wholesale. The two remain
|
|
482
|
-
* deliberately different: this one protects `isLspErrorEntry`'s "tool === 'lsp'"
|
|
483
|
-
* contract for PERSISTED widget state, which the ephemeral display list never
|
|
484
|
-
* touches.
|
|
485
|
-
*/
|
|
486
|
-
function cascadeReconcilableLspErrors(rawDiags, neighborPath) {
|
|
487
|
-
return convertLspDiagnostics(rawDiags
|
|
488
|
-
.filter((d) => d.severity === 1 && !findAuxiliaryProfileForSource(d.source))
|
|
489
|
-
.slice(0, MAX_PER_FILE), neighborPath);
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Read the content `binding` verdict off a diagnostics result (#1095).
|
|
493
|
-
*
|
|
494
|
-
* `touchFile` carries `binding` as an EXPLICIT enumerable field on its
|
|
495
|
-
* `TouchFileResult` wrapper (#1179), so it survives any copy of `.diags`.
|
|
496
|
-
* `getAllDiagnostics` still attaches `binding` as a lazy, disk-verifying
|
|
497
|
-
* NON-enumerable getter on each Map entry — reading it directly off the
|
|
498
|
-
* producer's own entry (never a spread/clone, which drops non-enumerables)
|
|
499
|
-
* triggers the disk verify exactly once (memoized per entry). Accepts either
|
|
500
|
-
* shape; returns `undefined` when no binding was attached at all (a partially-
|
|
501
|
-
* mocked client, a non-collecting touch) — indistinguishable from "unknown" at
|
|
502
|
-
* every call site, which is the intended pre-#1095 fall-through.
|
|
503
|
-
*/
|
|
504
|
-
function readBoundToCurrentDisk(rawDiags) {
|
|
505
|
-
return rawDiags
|
|
506
|
-
?.binding?.boundToCurrentDisk;
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* #1095: is this active-touch result a CONFIRMED observation of the neighbor's
|
|
510
|
-
* current on-disk LSP-error state — safe to reconcile into the footer widget and
|
|
511
|
-
* (when clean) to seed the recently-clean neighbor cache?
|
|
512
|
-
*
|
|
513
|
-
* Composes the two independent disqualifiers into ONE clearly-named predicate so
|
|
514
|
-
* a future flag cannot be silently missed at just one of the several gate sites:
|
|
515
|
-
* - `inconclusive` (#1093/#571): the notify/diagnostics wait lapsed its deadline,
|
|
516
|
-
* so a resolved `[]` is NOT a confirmed clean (the #533 false-clean trap).
|
|
517
|
-
* #1549: this now means a PRIMARY's deadline lapsed. A slow auxiliary lands in
|
|
518
|
-
* the coverage-gap bullet below instead of collapsing the whole touch, so this
|
|
519
|
-
* predicate still fails closed for it while the primary's findings survive —
|
|
520
|
-
* which is the point: the two disqualifiers were doing one job for two causes.
|
|
521
|
-
* - a COVERAGE GAP (#1470/#1493): an auxiliary never reported — its push wait was
|
|
522
|
-
* cut off by the aux grace timer (#1470), or it stayed silent with no stored
|
|
523
|
-
* publication for this content (#1493) — so the merged result is missing whatever
|
|
524
|
-
* that scanner would have said. Such a touch is deliberately NOT `inconclusive`
|
|
525
|
-
* (the primary answered), which is exactly why it needs naming here: reading
|
|
526
|
-
* `!inconclusive` alone would let a hung or silent opengrep wipe a live footer
|
|
527
|
-
* finding and seed the recently-clean cache.
|
|
528
|
-
* - `binding.boundToCurrentDisk === false` (#1095): the diagnostics were computed
|
|
529
|
-
* against a DIFFERENT disk state than what is on disk now (the server's view
|
|
530
|
-
* diverged / a pre-fix buffer) — not an observation of current disk. `true` and
|
|
531
|
-
* `"unknown"` both pass; `"unknown"` preserves pre-#1095 behavior for a
|
|
532
|
-
* version-less server exactly.
|
|
533
|
-
*
|
|
534
|
-
* #1179: both flags are EXPLICIT enumerable fields on the `touchFile`
|
|
535
|
-
* `TouchFileResult` wrapper — read them off the wrapper (`rawDiags`), whose
|
|
536
|
-
* `.diags` a downstream `.filter()`/copy operates on without touching the flags.
|
|
537
|
-
*/
|
|
538
|
-
function readInconclusive(rawDiags) {
|
|
539
|
-
return rawDiags?.inconclusive === true;
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* #1549: an inconclusive `neighbor_touch` row used to carry NO attribution, so a
|
|
543
|
-
* forensic sweep could only infer the cause from duration histograms. `touchFile`
|
|
544
|
-
* now names the PRIMARY server(s) whose deadline produced the verdict and which
|
|
545
|
-
* deadline it was (`notify-write` vs `diagnostics-wait`, or `mixed`) — carry both
|
|
546
|
-
* into cascade.log so the record stands on its own. Read off the wrapper, like
|
|
547
|
-
* every other flag here (#1179).
|
|
548
|
-
*/
|
|
549
|
-
function readInconclusiveAttribution(rawDiags) {
|
|
550
|
-
const wrapper = rawDiags;
|
|
551
|
-
return {
|
|
552
|
-
...(wrapper?.inconclusiveServerIds?.length && {
|
|
553
|
-
inconclusiveServerIds: [...wrapper.inconclusiveServerIds],
|
|
554
|
-
}),
|
|
555
|
-
...(wrapper?.inconclusiveReason && {
|
|
556
|
-
inconclusiveReason: wrapper.inconclusiveReason,
|
|
557
|
-
}),
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
function isConfirmedTouch(rawDiags) {
|
|
561
|
-
return (!readInconclusive(rawDiags) &&
|
|
562
|
-
touchCoverageGap(rawDiags).length === 0 &&
|
|
563
|
-
readBoundToCurrentDisk(rawDiags) !== false);
|
|
564
|
-
}
|
|
565
|
-
const reverseDepsIndexCache = new Map();
|
|
566
|
-
const REVERSE_DEPS_MAX_WARM_ROOTS = 8;
|
|
567
|
-
const REVERSE_DEPS_IDLE_EVICT_MS_DEFAULT = 20 * 60_000;
|
|
568
|
-
function reverseDepsIdleEvictMs() {
|
|
569
|
-
const value = Number.parseInt(process.env.PI_LENS_REVERSE_DEPS_IDLE_EVICT_MS ?? "", 10);
|
|
570
|
-
return Number.isSafeInteger(value) && value > 0
|
|
571
|
-
? value
|
|
572
|
-
: REVERSE_DEPS_IDLE_EVICT_MS_DEFAULT;
|
|
573
|
-
}
|
|
574
|
-
function deleteReverseDepsEntry(key) {
|
|
575
|
-
const entry = reverseDepsIndexCache.get(key);
|
|
576
|
-
if (entry?.idleTimer !== undefined)
|
|
577
|
-
clearTimeout(entry.idleTimer);
|
|
578
|
-
if (entry)
|
|
579
|
-
entry.idleTimer = undefined;
|
|
580
|
-
reverseDepsIndexCache.delete(key);
|
|
581
|
-
}
|
|
582
|
-
function touchReverseDepsEntry(key, entry, armIdleTimer = true) {
|
|
583
|
-
entry.lastUsedAt = Date.now();
|
|
584
|
-
if (entry.idleTimer !== undefined)
|
|
585
|
-
clearTimeout(entry.idleTimer);
|
|
586
|
-
entry.idleTimer = undefined;
|
|
587
|
-
if (!armIdleTimer)
|
|
588
|
-
return;
|
|
589
|
-
const stamp = entry.lastUsedAt;
|
|
590
|
-
entry.idleTimer = setTimeout(() => {
|
|
591
|
-
if (reverseDepsIndexCache.get(key) !== entry || entry.lastUsedAt !== stamp)
|
|
592
|
-
return;
|
|
593
|
-
deleteReverseDepsEntry(key);
|
|
594
|
-
}, reverseDepsIdleEvictMs());
|
|
595
|
-
entry.idleTimer.unref?.();
|
|
596
|
-
}
|
|
597
|
-
function setReverseDepsEntry(key, entry, armIdleTimer = true) {
|
|
598
|
-
const previous = reverseDepsIndexCache.get(key);
|
|
599
|
-
if (previous?.idleTimer !== undefined) {
|
|
600
|
-
clearTimeout(previous.idleTimer);
|
|
601
|
-
previous.idleTimer = undefined;
|
|
602
|
-
}
|
|
603
|
-
const resident = { ...entry, lastUsedAt: Date.now() };
|
|
604
|
-
reverseDepsIndexCache.set(key, resident);
|
|
605
|
-
touchReverseDepsEntry(key, resident, armIdleTimer);
|
|
606
|
-
while (reverseDepsIndexCache.size > REVERSE_DEPS_MAX_WARM_ROOTS) {
|
|
607
|
-
const victim = [...reverseDepsIndexCache.entries()].sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)[0];
|
|
608
|
-
if (!victim)
|
|
609
|
-
break;
|
|
610
|
-
deleteReverseDepsEntry(victim[0]);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
function reverseDepsReuseEnabled() {
|
|
614
|
-
const raw = process.env.PI_LENS_REVERSE_DEPS_REUSE;
|
|
615
|
-
return raw !== "0" && raw !== "false";
|
|
616
|
-
}
|
|
617
|
-
/** Test-reset hook — mirrors clearReviewGraphWorkspaceCache's scope. */
|
|
618
|
-
export function clearReverseDepsIndexCache() {
|
|
619
|
-
for (const key of reverseDepsIndexCache.keys())
|
|
620
|
-
deleteReverseDepsEntry(key);
|
|
621
|
-
}
|
|
622
|
-
/** Test-only visibility for Tier-2 eviction/recovery tests. */
|
|
623
|
-
export function _getReverseDepsIndexCacheKeysForTests() {
|
|
624
|
-
return [...reverseDepsIndexCache.entries()]
|
|
625
|
-
.sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)
|
|
626
|
-
.map(([key]) => key);
|
|
627
|
-
}
|
|
628
|
-
/** Test-only seed that still exercises the production timer/cap seam. */
|
|
629
|
-
export function _seedReverseDepsIndexCacheForTests(key, index, generation) {
|
|
630
|
-
setReverseDepsEntry(key, { index, savedToSnapshot: false, generation });
|
|
631
|
-
}
|
|
632
|
-
// Bounded transitive cascade (#162): expand neighbour derivation beyond the
|
|
633
|
-
// one-hop importers/callers to depth-2 dependents, so an edit's blast radius
|
|
634
|
-
// reaches indirect dependents — capped so the per-edit cost stays bounded. The
|
|
635
|
-
// one-hop set is always the floor (it sorts first, before depth-2). Both
|
|
636
|
-
// env-tunable; set depth to 1 to restore the old one-hop-only behaviour.
|
|
637
|
-
const CASCADE_TRANSITIVE_DEPTH = Math.max(1, Number.parseInt(process.env.PI_LENS_CASCADE_TRANSITIVE_DEPTH ?? "2", 10) || 2);
|
|
638
|
-
// #1462: the cap itself, and the derivation that narrows it when this run has
|
|
639
|
-
// already spent the settle window, live in clients/cascade-budget.ts alongside
|
|
640
|
-
// `cascadeSettleWaitMs` — the two knobs only make sense read together.
|
|
641
|
-
// Exported (not just module-local) so the MCP warm-analyze seam
|
|
642
|
-
// (clients/mcp/analyze.ts, #536) can gate its own buildOrUpdateGraph call on
|
|
643
|
-
// the SAME file-kind eligibility this cascade path uses — one source of truth
|
|
644
|
-
// for "does this language get graph nodes at all", never a second hardcoded copy.
|
|
645
|
-
export const CASCADE_GRAPH_KINDS = new Set([
|
|
646
|
-
"jsts",
|
|
647
|
-
"python",
|
|
648
|
-
"go",
|
|
649
|
-
"rust",
|
|
650
|
-
"ruby",
|
|
651
|
-
"cxx",
|
|
652
|
-
]);
|
|
653
|
-
/**
|
|
654
|
-
* Unified cascade orchestration — builds graph, discovers neighbors, and
|
|
655
|
-
* gathers per-file diagnostics with structured logging to cascade.log.
|
|
656
|
-
*
|
|
657
|
-
* autoPropagate (jsts): tsserver pushes diagnostics automatically, so we
|
|
658
|
-
* read from the passive snapshot instead of touching neighbors actively.
|
|
659
|
-
*
|
|
660
|
-
* Degraded fallback: when the graph produces no neighbors (ungraphed languages
|
|
661
|
-
* like Java/Kotlin/C#), fall back to the passive LSP snapshot from getAllDiagnostics
|
|
662
|
-
* to preserve cascade coverage.
|
|
663
|
-
*/
|
|
664
|
-
/**
|
|
665
|
-
* Whether a cascade neighbour is suppressed by the project's ignore config
|
|
666
|
-
* (`.pi-lens.json` / `.gitignore` / global `~/.pi-lens/config.json`), via the
|
|
667
|
-
* same `getProjectIgnoreMatcher` every other scan surface uses. Cascade surfaces
|
|
668
|
-
* collateral diagnostics in OTHER files an edit touched; a file the user ignores
|
|
669
|
-
* (e.g. a `*.test.ts` glob in `.pi-lens.json`) must not be re-surfaced here just
|
|
670
|
-
* because it imports the edited file — project walk and lens_diagnostics already
|
|
671
|
-
* filter it, and cascade was the last surface that didn't (#297). Fail-open: a
|
|
672
|
-
* config-probe error never drops a neighbour, matching the walkers' behaviour.
|
|
673
|
-
*/
|
|
674
|
-
function isIgnoredCascadeNeighbor(filePath, cwd) {
|
|
675
|
-
try {
|
|
676
|
-
return getProjectIgnoreMatcher(cwd).isIgnored(filePath, false);
|
|
677
|
-
}
|
|
678
|
-
catch {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* #348 phase 2 per-edit seam: update the warm in-memory word index for one
|
|
684
|
-
* file, mirroring the review graph's per-edit rebuild at the same call site.
|
|
685
|
-
*
|
|
686
|
-
* Rules (each a documented, deliberate simplicity choice, not an oversight):
|
|
687
|
-
* - `wordIndex` null (no index loaded yet) ⇒ no-op. Cold-session handoff is
|
|
688
|
-
* OWNED by phase 1's lifecycle/background build, never invented here — an
|
|
689
|
-
* edit arriving before that build finishes just doesn't update anything;
|
|
690
|
-
* the eventual full build already reflects every file on disk, incl. this
|
|
691
|
-
* edit, so nothing is lost, only delayed.
|
|
692
|
-
* - `wordIndex.forward` undefined (pre-phase-2 index shape, e.g. a snapshot
|
|
693
|
-
* persisted before this feature or one still using the old serialized
|
|
694
|
-
* shape) ⇒ no-op. `updateWordIndexDocument` already refuses to mutate a
|
|
695
|
-
* forward-index-less index (see its doc comment) — this is the same rule
|
|
696
|
-
* surfaced one layer up so the caller isn't left guessing why nothing
|
|
697
|
-
* happened. The NEXT full rebuild (session-start lifecycle) installs a
|
|
698
|
-
* forward-index-bearing index that later edits CAN update incrementally.
|
|
699
|
-
* - `content` undefined (pipeline couldn't read the file — deleted, or a
|
|
700
|
-
* transient race) ⇒ no-op. Deletions aren't plumbed at this seam (this
|
|
701
|
-
* call site only ever sees the edited file's post-write content, never a
|
|
702
|
-
* delete event) — a removed file ages out at the next full rebuild, same
|
|
703
|
-
* scope boundary the review graph accepts for deletes.
|
|
704
|
-
* - File over the shared `WORD_INDEX_MAX_BYTES` cap ⇒ removed/absent from
|
|
705
|
-
* the index (never partially indexed) — same cap phase 1's build path
|
|
706
|
-
* enforces via `collectWordIndexDocs`.
|
|
707
|
-
* - On a successful update, `onUpdated` fires so the caller can schedule a
|
|
708
|
-
* debounced persist (never a synchronous write per edit — same #260
|
|
709
|
-
* discipline as the graph).
|
|
710
|
-
*
|
|
711
|
-
* Race safety against a build-in-progress: the update runs cooperatively, on
|
|
712
|
-
* an 8 ms budget, through the word index's own per-index operation queue
|
|
713
|
-
* (#2067 AC4). It used to run synchronously here, on the reasoning that two
|
|
714
|
-
* overlapping cascades (#450's unawaited concurrency) could not interleave
|
|
715
|
-
* mid-mutation if nothing ever yielded — the reviewer's 300-document probe had
|
|
716
|
-
* lost 1,719 postings during an 8 ms yield. That hazard is what the queue
|
|
717
|
-
* closed: every async operation on one index is chained behind the previous
|
|
718
|
-
* one in call order, so overlapping cascades are ordered rather than
|
|
719
|
-
* interleaved, and the cooperative arena compactor is separately proof against
|
|
720
|
-
* a synchronous edit landing mid-copy (#2117 review F2). What the synchronous
|
|
721
|
-
* variant could not fix is its own cost: one replacement on a 2,600-document
|
|
722
|
-
* corpus held the event loop for the whole replacement, which is the block
|
|
723
|
-
* #2067 measured at 1.1 s before interning and still tens of ms after it.
|
|
724
|
-
*
|
|
725
|
-
* The only cross-build hazard is a full session-start rebuild REPLACING
|
|
726
|
-
* `runtime.wordIndex` with a new object between the caller reading
|
|
727
|
-
* `runtime.wordIndex` (in runtime-tool-result.ts, synchronous) and this
|
|
728
|
-
* function receiving it — in that case this call simply mutates whichever
|
|
729
|
-
* index object it was handed (old or new), and the other one is
|
|
730
|
-
* abandoned/superseded, never corrupted.
|
|
731
|
-
*/
|
|
732
|
-
async function updateWordIndexForCascade(args) {
|
|
733
|
-
const { wordIndex, filePath, content, onUpdated, dbg } = args;
|
|
734
|
-
if (!wordIndex || !wordIndex.forward)
|
|
735
|
-
return;
|
|
736
|
-
if (content === undefined)
|
|
737
|
-
return;
|
|
738
|
-
const byteLength = Buffer.byteLength(content, "utf-8");
|
|
739
|
-
if (byteLength > WORD_INDEX_MAX_BYTES) {
|
|
740
|
-
await removeWordIndexDocumentAsync(wordIndex, filePath);
|
|
741
|
-
dbg?.(`word-index per-edit: dropped ${filePath} (over size cap)`);
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
await updateWordIndexDocumentForEdit(wordIndex, {
|
|
745
|
-
path: filePath,
|
|
746
|
-
content,
|
|
747
|
-
});
|
|
748
|
-
dbg?.(`word-index per-edit: updated ${filePath}`);
|
|
749
|
-
}
|
|
750
|
-
onUpdated?.(wordIndex);
|
|
751
|
-
}
|
|
752
|
-
export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
753
|
-
const reverseDepsTimersToRelease = new Set();
|
|
754
|
-
const reverseDepsEntriesAtStart = new Set(reverseDepsIndexCache.values());
|
|
755
|
-
// #1462: legacy callers use this run's age when sizing the neighbour walk.
|
|
756
|
-
// The pipeline supplies a live turn_end clock so deferred work does not charge
|
|
757
|
-
// time that elapsed before turn_end began waiting.
|
|
758
|
-
const cascadeStart = Date.now();
|
|
759
|
-
try {
|
|
760
|
-
const { hasBlockers = false, dbg, turnSeq = 0, writeSeq, projectRoot, seqState, turnEndCascadeSettleStart, fileContent, wordIndex, onWordIndexUpdated, } = options;
|
|
761
|
-
ensureCascadeTurnScope(turnSeq);
|
|
762
|
-
if (hasBlockers) {
|
|
763
|
-
logCascade({
|
|
764
|
-
phase: "cascade_skip",
|
|
765
|
-
filePath,
|
|
766
|
-
reason: "primary_has_blockers",
|
|
767
|
-
});
|
|
768
|
-
return {
|
|
769
|
-
filePath,
|
|
770
|
-
result: undefined,
|
|
771
|
-
neighborCount: 0,
|
|
772
|
-
diagnosticCount: 0,
|
|
773
|
-
skipReason: "blockers",
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
const fileKind = detectFileKind(filePath);
|
|
777
|
-
if (!fileKind) {
|
|
778
|
-
logCascade({ phase: "cascade_skip", filePath, reason: "non_code_file" });
|
|
779
|
-
return {
|
|
780
|
-
filePath,
|
|
781
|
-
result: undefined,
|
|
782
|
-
neighborCount: 0,
|
|
783
|
-
diagnosticCount: 0,
|
|
784
|
-
skipReason: "non_code",
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
const normalizedFile = resolveRunnerPath(cwd, filePath);
|
|
788
|
-
const normalizedFileKey = normalizeMapKey(normalizedFile);
|
|
789
|
-
// B10: record this file as a primary edit so later cascade calls in the same
|
|
790
|
-
// turn won't show it as a neighbor.
|
|
791
|
-
primaryFilesThisTurn.add(normalizedFileKey);
|
|
792
|
-
// #348 phase 2: warm per-edit word-index maintenance, review-graph style —
|
|
793
|
-
// update at the SAME seam as the graph rebuild below, using content the
|
|
794
|
-
// pipeline already read (no extra I/O). See computeCascadeForFile's
|
|
795
|
-
// `wordIndex`/`fileContent` doc comments for the cold/no-forward-index
|
|
796
|
-
// no-op rules.
|
|
797
|
-
//
|
|
798
|
-
// Keyed by `path.resolve(filePath)` (native separators, tool-input casing).
|
|
799
|
-
// Since #1025 the word index's path maps are `PathKeyedMap`s that fold every
|
|
800
|
-
// key through `wordIndexKey` (`normalizeEphemeralMapKey` — slash-fold +
|
|
801
|
-
// win32-lowercase) INTERNALLY, so this per-edit key and the build path's own
|
|
802
|
-
// walk-derived key (`collectWordIndexDocs` → `collectSourceFilesAsync`)
|
|
803
|
-
// collapse to the same entry regardless of on-disk-vs-input casing/separator.
|
|
804
|
-
// The old hazard — this update silently orphaning a SECOND entry next to the
|
|
805
|
-
// walker's original-cased one — is now structurally impossible at the map
|
|
806
|
-
// layer, so this seam no longer has to hand-match the build path's key shape.
|
|
807
|
-
await updateWordIndexForCascade({
|
|
808
|
-
wordIndex,
|
|
809
|
-
filePath: nodePath.resolve(filePath),
|
|
810
|
-
content: fileContent,
|
|
811
|
-
onUpdated: onWordIndexUpdated,
|
|
812
|
-
dbg,
|
|
813
|
-
});
|
|
814
|
-
let impact = {
|
|
815
|
-
filePath: normalizedFile,
|
|
816
|
-
changedSymbols: [],
|
|
817
|
-
directImporters: [],
|
|
818
|
-
directCallers: [],
|
|
819
|
-
neighborFiles: [],
|
|
820
|
-
riskFlags: [],
|
|
821
|
-
};
|
|
822
|
-
let sortedNeighbors = [];
|
|
823
|
-
let importerSet = new Set();
|
|
824
|
-
let callerSet = new Set();
|
|
825
|
-
let referenceCount = 0;
|
|
826
|
-
let cascadeCandidateCount = 0;
|
|
827
|
-
let cascadeEligibleCount = 0;
|
|
828
|
-
let cascadeFilterDroppedCount = 0;
|
|
829
|
-
// #1446 item 4: how many eligible neighbors the flat CASCADE_NEIGHBOUR_BUDGET
|
|
830
|
-
// cut off, distinct from candidates dropped by the filters above it
|
|
831
|
-
// (missing on disk, vendor, ignored, already-primary-this-turn) — those are
|
|
832
|
-
// never actionable regardless of budget, so counting them as "truncated"
|
|
833
|
-
// would overstate what a larger budget could actually recover.
|
|
834
|
-
let cascadeBudgetTruncated = 0;
|
|
835
|
-
let transitiveTruncated = false;
|
|
836
|
-
// #1462: the budget actually in force for THIS run and the settle time left
|
|
837
|
-
// when it was sized. Both are overwritten inside the graph branch below,
|
|
838
|
-
// which is the only path that reaches `cascade_result` — the other branch
|
|
839
|
-
// returns a `non_code` skip before any of this is logged.
|
|
840
|
-
let cascadeNeighbourBudget = CASCADE_NEIGHBOUR_BUDGET;
|
|
841
|
-
let cascadeBudgetRemainingMs = 0;
|
|
842
|
-
let cascadeBudgetZone = "fits";
|
|
843
|
-
let cascadeDeliveryWindowMs = 0;
|
|
844
|
-
if (CASCADE_GRAPH_KINDS.has(fileKind)) {
|
|
845
|
-
const graphStart = Date.now();
|
|
846
|
-
const graph = await buildOrUpdateGraph(cwd, [normalizedFile], sessionFacts, seqState);
|
|
847
|
-
const graphMs = Date.now() - graphStart;
|
|
848
|
-
// #459: the reuse decision keys on graph.buildGeneration — a stamp that
|
|
849
|
-
// travels WITH the graph instance this cascade holds. Deliberately NOT the
|
|
850
|
-
// global last-build-info slot: post-#450 cascades overlap, and another
|
|
851
|
-
// cascade's cache-hit build can overwrite that slot (graphChanged:false)
|
|
852
|
-
// between this build mutating the graph and this read — which would turn a
|
|
853
|
-
// changed graph into a spurious reuse of a stale index that steady-state
|
|
854
|
-
// cache hits then never heal. Generation equality can't be clobbered into
|
|
855
|
-
// a false positive: a graph-mutating build always mints a new generation.
|
|
856
|
-
// An unstamped graph (mode "skipped") always rebuilds.
|
|
857
|
-
const graphBuildInfo = getGraphBuildInfoForGraph(graph);
|
|
858
|
-
// #1179 fail-closed: `getGraphBuildInfoForGraph` falls back to the global
|
|
859
|
-
// last-build slot on a WeakMap identity miss, which — once some real build
|
|
860
|
-
// has stamped — could carry a SIBLING graph's healthy `mode: "cached"`. The
|
|
861
|
-
// degraded-marker gate below must not read a sibling's state, so it checks
|
|
862
|
-
// trustworthiness and, when the slot cannot be trusted for THIS graph, treats
|
|
863
|
-
// coverage as unknown (degraded) rather than clean. Always trustworthy on the
|
|
864
|
-
// live path — every `_doBuildGraph` return stamps the graph before the cascade
|
|
865
|
-
// reads it — so this is inert today and only hardens a future unstamped graph.
|
|
866
|
-
const graphBuildInfoTrustworthy = graphBuildInfoIsTrustworthy(graph);
|
|
867
|
-
const workspaceKey = normalizeMapKey(cwd);
|
|
868
|
-
const cachedReverseDeps = reverseDepsIndexCache.get(workspaceKey);
|
|
869
|
-
if (cachedReverseDeps) {
|
|
870
|
-
touchReverseDepsEntry(workspaceKey, cachedReverseDeps, false);
|
|
871
|
-
reverseDepsTimersToRelease.add(workspaceKey);
|
|
872
|
-
}
|
|
873
|
-
const importDelta = getGraphImportChanges(graph);
|
|
874
|
-
// A one-step delta is only usable against an index cached at exactly the
|
|
875
|
-
// delta's predecessor generation. Builds minted elsewhere (mcp analyze,
|
|
876
|
-
// lens-map, session warm) advance generations whose import changes this
|
|
877
|
-
// delta does not cover — reusing/patching across that gap stamps a stale
|
|
878
|
-
// index as current, and generation equality then hides the loss forever
|
|
879
|
-
// (#939 review finding 1).
|
|
880
|
-
const deltaContiguous = importDelta !== undefined &&
|
|
881
|
-
importDelta.fromGeneration !== undefined &&
|
|
882
|
-
cachedReverseDeps !== undefined &&
|
|
883
|
-
cachedReverseDeps.generation === importDelta.fromGeneration;
|
|
884
|
-
const importChanges = deltaContiguous ? importDelta.changes : undefined;
|
|
885
|
-
const importsChanged = importChanges?.some((change) => change.existedBefore !== change.existsAfter ||
|
|
886
|
-
change.priorTargets.length !== change.newTargets.length ||
|
|
887
|
-
change.priorTargets.some((target, index) => target !== change.newTargets[index]));
|
|
888
|
-
const canReuse = reverseDepsReuseEnabled() &&
|
|
889
|
-
cachedReverseDeps !== undefined &&
|
|
890
|
-
((graph.buildGeneration !== undefined &&
|
|
891
|
-
cachedReverseDeps.generation === graph.buildGeneration) ||
|
|
892
|
-
(importChanges !== undefined && !importsChanged));
|
|
893
|
-
let reverseDepsIndex;
|
|
894
|
-
let reverseDepsSaved;
|
|
895
|
-
if (canReuse && cachedReverseDeps) {
|
|
896
|
-
reverseDepsIndex = cachedReverseDeps.index;
|
|
897
|
-
reverseDepsSaved = cachedReverseDeps.savedToSnapshot;
|
|
898
|
-
cachedReverseDeps.generation = graph.buildGeneration;
|
|
899
|
-
logCascade({
|
|
900
|
-
phase: "reverse_deps_cache",
|
|
901
|
-
filePath,
|
|
902
|
-
durationMs: Date.now() - graphStart,
|
|
903
|
-
metadata: {
|
|
904
|
-
action: "reused_unchanged",
|
|
905
|
-
savedToSnapshot: reverseDepsSaved,
|
|
906
|
-
importsFileCount: Object.keys(reverseDepsIndex.imports).length,
|
|
907
|
-
importedByFileCount: Object.keys(reverseDepsIndex.importedBy)
|
|
908
|
-
.length,
|
|
909
|
-
},
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
else if (cachedReverseDeps && importChanges && importsChanged) {
|
|
913
|
-
reverseDepsIndex = patchReverseDependencyIndex(cachedReverseDeps.index, importChanges);
|
|
914
|
-
reverseDepsSaved = writeReverseDependencyIndexToSnapshot({
|
|
915
|
-
cwd,
|
|
916
|
-
index: reverseDepsIndex,
|
|
917
|
-
dbg,
|
|
918
|
-
});
|
|
919
|
-
setReverseDepsEntry(workspaceKey, {
|
|
920
|
-
index: reverseDepsIndex,
|
|
921
|
-
savedToSnapshot: reverseDepsSaved,
|
|
922
|
-
generation: graph.buildGeneration,
|
|
923
|
-
}, false);
|
|
924
|
-
reverseDepsTimersToRelease.add(workspaceKey);
|
|
925
|
-
logCascade({
|
|
926
|
-
phase: "reverse_deps_cache",
|
|
927
|
-
filePath,
|
|
928
|
-
durationMs: Date.now() - graphStart,
|
|
929
|
-
metadata: {
|
|
930
|
-
action: "patched_import_changes",
|
|
931
|
-
changedFileCount: importChanges.length,
|
|
932
|
-
savedToSnapshot: reverseDepsSaved,
|
|
933
|
-
},
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
else {
|
|
937
|
-
reverseDepsIndex = buildReverseDependencyIndexFromGraph({
|
|
938
|
-
cwd,
|
|
939
|
-
graph,
|
|
940
|
-
});
|
|
941
|
-
reverseDepsSaved = writeReverseDependencyIndexToSnapshot({
|
|
942
|
-
cwd,
|
|
943
|
-
index: reverseDepsIndex,
|
|
944
|
-
dbg,
|
|
945
|
-
});
|
|
946
|
-
setReverseDepsEntry(workspaceKey, {
|
|
947
|
-
index: reverseDepsIndex,
|
|
948
|
-
savedToSnapshot: reverseDepsSaved,
|
|
949
|
-
generation: graph.buildGeneration,
|
|
950
|
-
}, false);
|
|
951
|
-
reverseDepsTimersToRelease.add(workspaceKey);
|
|
952
|
-
logCascade({
|
|
953
|
-
phase: "reverse_deps_cache",
|
|
954
|
-
filePath,
|
|
955
|
-
durationMs: Date.now() - graphStart,
|
|
956
|
-
metadata: {
|
|
957
|
-
action: "refresh_from_review_graph",
|
|
958
|
-
savedToSnapshot: reverseDepsSaved,
|
|
959
|
-
importsFileCount: Object.keys(reverseDepsIndex.imports).length,
|
|
960
|
-
importedByFileCount: Object.keys(reverseDepsIndex.importedBy)
|
|
961
|
-
.length,
|
|
962
|
-
importEdgeCount: Object.values(reverseDepsIndex.imports).reduce((total, imports) => total + imports.length, 0),
|
|
963
|
-
},
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
// Count files represented in the graph (nodes with a filePath).
|
|
967
|
-
const graphFileCount = new Set([...graph.nodes.values()].flatMap((n) => n.filePath ? [n.filePath] : [])).size;
|
|
968
|
-
logCascade({
|
|
969
|
-
phase: "graph_build",
|
|
970
|
-
filePath,
|
|
971
|
-
graphBuiltMs: graphMs,
|
|
972
|
-
graphReused: graphBuildInfo.reused,
|
|
973
|
-
graphNodeCount: graph.nodes.size,
|
|
974
|
-
graphFileCount,
|
|
975
|
-
graphChangedSymbolCount: (graph.changedSymbolsByFile.get(normalizedFileKey) ?? []).length,
|
|
976
|
-
metadata: {
|
|
977
|
-
graphBuildMode: graphBuildInfo.mode,
|
|
978
|
-
skipReason: graphBuildInfo.skipReason,
|
|
979
|
-
sourceFileCount: graphBuildInfo.sourceFileCount,
|
|
980
|
-
maxFileCount: graphBuildInfo.maxFileCount,
|
|
981
|
-
// #451: when the seq fast path fell back (or was skipped), why — so
|
|
982
|
-
// cascade.log surfaces the fast-path hit/miss rate.
|
|
983
|
-
seqFastpathFallback: graphBuildInfo.seqFastpathFallback,
|
|
984
|
-
},
|
|
985
|
-
});
|
|
986
|
-
impact = computeImpactCascade(graph, normalizedFile, cwd);
|
|
987
|
-
// #1023: buildOrUpdateGraph returns an EMPTY graph (seeded only with the
|
|
988
|
-
// changed file's own symbols) when the repo is over
|
|
989
|
-
// PI_LENS_REVIEW_GRAPH_MAX_FILES (`too_many_files`) or the root is unsafe
|
|
990
|
-
// (`unsafe_root`) — both stamp mode "skipped" on the already-read
|
|
991
|
-
// graphBuildInfo. A capped or entry-budget-truncated graph is also not a
|
|
992
|
-
// complete dependent set even though it has nodes, so it must not look like
|
|
993
|
-
// a clean leaf. Thread the ALREADY-KNOWN degraded state (never re-derived)
|
|
994
|
-
// onto the result so the turn-end seam surfaces an honest advisory instead
|
|
995
|
-
// of a silent all-clear (#533). Keyed strictly off the graph's explicit
|
|
996
|
-
// degraded marker, NOT off `neighborFiles.length === 0`.
|
|
997
|
-
if ((graphBuildInfo.mode === "skipped" ||
|
|
998
|
-
graph.persistCoverage?.partial === true ||
|
|
999
|
-
!graphBuildInfoTrustworthy) &&
|
|
1000
|
-
!impact.indeterminate) {
|
|
1001
|
-
const coverage = graph.persistCoverage;
|
|
1002
|
-
impact.indeterminate = {
|
|
1003
|
-
reason: "graph_degraded",
|
|
1004
|
-
detail: !graphBuildInfoTrustworthy
|
|
1005
|
-
? // #1179 fail-closed: the graph's own build-info was not found under
|
|
1006
|
-
// its identity and the global slot may be a sibling build's — don't
|
|
1007
|
-
// trust its mode. Surface an honest "unknown" advisory rather than a
|
|
1008
|
-
// (possibly sibling-derived) all-clear.
|
|
1009
|
-
"review graph coverage unknown — build state unavailable for this graph"
|
|
1010
|
-
: graphBuildInfo.mode === "skipped"
|
|
1011
|
-
? graphBuildInfo.skipReason === "too_many_files"
|
|
1012
|
-
? graphBuildInfo.sourceFileCountTruncated
|
|
1013
|
-
? `review graph disabled — more than ${graphBuildInfo.maxFileCount ?? "?"} files (cap ${graphBuildInfo.maxFileCount ?? "?"})`
|
|
1014
|
-
: `review graph disabled — ${graphBuildInfo.sourceFileCount ?? "?"} files over the ${graphBuildInfo.maxFileCount ?? "?"} cap`
|
|
1015
|
-
: graphBuildInfo.skipReason === "unsafe_root"
|
|
1016
|
-
? "review graph skipped — workspace root is at/above home dir"
|
|
1017
|
-
: `review graph unavailable (${graphBuildInfo.skipReason ?? "skipped"})`
|
|
1018
|
-
: coverage?.sourceFilesTruncated
|
|
1019
|
-
? "review graph partial — source walk stopped at its visited-entry budget"
|
|
1020
|
-
: "review graph partial — persisted graph coverage is incomplete",
|
|
1021
|
-
sourceFileCount: graphBuildInfo.sourceFileCount,
|
|
1022
|
-
maxFileCount: graphBuildInfo.maxFileCount,
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
const reverseDepNeighbors = getAffectedFilesFromIndex(reverseDepsIndex, normalizedFile, 1, MAX_FILES * 2);
|
|
1026
|
-
logCascade({
|
|
1027
|
-
phase: "reverse_deps_cache",
|
|
1028
|
-
filePath,
|
|
1029
|
-
metadata: {
|
|
1030
|
-
action: "merge_neighbors",
|
|
1031
|
-
depth: 1,
|
|
1032
|
-
neighborCount: reverseDepNeighbors.length,
|
|
1033
|
-
neighbors: reverseDepNeighbors.slice(0, 10),
|
|
1034
|
-
},
|
|
1035
|
-
});
|
|
1036
|
-
if (reverseDepNeighbors.length > 0) {
|
|
1037
|
-
impact.directImporters = [
|
|
1038
|
-
...new Set([...impact.directImporters, ...reverseDepNeighbors]),
|
|
1039
|
-
];
|
|
1040
|
-
impact.neighborFiles = [
|
|
1041
|
-
...new Set([...impact.neighborFiles, ...reverseDepNeighbors]),
|
|
1042
|
-
];
|
|
1043
|
-
logCascade({
|
|
1044
|
-
phase: "neighbor_snapshot",
|
|
1045
|
-
filePath,
|
|
1046
|
-
neighborFile: "[reverse-deps-cache]",
|
|
1047
|
-
diagnosticCount: reverseDepNeighbors.length,
|
|
1048
|
-
autoPropagate: false,
|
|
1049
|
-
metadata: { reverseDepsCache: true },
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
// Symbol-level blast radius via LSP references (precision upgrade over
|
|
1053
|
-
// file-level import edges). Only when changed symbols are detected.
|
|
1054
|
-
// Keep the budget tight: 750ms per symbol, 1200ms total, max 3 symbols.
|
|
1055
|
-
if (impact.changedSymbols.length > 0) {
|
|
1056
|
-
const lspService = getLSPService();
|
|
1057
|
-
const symbolNodeIds = graph.symbolNodesByFile.get(normalizedFileKey) ?? [];
|
|
1058
|
-
const refFiles = new Set();
|
|
1059
|
-
const refsStart = Date.now();
|
|
1060
|
-
for (const symbolName of impact.changedSymbols.slice(0, 3)) {
|
|
1061
|
-
const symbolNodeId = symbolNodeIds.find((id) => {
|
|
1062
|
-
const node = graph.nodes.get(id);
|
|
1063
|
-
return node?.symbolName === symbolName;
|
|
1064
|
-
});
|
|
1065
|
-
if (!symbolNodeId)
|
|
1066
|
-
continue;
|
|
1067
|
-
const node = graph.nodes.get(symbolNodeId);
|
|
1068
|
-
const line = Number(node?.metadata?.line ?? 0);
|
|
1069
|
-
const column = Number(node?.metadata?.column ?? 0);
|
|
1070
|
-
if (line <= 0)
|
|
1071
|
-
continue;
|
|
1072
|
-
// #1109: store the timer and clear it once the race settles. Without
|
|
1073
|
-
// this, when `references()` wins (the common case), the losing
|
|
1074
|
-
// `setTimeout` stays a REF'D pending timer for the remaining 750ms —
|
|
1075
|
-
// harmless in a long-lived session, but a keep-alive tail in a
|
|
1076
|
-
// one-shot `pi --print` process (same uncleared-race-timeout class
|
|
1077
|
-
// fixed for the LSP client-wait leak in clients/lsp/index.ts, #1097).
|
|
1078
|
-
let refsTimer;
|
|
1079
|
-
try {
|
|
1080
|
-
const refs = await Promise.race([
|
|
1081
|
-
lspService.references(normalizedFile, line - 1, column - 1, false),
|
|
1082
|
-
new Promise((_, reject) => {
|
|
1083
|
-
refsTimer = setTimeout(() => reject(new Error("timeout")), 750);
|
|
1084
|
-
}),
|
|
1085
|
-
]);
|
|
1086
|
-
for (const ref of refs) {
|
|
1087
|
-
let resolved;
|
|
1088
|
-
try {
|
|
1089
|
-
resolved = ref.uri.startsWith("file://")
|
|
1090
|
-
? fileURLToPath(ref.uri)
|
|
1091
|
-
: ref.uri;
|
|
1092
|
-
}
|
|
1093
|
-
catch {
|
|
1094
|
-
continue;
|
|
1095
|
-
}
|
|
1096
|
-
if (normalizeMapKey(resolved) !== normalizedFileKey &&
|
|
1097
|
-
nodeFs.existsSync(resolved)) {
|
|
1098
|
-
refFiles.add(normalizeMapKey(resolved));
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
catch {
|
|
1103
|
-
// Timeout or LSP error — fall back to import-graph neighbors
|
|
1104
|
-
}
|
|
1105
|
-
finally {
|
|
1106
|
-
if (refsTimer !== undefined)
|
|
1107
|
-
clearTimeout(refsTimer);
|
|
1108
|
-
}
|
|
1109
|
-
if (Date.now() - refsStart > 1200)
|
|
1110
|
-
break; // Hard ceiling
|
|
1111
|
-
}
|
|
1112
|
-
if (refFiles.size > 0) {
|
|
1113
|
-
impact.neighborFiles = [
|
|
1114
|
-
...new Set([...impact.neighborFiles, ...refFiles]),
|
|
1115
|
-
];
|
|
1116
|
-
logCascade({
|
|
1117
|
-
phase: "neighbor_snapshot",
|
|
1118
|
-
filePath,
|
|
1119
|
-
neighborFile: "[lsp-references]",
|
|
1120
|
-
diagnosticCount: refFiles.size,
|
|
1121
|
-
durationMs: Date.now() - refsStart,
|
|
1122
|
-
autoPropagate: false,
|
|
1123
|
-
metadata: { lspReferences: true },
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
// #1462: size the rest of this run against the on-time window it has NOT
|
|
1128
|
-
// already spent. Derived HERE — after the graph build, the reverse-deps
|
|
1129
|
-
// refresh and the LSP-reference expansion, which is where the measured
|
|
1130
|
-
// overruns were bought — and used for both the transitive BFS cap and the
|
|
1131
|
-
// final slice, so a rescued run expands less as well as walking less.
|
|
1132
|
-
// Narrowing happens ONLY inside the rescue band (see cascade-budget.ts):
|
|
1133
|
-
// a run that is merely late keeps the whole set and is delivered complete
|
|
1134
|
-
// by #1443's carry-over, because a dropped neighbour is lost for good
|
|
1135
|
-
// while a late one is not. A wired callback that cannot observe an active
|
|
1136
|
-
// turn_end wait uses zero elapsed time rather than charging pre-turn age.
|
|
1137
|
-
{
|
|
1138
|
-
const settleStart = turnEndCascadeSettleStart?.();
|
|
1139
|
-
const elapsedMs = turnEndCascadeSettleStart
|
|
1140
|
-
? settleStart !== undefined && Number.isFinite(settleStart)
|
|
1141
|
-
? Math.max(0, Date.now() - settleStart)
|
|
1142
|
-
: 0
|
|
1143
|
-
: Date.now() - cascadeStart;
|
|
1144
|
-
const decision = deriveCascadeNeighbourBudget({ elapsedMs });
|
|
1145
|
-
cascadeNeighbourBudget = decision.budget;
|
|
1146
|
-
cascadeBudgetRemainingMs = decision.remainingMs;
|
|
1147
|
-
cascadeBudgetZone = decision.zone;
|
|
1148
|
-
cascadeDeliveryWindowMs = decision.deliveryWindowMs;
|
|
1149
|
-
}
|
|
1150
|
-
// Bounded transitive expansion: add depth>1 dependents (indirect
|
|
1151
|
-
// importers/callers/referencers) so the blast radius isn't limited to one
|
|
1152
|
-
// hop. The one-hop sets above remain the floor (they sort first); these
|
|
1153
|
-
// fill the remaining budget. Graph BFS is in-memory + capped.
|
|
1154
|
-
if (CASCADE_TRANSITIVE_DEPTH > 1) {
|
|
1155
|
-
const transitive = computeTransitiveImpact(graph, normalizedFile, {
|
|
1156
|
-
maxDepth: CASCADE_TRANSITIVE_DEPTH,
|
|
1157
|
-
maxHits: cascadeNeighbourBudget,
|
|
1158
|
-
});
|
|
1159
|
-
transitiveTruncated = transitive.truncated;
|
|
1160
|
-
const added = [
|
|
1161
|
-
...new Set(transitive.hits
|
|
1162
|
-
.map((hit) => hit.file)
|
|
1163
|
-
.filter((file) => file && normalizeMapKey(file) !== normalizedFileKey)),
|
|
1164
|
-
].filter((file) => !impact.neighborFiles.includes(file));
|
|
1165
|
-
if (added.length > 0) {
|
|
1166
|
-
impact.neighborFiles = [...impact.neighborFiles, ...added];
|
|
1167
|
-
logCascade({
|
|
1168
|
-
phase: "neighbor_snapshot",
|
|
1169
|
-
filePath,
|
|
1170
|
-
neighborFile: "[transitive-impact]",
|
|
1171
|
-
diagnosticCount: added.length,
|
|
1172
|
-
autoPropagate: false,
|
|
1173
|
-
metadata: {
|
|
1174
|
-
transitive: true,
|
|
1175
|
-
maxDepth: CASCADE_TRANSITIVE_DEPTH,
|
|
1176
|
-
maxDepthReached: transitive.maxDepthReached,
|
|
1177
|
-
truncated: transitive.truncated,
|
|
1178
|
-
},
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
// #1080: exclude KNOWN test-role files from every collateral impact
|
|
1183
|
-
// surface — the formatted header (formatImpactCascade reads `impact`
|
|
1184
|
-
// directly for `Direct importers`/`Direct callers`/`Check next` counts and
|
|
1185
|
-
// names), the active-touch/passive-snapshot neighbor set (sortedNeighbors is
|
|
1186
|
-
// derived from `impact.neighborFiles` below), and the returned `impact`
|
|
1187
|
-
// object. Applied HERE — after graph neighbors, reverse-deps, LSP reference
|
|
1188
|
-
// expansion, and transitive expansion have all been merged in — so it covers
|
|
1189
|
-
// every neighbor source (incl. module-level downstream files that entered via
|
|
1190
|
-
// computeImpactCascade and reference URIs pointing at `*.test.*`). Filtering
|
|
1191
|
-
// upstream of `sortedNeighbors` also means a test URI is never actively
|
|
1192
|
-
// touched solely for cascade diagnostics. Composes the shared `detectFileRole`
|
|
1193
|
-
// seam; a classifier failure RETAINS the candidate (honest — never a false
|
|
1194
|
-
// clean). The project ignore filter below is separate and unchanged (#297).
|
|
1195
|
-
cascadeCandidateCount = impact.neighborFiles.length;
|
|
1196
|
-
impact.directImporters = impact.directImporters.filter((f) => !isTestRoleCollateral(f));
|
|
1197
|
-
impact.directCallers = impact.directCallers.filter((f) => !isTestRoleCollateral(f));
|
|
1198
|
-
impact.neighborFiles = impact.neighborFiles.filter((f) => !isTestRoleCollateral(f));
|
|
1199
|
-
// Sort by relationship strength (B6) then cap to the neighbour budget.
|
|
1200
|
-
// directImporters are most impactful, then callers, then reference edges.
|
|
1201
|
-
importerSet = new Set(impact.directImporters);
|
|
1202
|
-
callerSet = new Set(impact.directCallers);
|
|
1203
|
-
// neighbors that are neither direct importers nor callers are reference-edge neighbors
|
|
1204
|
-
const importerOrCallerSet = new Set([
|
|
1205
|
-
...impact.directImporters,
|
|
1206
|
-
...impact.directCallers,
|
|
1207
|
-
]);
|
|
1208
|
-
referenceCount = impact.neighborFiles.filter((n) => !importerOrCallerSet.has(n)).length;
|
|
1209
|
-
const eligibleNeighbors = [...impact.neighborFiles]
|
|
1210
|
-
.filter((n) => nodeFs.existsSync(n))
|
|
1211
|
-
.filter((n) => !isExternalOrVendorFile(n, cwd))
|
|
1212
|
-
// Honour the project's ignore config: a user-ignored neighbour (e.g.
|
|
1213
|
-
// `**/*.test.ts`) must not surface as collateral cascade noise (#297).
|
|
1214
|
-
.filter((n) => !isIgnoredCascadeNeighbor(n, cwd))
|
|
1215
|
-
// B10: exclude files already edited as primary this turn — their own pipeline
|
|
1216
|
-
// run is the authoritative diagnostic source; showing them as neighbors is noise.
|
|
1217
|
-
.filter((n) => !primaryFilesThisTurn.has(normalizeMapKey(n)))
|
|
1218
|
-
.sort((a, b) => {
|
|
1219
|
-
const rank = (p) => importerSet.has(p) ? 0 : callerSet.has(p) ? 1 : 2;
|
|
1220
|
-
return rank(a) - rank(b);
|
|
1221
|
-
});
|
|
1222
|
-
cascadeEligibleCount = eligibleNeighbors.length;
|
|
1223
|
-
cascadeFilterDroppedCount = Math.max(0, cascadeCandidateCount - cascadeEligibleCount);
|
|
1224
|
-
cascadeBudgetTruncated = Math.max(0, eligibleNeighbors.length - cascadeNeighbourBudget);
|
|
1225
|
-
sortedNeighbors = eligibleNeighbors.slice(0, cascadeNeighbourBudget);
|
|
1226
|
-
if (cascadeBudgetTruncated > 0 || transitiveTruncated) {
|
|
1227
|
-
const budget = {
|
|
1228
|
-
candidateCount: cascadeCandidateCount,
|
|
1229
|
-
eligibleCount: cascadeEligibleCount,
|
|
1230
|
-
selectedCount: sortedNeighbors.length,
|
|
1231
|
-
truncatedCount: cascadeBudgetTruncated,
|
|
1232
|
-
...(transitiveTruncated && { transitiveTruncated: true }),
|
|
1233
|
-
};
|
|
1234
|
-
const detailParts = [
|
|
1235
|
-
...(cascadeBudgetTruncated > 0
|
|
1236
|
-
? [
|
|
1237
|
-
`cascade neighbor budget checked ${budget.selectedCount} of ${budget.eligibleCount} eligible dependents ` +
|
|
1238
|
-
`(${budget.truncatedCount} omitted)`,
|
|
1239
|
-
]
|
|
1240
|
-
: []),
|
|
1241
|
-
...(transitiveTruncated
|
|
1242
|
-
? [
|
|
1243
|
-
"transitive expansion was capped before all eligible dependents were enumerated",
|
|
1244
|
-
]
|
|
1245
|
-
: []),
|
|
1246
|
-
];
|
|
1247
|
-
const detail = detailParts.join("; ");
|
|
1248
|
-
impact.indeterminate = impact.indeterminate
|
|
1249
|
-
? {
|
|
1250
|
-
...impact.indeterminate,
|
|
1251
|
-
detail: impact.indeterminate.detail
|
|
1252
|
-
? `${impact.indeterminate.detail}; ${detail}`
|
|
1253
|
-
: detail,
|
|
1254
|
-
budget,
|
|
1255
|
-
}
|
|
1256
|
-
: { reason: "budget_truncated", detail, budget };
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
else {
|
|
1260
|
-
logCascade({
|
|
1261
|
-
phase: "cascade_skip",
|
|
1262
|
-
filePath,
|
|
1263
|
-
reason: "unsupported_graph_kind",
|
|
1264
|
-
metadata: { fileKind },
|
|
1265
|
-
});
|
|
1266
|
-
return {
|
|
1267
|
-
filePath,
|
|
1268
|
-
result: undefined,
|
|
1269
|
-
neighborCount: 0,
|
|
1270
|
-
diagnosticCount: 0,
|
|
1271
|
-
skipReason: "non_code",
|
|
1272
|
-
};
|
|
1273
|
-
}
|
|
1274
|
-
logCascade({
|
|
1275
|
-
phase: "neighbors_computed",
|
|
1276
|
-
filePath,
|
|
1277
|
-
neighborCount: sortedNeighbors.length,
|
|
1278
|
-
totalNeighborCount: impact.neighborFiles.length,
|
|
1279
|
-
importerCount: impact.directImporters.length,
|
|
1280
|
-
callerCount: impact.directCallers.length,
|
|
1281
|
-
referenceCount: Math.max(0, referenceCount),
|
|
1282
|
-
riskFlags: impact.riskFlags,
|
|
1283
|
-
metadata: {
|
|
1284
|
-
neighbors: sortedNeighbors.slice(0, 10),
|
|
1285
|
-
candidateNeighborCount: cascadeCandidateCount,
|
|
1286
|
-
eligibleNeighborCount: cascadeEligibleCount,
|
|
1287
|
-
filterDroppedCount: cascadeFilterDroppedCount,
|
|
1288
|
-
budgetTrimmedCount: cascadeBudgetTruncated,
|
|
1289
|
-
},
|
|
1290
|
-
});
|
|
1291
|
-
const lspService = getLSPService();
|
|
1292
|
-
// Hoist passive snapshot once — used for auto-propagating LSPs and fallback path.
|
|
1293
|
-
// #1095 memo-freeze caveat: each getAllDiagnostics() result attaches `.binding`
|
|
1294
|
-
// as a LAZY getter that memoizes its disk verdict PER RESULT OBJECT. This cascade
|
|
1295
|
-
// re-calls getAllDiagnostics() every run and never retains the Map across turns,
|
|
1296
|
-
// so every run reads a FRESH verdict against current disk. If cross-turn retention
|
|
1297
|
-
// of this Map is ever introduced, the memoized binding would freeze stale — re-read
|
|
1298
|
-
// a fresh getAllDiagnostics() result at binding-read time instead of caching it.
|
|
1299
|
-
const allDiags = await lspService.getAllDiagnostics();
|
|
1300
|
-
const neighbors = [];
|
|
1301
|
-
let producedLspData = false;
|
|
1302
|
-
let coldSnapshotPaths = [];
|
|
1303
|
-
// #1104: did any DEGRADED-fallback display path (touch-error fallback,
|
|
1304
|
-
// appendFallbackNeighbors) withhold a TTL-fresh entry solely because its
|
|
1305
|
-
// content binding was rejected (`boundToCurrentDisk === false` — computed
|
|
1306
|
-
// against a diverged/pre-fix-edit disk state)? Tracked separately from
|
|
1307
|
-
// `producedLspData` so the HONESTY check below can tell "genuinely nothing to
|
|
1308
|
-
// show" apart from "something existed but was untrustworthy and was hidden" —
|
|
1309
|
-
// the latter must not collapse into a clean-looking result (#1104 honesty
|
|
1310
|
-
// rule, same doctrine as #1023's graph-degraded indeterminate marker).
|
|
1311
|
-
let fallbackBindingRejected = false;
|
|
1312
|
-
const noLspCandidatePaths = new Set();
|
|
1313
|
-
// #1444: neighbours whose in-lane wait was skipped for the quiet-window
|
|
1314
|
-
// reconcile to answer later. Logged on `cascade_result` so a cascade that
|
|
1315
|
-
// deferred EVERY neighbour is distinguishable from a genuine leaf (both are
|
|
1316
|
-
// `neighborCount: 0` with no output otherwise).
|
|
1317
|
-
let collectLaterSkipped = 0;
|
|
1318
|
-
// #1446 item 5: `recentlyCleanNeighborCache` hits are the whole point of the
|
|
1319
|
-
// cache, but they were never counted — the only visible signal was 267s/day
|
|
1320
|
-
// of touch wall time with no way to tell whether the cache was absorbing
|
|
1321
|
-
// repeat work or every touch was cold. (#1899 removed the sibling
|
|
1322
|
-
// `cacheHits` counter along with the dead `neighborTouchCache`.)
|
|
1323
|
-
let recentlyCleanHits = 0;
|
|
1324
|
-
// F1 (#1446 follow-up): `coldTouches` must be counted at the point each
|
|
1325
|
-
// neighbour's OUTCOME is actually known, not derived from `coldSnapshotPaths`
|
|
1326
|
-
// (finalized earlier, before the cache-hit checks below run against it). Using
|
|
1327
|
-
// the pre-outcome list let a neighbour double-count (cold-snapshot AND cache/
|
|
1328
|
-
// recently-clean hit) or vanish from every bucket (an `activePaths` neighbour —
|
|
1329
|
-
// e.g. Python/Go — that misses both caches). These counters partition the
|
|
1330
|
-
// selected-neighbour outcomes; passive snapshots and no-server selections are
|
|
1331
|
-
// tracked separately instead of silently disappearing from the denominator.
|
|
1332
|
-
// `coldTouches` includes every genuine active touch attempt, while
|
|
1333
|
-
// `touchFailures` is its separate bounded failure subtype.
|
|
1334
|
-
let deferredTouches = 0;
|
|
1335
|
-
let coldTouches = 0;
|
|
1336
|
-
let passiveSnapshotHits = 0;
|
|
1337
|
-
let noLspConfigured = 0;
|
|
1338
|
-
let touchFailures = 0;
|
|
1339
|
-
// #3157: this run's AGGREGATE display-policy drops, across every neighbour
|
|
1340
|
-
// the four display sites below rendered. Aggregate, not per neighbour: a
|
|
1341
|
-
// cascade walks up to `CASCADE_NEIGHBOUR_BUDGET` (40) neighbours per edit,
|
|
1342
|
-
// so a record inside the loop would be per-occurrence logging on a per-edit
|
|
1343
|
-
// path — one bounded row per RUN instead (AGENTS.md "bounded observability").
|
|
1344
|
-
// `policyElapsedMs` accumulates ONLY the stack's own time (round 2, F2): a
|
|
1345
|
-
// clock started here would have spanned the whole `touchFile` fan-out and
|
|
1346
|
-
// reported the neighbour WALK as the policy phase — hundreds of ms against
|
|
1347
|
-
// the sub-millisecond cost AC 3 measured, under the same phase literal the
|
|
1348
|
-
// quiet-window lane writes.
|
|
1349
|
-
let policyElapsedMs = 0;
|
|
1350
|
-
const policyCounts = {
|
|
1351
|
-
suppressed: 0,
|
|
1352
|
-
auxSuppressed: 0,
|
|
1353
|
-
total: 0,
|
|
1354
|
-
inputTruncated: 0,
|
|
1355
|
-
};
|
|
1356
|
-
/**
|
|
1357
|
-
* The ONE seam every in-lane cascade DISPLAY list goes through (#3157).
|
|
1358
|
-
*
|
|
1359
|
-
* Before this, all four sites open-coded `convertLspDiagnostics(...)` and
|
|
1360
|
-
* rendered the result raw: `grep applyFindingPolicy clients/dispatch/
|
|
1361
|
-
* integration.ts` returned nothing, so a neighbour ERROR the agent had
|
|
1362
|
-
* marked `false-positive` was hidden by `mode=delta`/`mode=full`/the
|
|
1363
|
-
* per-edit dispatcher/the probe lane/the quiet-window cascade run and STILL
|
|
1364
|
-
* re-reported here on every edit that cascaded to it. The severity filter
|
|
1365
|
-
* and the `MAX_PER_FILE` display cap were the fourth copy of the same three
|
|
1366
|
-
* lines and fold in with it.
|
|
1367
|
-
*
|
|
1368
|
-
* `neighborContent` is passed by the ONE caller that has already read the
|
|
1369
|
-
* neighbour (the active touch reads it to feed `touchFile`) — those are the
|
|
1370
|
-
* exact bytes the diagnostics were computed against, so it is both free and
|
|
1371
|
-
* more faithful than a re-read. The other three sites let the shared helper
|
|
1372
|
-
* read once, and only once a neighbour actually has an ERROR to render.
|
|
1373
|
-
*
|
|
1374
|
-
* Stage order (round 2, F1): severity filter → input bound → convert →
|
|
1375
|
-
* retag → policy → DISPLAY cap. `MAX_PER_FILE` is applied to the survivors
|
|
1376
|
-
* inside the helper; applying it here, before the policy, let policy drops
|
|
1377
|
-
* consume display slots and rendered a false clean.
|
|
1378
|
-
*/
|
|
1379
|
-
const cascadeDisplay = (rawDiags, neighborPath, neighborContent) => {
|
|
1380
|
-
const started = Date.now();
|
|
1381
|
-
const errors = rawDiags.filter((d) => d.severity === 1);
|
|
1382
|
-
const policyInput = errors.slice(0, MAX_POLICY_INPUT_PER_FILE);
|
|
1383
|
-
const result = applyCascadeDisplayPolicy(policyInput, {
|
|
1384
|
-
cwd,
|
|
1385
|
-
policyRoot: projectRoot ?? cwd,
|
|
1386
|
-
filePath: neighborPath,
|
|
1387
|
-
displayCap: MAX_PER_FILE,
|
|
1388
|
-
...(neighborContent !== undefined && { content: neighborContent }),
|
|
1389
|
-
});
|
|
1390
|
-
policyCounts.suppressed += result.suppressed;
|
|
1391
|
-
policyCounts.auxSuppressed += result.auxSuppressed;
|
|
1392
|
-
policyCounts.total += result.total;
|
|
1393
|
-
policyCounts.inputTruncated += errors.length - policyInput.length;
|
|
1394
|
-
policyElapsedMs += Date.now() - started;
|
|
1395
|
-
return result.diagnostics;
|
|
1396
|
-
};
|
|
1397
|
-
if (sortedNeighbors.length > 0) {
|
|
1398
|
-
const snapshotPaths = sortedNeighbors.filter(shouldReadCascadeFromSnapshot);
|
|
1399
|
-
const activePaths = sortedNeighbors.filter((n) => !shouldReadCascadeFromSnapshot(n));
|
|
1400
|
-
// Auto-propagating LSPs (TypeScript/Deno) — read passive snapshot with normalized key.
|
|
1401
|
-
// When the snapshot is valid, use it immediately (no touch needed — server already has
|
|
1402
|
-
// fresh data from auto-propagation). When missing or stale, fall through to the active
|
|
1403
|
-
// touch pool below so we get real diagnostics instead of silently returning zero.
|
|
1404
|
-
coldSnapshotPaths = [];
|
|
1405
|
-
for (const neighborPath of snapshotPaths) {
|
|
1406
|
-
const neighborStart = Date.now();
|
|
1407
|
-
const entry = allDiags.get(normalizeMapKey(neighborPath));
|
|
1408
|
-
const snapshotAgeSec = entry
|
|
1409
|
-
? Math.round((Date.now() - entry.ts) / 1000)
|
|
1410
|
-
: undefined;
|
|
1411
|
-
const ttlFresh = entry != null && Date.now() - entry.ts < CASCADE_DIAGNOSTICS_TTL_MS;
|
|
1412
|
-
// #1095: content binding is the INNER gate; TTL stays the outer bound.
|
|
1413
|
-
// false → the server's diagnostics were computed against a DIFFERENT
|
|
1414
|
-
// disk state (e.g. the PRE-fix content) — don't trust or
|
|
1415
|
-
// reconcile this snapshot; fall through to an active touch on
|
|
1416
|
-
// the same (cold-snapshot) budget as a TTL-stale entry. This
|
|
1417
|
-
// kills the window where the first cascade after a fix-edit
|
|
1418
|
-
// replays the neighbor's pre-fix snapshot.
|
|
1419
|
-
// "unknown" → version-less/unreadable: keep EXACTLY the pre-#1095 TTL-only
|
|
1420
|
-
// behavior (reconcile if TTL-fresh).
|
|
1421
|
-
// true → bound to current disk: reconcile (TTL still the outer bound).
|
|
1422
|
-
// Reading `.binding` triggers the lazy disk verify on the getAllDiagnostics
|
|
1423
|
-
// result — done ONLY when TTL-fresh so a doomed (stale) entry never pays the
|
|
1424
|
-
// stat+hash.
|
|
1425
|
-
const boundToDisk = ttlFresh
|
|
1426
|
-
? readBoundToCurrentDisk(entry)
|
|
1427
|
-
: undefined;
|
|
1428
|
-
const bindingRejected = boundToDisk === false;
|
|
1429
|
-
const snapshotValid = ttlFresh && !bindingRejected;
|
|
1430
|
-
if (!snapshotValid) {
|
|
1431
|
-
// No usable snapshot — queue for active touch alongside non-jsts neighbors.
|
|
1432
|
-
logCascade({
|
|
1433
|
-
phase: "neighbor_snapshot",
|
|
1434
|
-
filePath,
|
|
1435
|
-
neighborFile: neighborPath,
|
|
1436
|
-
diagnosticCount: 0,
|
|
1437
|
-
durationMs: Date.now() - neighborStart,
|
|
1438
|
-
autoPropagate: true,
|
|
1439
|
-
snapshotMissing: entry == null,
|
|
1440
|
-
snapshotAgeSec,
|
|
1441
|
-
coldSnapshot: true,
|
|
1442
|
-
// #1095: distinguish a binding-rejected fall-through (TTL-fresh but the
|
|
1443
|
-
// server's view diverged from disk) from a plain TTL-stale/missing one.
|
|
1444
|
-
...(bindingRejected && {
|
|
1445
|
-
metadata: { bindingState: bindingStateLabel(boundToDisk) },
|
|
1446
|
-
}),
|
|
1447
|
-
});
|
|
1448
|
-
coldSnapshotPaths.push(neighborPath);
|
|
1449
|
-
continue;
|
|
1450
|
-
}
|
|
1451
|
-
// #692: `source: "cascade"` used to be passed here to label `rule`
|
|
1452
|
-
// (`cascade:<code>`) — that override is gone (identity must come from
|
|
1453
|
-
// the diagnostic's own source; see `scanOrigin`'s doc comment), and
|
|
1454
|
-
// cascade neighbor diagnostics are ephemeral display-only output
|
|
1455
|
-
// (never reconciled into persisted widget/dedup state), so the label
|
|
1456
|
-
// had no remaining purpose and is simply dropped rather than migrated.
|
|
1457
|
-
// #3157 site 1 of 4: the passive cold-snapshot hit.
|
|
1458
|
-
const diags = cascadeDisplay(entry.diags, neighborPath);
|
|
1459
|
-
producedLspData = true;
|
|
1460
|
-
passiveSnapshotHits++;
|
|
1461
|
-
const durationMs = Date.now() - neighborStart;
|
|
1462
|
-
logCascade({
|
|
1463
|
-
phase: "neighbor_snapshot",
|
|
1464
|
-
filePath,
|
|
1465
|
-
neighborFile: neighborPath,
|
|
1466
|
-
diagnosticCount: diags.length,
|
|
1467
|
-
durationMs,
|
|
1468
|
-
autoPropagate: true,
|
|
1469
|
-
snapshotMissing: false,
|
|
1470
|
-
snapshotAgeSec,
|
|
1471
|
-
});
|
|
1472
|
-
// #1093: a valid passive snapshot IS a confirmed observation of this
|
|
1473
|
-
// neighbor's current LSP-error state (#571 semantics) — reconcile it into
|
|
1474
|
-
// the footer widget, INCLUDING the confirmed-clean `[]` case, so a
|
|
1475
|
-
// fix-edit to the primary that resolves a cross-file error in this
|
|
1476
|
-
// neighbor clears the neighbor's now-stale footer entry (the #1092
|
|
1477
|
-
// defect). MERGE (genuine LSP errors only — auxiliary findings excluded,
|
|
1478
|
-
// see `cascadeReconcilableLspErrors`) so a live biome/ruff/aux finding or
|
|
1479
|
-
// LSP warning on the neighbor is preserved. Keyed by the primary edit's
|
|
1480
|
-
// `writeSeq` so a genuinely newer per-edit write still wins the
|
|
1481
|
-
// WriteOrderingGuard. `observedAt = entry.ts` (the snapshot's own publish
|
|
1482
|
-
// time, up to CASCADE_DIAGNOSTICS_TTL_MS old) — NOT now() — so replaying an aging
|
|
1483
|
-
// snapshot never re-arms the mtime-staleness gate (the same #1092
|
|
1484
|
-
// re-arming defect this PR fixes for cache hits).
|
|
1485
|
-
//
|
|
1486
|
-
// #1186: `observedAt` here stamps only the INCOMING LSP-error entries.
|
|
1487
|
-
// PRESERVED entries keep their own (possibly fresher) per-entry
|
|
1488
|
-
// `observedAt`, and `reconcileStaleWidgetFiles` now gates per ENTRY — so
|
|
1489
|
-
// if the neighbor's mtime later falls between this `entry.ts` and a
|
|
1490
|
-
// preserved entry's real observation time, only the stale incoming entry
|
|
1491
|
-
// drops and the fresher preserved finding survives (previously the whole
|
|
1492
|
-
// record was over-cleared; that residual is now fixed).
|
|
1493
|
-
reconcileCascadeNeighborLspErrors(neighborPath, cascadeReconcilableLspErrors(entry.diags, neighborPath), writeSeq, entry.ts);
|
|
1494
|
-
neighbors.push({
|
|
1495
|
-
filePath: neighborPath,
|
|
1496
|
-
reason: neighborReason(importerSet, callerSet, neighborPath),
|
|
1497
|
-
diagnostics: diags,
|
|
1498
|
-
lspTouched: false,
|
|
1499
|
-
durationMs,
|
|
1500
|
-
});
|
|
1501
|
-
}
|
|
1502
|
-
// fan-out active touches in parallel (A3):
|
|
1503
|
-
// - non-jsts neighbors (always touched)
|
|
1504
|
-
// - autoPropagate neighbors whose snapshot was missing/stale (coldSnapshotPaths)
|
|
1505
|
-
// use a tighter 1000ms budget since the server is expected to be warm already.
|
|
1506
|
-
const touchResults = await Promise.allSettled([...activePaths, ...coldSnapshotPaths].map(async (neighborPath) => {
|
|
1507
|
-
const isColdSnapshot = coldSnapshotPaths.includes(neighborPath);
|
|
1508
|
-
const neighborStart = Date.now();
|
|
1509
|
-
const cacheKey = normalizeMapKey(neighborPath);
|
|
1510
|
-
const passiveEntry = allDiags.get(cacheKey);
|
|
1511
|
-
const hasFreshPassiveErrors = passiveEntry != null &&
|
|
1512
|
-
Date.now() - passiveEntry.ts < CASCADE_DIAGNOSTICS_TTL_MS &&
|
|
1513
|
-
passiveEntry.diags.some((d) => d.severity === 1);
|
|
1514
|
-
const recentlyClean = recentlyCleanNeighborCache.get(cacheKey);
|
|
1515
|
-
if (recentlyClean &&
|
|
1516
|
-
turnSeq - recentlyClean.turnSeq <= RECENTLY_CLEAN_TTL_TURNS &&
|
|
1517
|
-
!hasFreshPassiveErrors) {
|
|
1518
|
-
producedLspData = true;
|
|
1519
|
-
recentlyCleanHits++;
|
|
1520
|
-
const durationMs = Date.now() - neighborStart;
|
|
1521
|
-
logCascade({
|
|
1522
|
-
phase: "neighbor_snapshot",
|
|
1523
|
-
filePath,
|
|
1524
|
-
neighborFile: neighborPath,
|
|
1525
|
-
diagnosticCount: 0,
|
|
1526
|
-
durationMs,
|
|
1527
|
-
autoPropagate: false,
|
|
1528
|
-
snapshotMissing: false,
|
|
1529
|
-
metadata: {
|
|
1530
|
-
recentlyClean: true,
|
|
1531
|
-
cleanTurnSeq: recentlyClean.turnSeq,
|
|
1532
|
-
},
|
|
1533
|
-
});
|
|
1534
|
-
return {
|
|
1535
|
-
filePath: neighborPath,
|
|
1536
|
-
reason: neighborReason(importerSet, callerSet, neighborPath),
|
|
1537
|
-
diagnostics: [],
|
|
1538
|
-
lspTouched: false,
|
|
1539
|
-
durationMs,
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
// #1899: the A5 same-write cache read stood here. See the removal note
|
|
1543
|
-
// at the module's cache declarations — the gate could only pass inside
|
|
1544
|
-
// a single write, which never runs two cascades.
|
|
1545
|
-
const configuredServerCount = getServersForFileWithConfig(neighborPath).length;
|
|
1546
|
-
if (configuredServerCount === 0) {
|
|
1547
|
-
noLspConfigured++;
|
|
1548
|
-
noLspCandidatePaths.add(cacheKey);
|
|
1549
|
-
logCascade({
|
|
1550
|
-
phase: "neighbor_fallback",
|
|
1551
|
-
filePath,
|
|
1552
|
-
neighborFile: neighborPath,
|
|
1553
|
-
fallbackUsed: false,
|
|
1554
|
-
error: "no_lsp_server_configured",
|
|
1555
|
-
});
|
|
1556
|
-
return undefined;
|
|
1557
|
-
}
|
|
1558
|
-
// A6: async read to avoid blocking event loop on network-mounted drives
|
|
1559
|
-
const content = await nodeFs.promises.readFile(neighborPath, "utf8");
|
|
1560
|
-
// #458/#1444: tier-aware cascade-lane wait. A Tier-3 silent server
|
|
1561
|
-
// cannot give this wait an affirmative clean signal. Native TS7 does
|
|
1562
|
-
// publish, but not inside the cold-snapshot budget. In both cases the
|
|
1563
|
-
// in-lane budget is pure cost. Fire the touch (didOpen/
|
|
1564
|
-
// didChange still happens — the server starts real work) and record
|
|
1565
|
-
// it as outstanding for the agent_settled quiet window to reconcile
|
|
1566
|
-
// instead of waiting here. Ambiguous/missing capability data always
|
|
1567
|
-
// classifies as "waits" (today's behavior) — see cascade-tier.ts.
|
|
1568
|
-
// The whole attempt is try/caught: any surprise (a service shape
|
|
1569
|
-
// that doesn't expose getCapabilitySnapshots/getClientForFile, a
|
|
1570
|
-
// thrown rejection) falls through to the existing full-wait path
|
|
1571
|
-
// below rather than skip the wait on a failure.
|
|
1572
|
-
if (isTierAwareCascadeEnabled()) {
|
|
1573
|
-
try {
|
|
1574
|
-
const snapshots = (await lspService.getCapabilitySnapshots?.(neighborPath)) ?? [];
|
|
1575
|
-
const tier = classifyCascadeWaitTier(lspService, neighborPath, snapshots);
|
|
1576
|
-
if (tier === "tier3-silent" ||
|
|
1577
|
-
tier === "collect-later" ||
|
|
1578
|
-
tier === "diagnostics-unsupported") {
|
|
1579
|
-
const spawnedForTouch = await lspService.getClientForFile(neighborPath);
|
|
1580
|
-
if (spawnedForTouch) {
|
|
1581
|
-
// Sampled BEFORE the touchFile notify: a publish landing
|
|
1582
|
-
// in the notify→record gap must read as post-touch at
|
|
1583
|
-
// reconcile time, never be misclassified as pre-touch
|
|
1584
|
-
// (the reconcile compares this against the client's
|
|
1585
|
-
// PER-FILE publish timestamp — see cascade-tier.ts).
|
|
1586
|
-
const touchedAt = Date.now();
|
|
1587
|
-
await lspService.touchFile(neighborPath, content, {
|
|
1588
|
-
diagnostics: "none",
|
|
1589
|
-
collectDiagnostics: false,
|
|
1590
|
-
silent: true,
|
|
1591
|
-
source: "cascade",
|
|
1592
|
-
clientScope: "primary",
|
|
1593
|
-
});
|
|
1594
|
-
if (tier === "diagnostics-unsupported") {
|
|
1595
|
-
logCascade({
|
|
1596
|
-
phase: "cascade_skip",
|
|
1597
|
-
filePath,
|
|
1598
|
-
neighborFile: neighborPath,
|
|
1599
|
-
durationMs: Date.now() - neighborStart,
|
|
1600
|
-
lspServerCount: configuredServerCount,
|
|
1601
|
-
coldSnapshot: isColdSnapshot,
|
|
1602
|
-
metadata: {
|
|
1603
|
-
serverId: spawnedForTouch.client.serverId,
|
|
1604
|
-
waitTier: tier,
|
|
1605
|
-
},
|
|
1606
|
-
});
|
|
1607
|
-
return undefined;
|
|
1608
|
-
}
|
|
1609
|
-
recordOutstandingCascadeTouch({
|
|
1610
|
-
filePath: neighborPath,
|
|
1611
|
-
serverId: spawnedForTouch.client.serverId,
|
|
1612
|
-
touchedAt,
|
|
1613
|
-
});
|
|
1614
|
-
const durationMs = Date.now() - neighborStart;
|
|
1615
|
-
if (tier === "collect-later")
|
|
1616
|
-
collectLaterSkipped++;
|
|
1617
|
-
// F1: both tier3-silent and collect-later skip the in-lane
|
|
1618
|
-
// wait and record an outstanding touch for the quiet-window
|
|
1619
|
-
// reconcile — neither a cache hit nor a genuine completed
|
|
1620
|
-
// cold touch, so both share this explicit "deferred" bucket
|
|
1621
|
-
// instead of falling out of the partition uncounted.
|
|
1622
|
-
deferredTouches++;
|
|
1623
|
-
logCascade({
|
|
1624
|
-
phase: "cascade_tier3_skip",
|
|
1625
|
-
filePath,
|
|
1626
|
-
neighborFile: neighborPath,
|
|
1627
|
-
durationMs,
|
|
1628
|
-
lspServerCount: configuredServerCount,
|
|
1629
|
-
coldSnapshot: isColdSnapshot,
|
|
1630
|
-
metadata: {
|
|
1631
|
-
serverId: spawnedForTouch.client.serverId,
|
|
1632
|
-
waitTier: tier,
|
|
1633
|
-
},
|
|
1634
|
-
});
|
|
1635
|
-
// Deliberately NOT cached as clean/diagnosed — the wait was
|
|
1636
|
-
// skipped, not resolved, so recentlyCleanNeighborCache must
|
|
1637
|
-
// not treat this as a real answer
|
|
1638
|
-
// (#240 doctrine). Return undefined: the degraded-fallback
|
|
1639
|
-
// path below still has a chance to surface a passive/stale
|
|
1640
|
-
// snapshot, same as any other "no fresh data this touch" case.
|
|
1641
|
-
return undefined;
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
catch (tierErr) {
|
|
1646
|
-
dbg?.(`cascade tier-aware skip attempt failed for ${neighborPath}, falling back to full wait: ${tierErr}`);
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
// Open with silent=true (suppresses didChangeWatchedFiles rechecks, C2)
|
|
1650
|
-
// and collect diagnostics from the same touched clients.
|
|
1651
|
-
// Cold-snapshot neighbors (autoPropagate LSP, server warm) use a tighter
|
|
1652
|
-
// 1000ms budget — they should respond quickly; we'd rather return zero
|
|
1653
|
-
// than block cascade for 2s on a slow open.
|
|
1654
|
-
// F1: this is the ONE remaining outcome after cache hit, recently-clean
|
|
1655
|
-
// hit, and tier-aware deferral have all been ruled out — a genuine
|
|
1656
|
-
// active LSP touch is being issued right now. Count it here (an
|
|
1657
|
-
// attempt, whether it resolves, times out, or the promise rejects
|
|
1658
|
-
// below in the allSettled catch) rather than from `coldSnapshotPaths`,
|
|
1659
|
-
// which is finalized before any of the above checks run and includes
|
|
1660
|
-
// neighbours that resolve via cache/recently-clean instead.
|
|
1661
|
-
coldTouches++;
|
|
1662
|
-
// #1720: language-server scope only, matching the tier-aware touch
|
|
1663
|
-
// above (`clientScope: "primary"`, integration.ts:1826). A neighbor's
|
|
1664
|
-
// content did not change — only its import target did — so an
|
|
1665
|
-
// auxiliary scanner's (ast-grep/opengrep/typos) file-local verdict for
|
|
1666
|
-
// it cannot have changed; only cross-file type semantics (the
|
|
1667
|
-
// language server) can. `reconcileCascadeNeighborLspErrors` already
|
|
1668
|
-
// merges errors-only and preserves the neighbor's existing aux
|
|
1669
|
-
// findings by construction (see its doc comment and
|
|
1670
|
-
// `cascadeReconcilableLspErrors` above), so an `"all"`-scope touch
|
|
1671
|
-
// paid full aux notify traffic and aux confirmation-wait latency for
|
|
1672
|
-
// a re-derivation nothing downstream ever reads. `"primary"` resolves
|
|
1673
|
-
// per `neighborPath` (`getClientForFile` → `getServersForFileWithConfig`),
|
|
1674
|
-
// so a multi-language neighbor still gets its OWN language server,
|
|
1675
|
-
// not the primary edit's.
|
|
1676
|
-
const rawDiags = await lspService.touchFile(neighborPath, content, {
|
|
1677
|
-
diagnostics: "document",
|
|
1678
|
-
collectDiagnostics: true,
|
|
1679
|
-
maxClientWaitMs: isColdSnapshot ? 1000 : 2000,
|
|
1680
|
-
silent: true,
|
|
1681
|
-
source: "cascade",
|
|
1682
|
-
clientScope: "primary",
|
|
1683
|
-
});
|
|
1684
|
-
if (!rawDiags)
|
|
1685
|
-
return undefined;
|
|
1686
|
-
// #1093/#571/#1095: a touch result is only a CONFIRMED observation of the
|
|
1687
|
-
// neighbor's current on-disk state when it is neither `inconclusive` (the
|
|
1688
|
-
// notify/diagnostics wait lapsed — e.g. the tight 1000ms cold-snapshot
|
|
1689
|
-
// budget on a slow server) NOR bound-false (`binding.boundToCurrentDisk
|
|
1690
|
-
// === false` — the diagnostics were computed against a different disk state
|
|
1691
|
-
// than what's on disk now). Either disqualifier means a resolved `[]` is
|
|
1692
|
-
// NOT a confirmed clean: treating it as one would WIPE a live footer finding
|
|
1693
|
-
// (the #533 false-clean trap, worse than the stale-display bug). Both flags
|
|
1694
|
-
// are folded into `isConfirmedTouch` so no gate below can miss one. A
|
|
1695
|
-
// confirmed result reconciles and may seed the recently-clean cache; an
|
|
1696
|
-
// unconfirmed one does neither (else the short-circuit on the next cascade
|
|
1697
|
-
// would make the wipe self-sustain).
|
|
1698
|
-
const confirmed = isConfirmedTouch(rawDiags);
|
|
1699
|
-
const bindingRejected = readBoundToCurrentDisk(rawDiags) === false;
|
|
1700
|
-
const inconclusive = readInconclusive(rawDiags);
|
|
1701
|
-
// #1470/#1493: the third, independent reason a touch is unconfirmed — an
|
|
1702
|
-
// auxiliary that never reported, cut off by our grace timer or silent
|
|
1703
|
-
// with nothing published for this content. Logged alongside the other two
|
|
1704
|
-
// so cascade.log alone still tells the three apart.
|
|
1705
|
-
const unconfirmedServerIds = touchCoverageGap(rawDiags);
|
|
1706
|
-
// #692: `source: "cascade"` no longer overrides `rule` (see the
|
|
1707
|
-
// doc comment on the sibling call above) — dropped rather than
|
|
1708
|
-
// migrated to `scanOrigin` since cascade output never touches
|
|
1709
|
-
// persisted widget/dedup state.
|
|
1710
|
-
// #1179: the severity filter inside `cascadeDisplay` operates on
|
|
1711
|
-
// `rawDiags.diags`; the `inconclusive`/`binding` flags read above
|
|
1712
|
-
// stay on the `rawDiags` wrapper and are unaffected (the shape-5 fix).
|
|
1713
|
-
// #3157 site 2 of 4: the fresh in-lane touch. `content` is the copy
|
|
1714
|
-
// this touch was computed against — no second read.
|
|
1715
|
-
const diags = cascadeDisplay(rawDiags.diags, neighborPath, content);
|
|
1716
|
-
const durationMs = Date.now() - neighborStart;
|
|
1717
|
-
// #1899: the A5 same-write cache write stood here.
|
|
1718
|
-
if (diags.length === 0) {
|
|
1719
|
-
// Only a CONFIRMED clean touch may seed the recently-clean cache
|
|
1720
|
-
// (#1095: a bound-false touch is unconfirmed, exactly like inconclusive).
|
|
1721
|
-
if (confirmed) {
|
|
1722
|
-
recentlyCleanNeighborCache.set(cacheKey, {
|
|
1723
|
-
turnSeq,
|
|
1724
|
-
checkedAt: Date.now(),
|
|
1725
|
-
});
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
else {
|
|
1729
|
-
recentlyCleanNeighborCache.delete(cacheKey);
|
|
1730
|
-
}
|
|
1731
|
-
producedLspData = true;
|
|
1732
|
-
logCascade({
|
|
1733
|
-
phase: "neighbor_touch",
|
|
1734
|
-
filePath,
|
|
1735
|
-
neighborFile: neighborPath,
|
|
1736
|
-
diagnosticCount: diags.length,
|
|
1737
|
-
durationMs,
|
|
1738
|
-
lspTouched: true,
|
|
1739
|
-
lspServerCount: configuredServerCount,
|
|
1740
|
-
coldSnapshot: isColdSnapshot,
|
|
1741
|
-
// #1104: an unconfirmed touch has two independent, otherwise
|
|
1742
|
-
// indistinguishable causes — `inconclusive` (the notify/diagnostics
|
|
1743
|
-
// wait lapsed its deadline) and bound-false (`bindingState`, #1095 —
|
|
1744
|
-
// diagnostics computed against a diverged disk state). Surface
|
|
1745
|
-
// `inconclusive` unconditionally so cascade.log alone (no
|
|
1746
|
-
// cross-referencing latency.log) tells them apart; `bindingState`
|
|
1747
|
-
// stays conditional since "bound" carries no extra signal.
|
|
1748
|
-
metadata: {
|
|
1749
|
-
inconclusive,
|
|
1750
|
-
// #1549: WHICH primary and WHICH deadline, present only when the
|
|
1751
|
-
// touch actually reported itself inconclusive.
|
|
1752
|
-
...(inconclusive && readInconclusiveAttribution(rawDiags)),
|
|
1753
|
-
...(bindingRejected && {
|
|
1754
|
-
bindingState: bindingStateLabel(false),
|
|
1755
|
-
}),
|
|
1756
|
-
...(unconfirmedServerIds.length > 0 && {
|
|
1757
|
-
unconfirmedServerIds: [...unconfirmedServerIds],
|
|
1758
|
-
}),
|
|
1759
|
-
},
|
|
1760
|
-
});
|
|
1761
|
-
// #1093/#1095: a completed, CONFIRMED active touch is a confirmed
|
|
1762
|
-
// observation of this neighbor's current LSP-error state (#571) —
|
|
1763
|
-
// reconcile it into the footer widget, INCLUDING the confirmed-clean `[]`
|
|
1764
|
-
// case, so a fix-edit to the primary that resolves a cross-file error in
|
|
1765
|
-
// this neighbor clears the neighbor's now-stale footer entry (the #1092
|
|
1766
|
-
// defect). MERGE (genuine LSP errors only — auxiliary findings excluded,
|
|
1767
|
-
// see `cascadeReconcilableLspErrors`) so a live biome/ruff/aux finding or
|
|
1768
|
-
// LSP warning survives this errors-only re-check. Keyed by the primary
|
|
1769
|
-
// edit's `writeSeq` so a genuinely newer per-edit write still wins the
|
|
1770
|
-
// WriteOrderingGuard. `observedAt` stays now (a fresh touch). The
|
|
1771
|
-
// inconclusive touch, the BOUND-FALSE touch (#1095 — computed against a
|
|
1772
|
-
// diverged disk state), the tier-3-silent skip, the recently-clean
|
|
1773
|
-
// short-circuit, the within-turn cache hit, and the rejected-touch
|
|
1774
|
-
// fallback are all deliberately NOT reconciled — none is a confirmed
|
|
1775
|
-
// observation.
|
|
1776
|
-
if (confirmed) {
|
|
1777
|
-
reconcileCascadeNeighborLspErrors(neighborPath, cascadeReconcilableLspErrors(rawDiags.diags, neighborPath), writeSeq);
|
|
1778
|
-
}
|
|
1779
|
-
return {
|
|
1780
|
-
filePath: neighborPath,
|
|
1781
|
-
reason: neighborReason(importerSet, callerSet, neighborPath),
|
|
1782
|
-
diagnostics: diags,
|
|
1783
|
-
lspTouched: true,
|
|
1784
|
-
...(inconclusive && { inconclusive: true }),
|
|
1785
|
-
// #1459: carry the coverage gap to the agent-facing surface. A
|
|
1786
|
-
// scanner whose breaker was open, whose resync the fan-out gate
|
|
1787
|
-
// deferred, or whom the aux grace timer cut off never looked at this
|
|
1788
|
-
// file — without this the neighbour reaches the agent as
|
|
1789
|
-
// `{ diagnostics: [] }` with no marker, which is the false-clean the
|
|
1790
|
-
// touch already refuses to claim for itself.
|
|
1791
|
-
...(unconfirmedServerIds.length > 0 && {
|
|
1792
|
-
unconfirmedServerIds: [...unconfirmedServerIds],
|
|
1793
|
-
}),
|
|
1794
|
-
durationMs,
|
|
1795
|
-
};
|
|
1796
|
-
}));
|
|
1797
|
-
const allTouchPaths = [...activePaths, ...coldSnapshotPaths];
|
|
1798
|
-
for (let i = 0; i < touchResults.length; i++) {
|
|
1799
|
-
const result = touchResults[i];
|
|
1800
|
-
const neighborPath = allTouchPaths[i];
|
|
1801
|
-
if (result.status === "fulfilled") {
|
|
1802
|
-
if (result.value)
|
|
1803
|
-
neighbors.push(result.value);
|
|
1804
|
-
}
|
|
1805
|
-
else {
|
|
1806
|
-
touchFailures++;
|
|
1807
|
-
// A3: one failed LSP doesn't kill the rest — fall back to passive snapshot
|
|
1808
|
-
dbg?.(`cascade neighbor touch error for ${neighborPath}: ${result.reason}`);
|
|
1809
|
-
const entry = allDiags.get(normalizeMapKey(neighborPath));
|
|
1810
|
-
const ttlFresh = entry != null && Date.now() - entry.ts < CASCADE_DIAGNOSTICS_TTL_MS;
|
|
1811
|
-
// #1104: consult binding before trusting a TTL-fresh fallback snapshot —
|
|
1812
|
-
// MATCH #1100/#1095 semantics (false → skip, "unknown" → keep the
|
|
1813
|
-
// pre-#1104 TTL-only behavior, true → use). Without this, a failed
|
|
1814
|
-
// active touch could still re-display a bound-false (pre-fix-edit)
|
|
1815
|
-
// snapshot even though the reconcile path (#1100) already refuses to
|
|
1816
|
-
// trust it for the widget — the widget is protected but the display
|
|
1817
|
-
// wasn't. Reading `.binding` triggers the lazy disk verify — done ONLY
|
|
1818
|
-
// when TTL-fresh, same discipline as the snapshot-tier gate above.
|
|
1819
|
-
const boundToDisk = ttlFresh
|
|
1820
|
-
? readBoundToCurrentDisk(entry)
|
|
1821
|
-
: undefined;
|
|
1822
|
-
const bindingRejected = boundToDisk === false;
|
|
1823
|
-
if (bindingRejected)
|
|
1824
|
-
fallbackBindingRejected = true;
|
|
1825
|
-
logCascade({
|
|
1826
|
-
phase: "neighbor_fallback",
|
|
1827
|
-
filePath,
|
|
1828
|
-
neighborFile: neighborPath,
|
|
1829
|
-
fallbackUsed: true,
|
|
1830
|
-
error: String(result.reason),
|
|
1831
|
-
// #1104: distinguish a binding-rejected fallback (TTL-fresh but the
|
|
1832
|
-
// server's view diverged from disk) from a plain TTL-stale/missing
|
|
1833
|
-
// one — same conditional pattern as the neighbor_touch/neighbor_snapshot
|
|
1834
|
-
// phases above.
|
|
1835
|
-
...(bindingRejected && {
|
|
1836
|
-
metadata: { bindingState: bindingStateLabel(boundToDisk) },
|
|
1837
|
-
}),
|
|
1838
|
-
});
|
|
1839
|
-
// #692: `source: "cascade"` dropped (see the doc comment above the
|
|
1840
|
-
// first cascade call site in this file) — no longer affects `rule`
|
|
1841
|
-
// and cascade output never touches persisted widget/dedup state.
|
|
1842
|
-
// #3157 site 3 of 4: the touch-error fallback. The touch failed, so
|
|
1843
|
-
// no content was read for this neighbour — the helper reads it.
|
|
1844
|
-
const diags = ttlFresh && !bindingRejected
|
|
1845
|
-
? cascadeDisplay(entry.diags, neighborPath)
|
|
1846
|
-
: [];
|
|
1847
|
-
neighbors.push({
|
|
1848
|
-
filePath: neighborPath,
|
|
1849
|
-
reason: "fallback",
|
|
1850
|
-
diagnostics: diags,
|
|
1851
|
-
lspTouched: false,
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
// CR-3/A2: degraded fallback when no neighbor produced trustworthy LSP data —
|
|
1857
|
-
// not merely when the graph returned zero neighbors. In a mixed run, only
|
|
1858
|
-
// candidates that had no configured LSP may use passive fallback; broadening
|
|
1859
|
-
// to unrelated cache entries would make the partial active result misleading.
|
|
1860
|
-
if (producedLspData && noLspCandidatePaths.size > 0) {
|
|
1861
|
-
const bindingRejected = appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath, cascadeDisplay, noLspCandidatePaths);
|
|
1862
|
-
if (bindingRejected)
|
|
1863
|
-
fallbackBindingRejected = true;
|
|
1864
|
-
}
|
|
1865
|
-
else if (!producedLspData) {
|
|
1866
|
-
const bindingRejected = appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath, cascadeDisplay);
|
|
1867
|
-
if (bindingRejected)
|
|
1868
|
-
fallbackBindingRejected = true;
|
|
1869
|
-
if (neighbors.some((n) => n.reason === "fallback")) {
|
|
1870
|
-
logCascade({
|
|
1871
|
-
phase: "neighbor_fallback",
|
|
1872
|
-
filePath,
|
|
1873
|
-
fallbackUsed: true,
|
|
1874
|
-
neighborCount: neighbors.length,
|
|
1875
|
-
});
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
// #3157: ONE row per cascade run, covering every neighbour the four display
|
|
1879
|
-
// sites filtered — never one per neighbour. `durationMs` is the stack's own
|
|
1880
|
-
// accumulated time (the content reads the fold added included), NOT the
|
|
1881
|
-
// neighbour walk it runs inside, so this row stays comparable with the
|
|
1882
|
-
// quiet-window lane's row under the same phase literal (round 2, F2).
|
|
1883
|
-
// Silent when nothing was dropped or truncated; gated inside
|
|
1884
|
-
// `recordCascadeFindingPolicy`.
|
|
1885
|
-
recordCascadeFindingPolicy({
|
|
1886
|
-
filePath,
|
|
1887
|
-
durationMs: policyElapsedMs,
|
|
1888
|
-
suppressed: policyCounts.suppressed,
|
|
1889
|
-
total: policyCounts.total,
|
|
1890
|
-
auxSuppressed: policyCounts.auxSuppressed,
|
|
1891
|
-
inputTruncated: policyCounts.inputTruncated,
|
|
1892
|
-
});
|
|
1893
|
-
const visibleNeighbors = applyCascadeDeltaBaselines(neighbors);
|
|
1894
|
-
const formatted = formatCascadeResult(cwd, impact, visibleNeighbors, impact.neighborFiles.length, policyCounts.suppressed, policyCounts.inputTruncated);
|
|
1895
|
-
// #1104 HONESTY: filtering a bound-false display candidate must not turn a
|
|
1896
|
-
// degraded/indeterminate cascade into a clean-looking one (same doctrine as
|
|
1897
|
-
// #1023's graph-degraded marker). If every candidate the degraded-fallback
|
|
1898
|
-
// paths considered this run was binding-rejected and nothing else produced
|
|
1899
|
-
// output, thread the SAME indeterminate marker #1023 built so the turn-end
|
|
1900
|
-
// advisory (clients/runtime-turn.ts) surfaces an honest note instead of
|
|
1901
|
-
// silence — never let a withheld-stale-snapshot run look like a genuine
|
|
1902
|
-
// clean leaf. `!impact.indeterminate` preserves a graph-degraded marker that
|
|
1903
|
-
// already exists (never overwritten).
|
|
1904
|
-
if (!formatted && fallbackBindingRejected && !impact.indeterminate) {
|
|
1905
|
-
impact.indeterminate = {
|
|
1906
|
-
reason: "lsp_binding_rejected",
|
|
1907
|
-
detail: "cascade fallback diagnostics were withheld — stale snapshot content did not match current disk (binding rejected)",
|
|
1908
|
-
};
|
|
1909
|
-
}
|
|
1910
|
-
const filesWithErrors = visibleNeighbors.filter((n) => n.diagnostics.length > 0).length;
|
|
1911
|
-
const selectedOutcomeCount = passiveSnapshotHits +
|
|
1912
|
-
recentlyCleanHits +
|
|
1913
|
-
deferredTouches +
|
|
1914
|
-
coldTouches +
|
|
1915
|
-
noLspConfigured;
|
|
1916
|
-
const selectedOutcomeGap = Math.max(0, sortedNeighbors.length - selectedOutcomeCount);
|
|
1917
|
-
logCascade({
|
|
1918
|
-
phase: "cascade_result",
|
|
1919
|
-
filePath,
|
|
1920
|
-
neighborCount: visibleNeighbors.length,
|
|
1921
|
-
diagnosticCount: visibleNeighbors.reduce((sum, n) => sum + n.diagnostics.length, 0),
|
|
1922
|
-
metadata: {
|
|
1923
|
-
filesWithErrors,
|
|
1924
|
-
hasOutput: formatted.length > 0,
|
|
1925
|
-
indeterminateReason: impact.indeterminate?.reason,
|
|
1926
|
-
candidateNeighborCount: cascadeCandidateCount,
|
|
1927
|
-
eligibleNeighborCount: cascadeEligibleCount,
|
|
1928
|
-
filterDroppedCount: cascadeFilterDroppedCount,
|
|
1929
|
-
selectedNeighborCount: sortedNeighbors.length,
|
|
1930
|
-
selectedOutcomeCount,
|
|
1931
|
-
selectedOutcomeGap,
|
|
1932
|
-
passiveSnapshotHits,
|
|
1933
|
-
noLspConfigured,
|
|
1934
|
-
touchFailures,
|
|
1935
|
-
// #1444: >0 means "answers are still outstanding", not "nothing found".
|
|
1936
|
-
collectLaterSkipped,
|
|
1937
|
-
// Log when cascade ran but found nothing — distinguishes "clean" from "no signal"
|
|
1938
|
-
noNeighbors: visibleNeighbors.length === 0,
|
|
1939
|
-
noErrors: visibleNeighbors.length > 0 && filesWithErrors === 0,
|
|
1940
|
-
// #1446 item 5: cache effectiveness as a number instead of an inference
|
|
1941
|
-
// from `coldSnapshot`/`snapshotMissing` flags scattered across
|
|
1942
|
-
// per-neighbor `neighbor_touch`/`neighbor_snapshot` rows.
|
|
1943
|
-
// F1: recentlyCleanHits + deferredTouches + coldTouches
|
|
1944
|
-
// retain their outcome semantics. Passive snapshots and no-server selections
|
|
1945
|
-
// complete the selected-neighbor denominator; touchFailures is a bounded
|
|
1946
|
-
// subtype of coldTouches, not an additional outcome.
|
|
1947
|
-
recentlyCleanHits,
|
|
1948
|
-
deferredTouches,
|
|
1949
|
-
coldTouches,
|
|
1950
|
-
// #1446 item 4: the budget in force and how many eligible candidates
|
|
1951
|
-
// it cut off this run — the correctness half (a truncated run being
|
|
1952
|
-
// silently discarded) is #1443.
|
|
1953
|
-
// #1462: `neighborBudget` is now the DERIVED budget (still "the one in
|
|
1954
|
-
// force"); `neighborBudgetCeiling` is the flat cap, `budgetRemainingMs`
|
|
1955
|
-
// the on-time window left when it was sized, and `budgetZone` the rule
|
|
1956
|
-
// that decided it. Only `budgetZone: "narrowed"` shortens a walk — the
|
|
1957
|
-
// other three keep the flat cap, so a row where budget < ceiling is a
|
|
1958
|
-
// deliberate rescue, never a cold-start stub. `budgetDeliveryWindowMs`
|
|
1959
|
-
// records how long the pipeline (turn_end settle + quiet-window drain)
|
|
1960
|
-
// keeps a slow run alive, which is why the other zones can afford to
|
|
1961
|
-
// stay wide.
|
|
1962
|
-
//
|
|
1963
|
-
// NOTE for whoever reads these rows: `budgetTruncated` INVERTS under a
|
|
1964
|
-
// narrowed budget — it is `eligible - budget`, so a smaller budget
|
|
1965
|
-
// mechanically raises it. Compare truncation WITHIN a zone, never
|
|
1966
|
-
// across one.
|
|
1967
|
-
neighborBudget: cascadeNeighbourBudget,
|
|
1968
|
-
neighborBudgetCeiling: CASCADE_NEIGHBOUR_BUDGET,
|
|
1969
|
-
budgetRemainingMs: cascadeBudgetRemainingMs,
|
|
1970
|
-
budgetZone: cascadeBudgetZone,
|
|
1971
|
-
budgetDeliveryWindowMs: cascadeDeliveryWindowMs,
|
|
1972
|
-
budgetTruncated: cascadeBudgetTruncated,
|
|
1973
|
-
transitiveTruncated,
|
|
1974
|
-
budgetTrimmedCount: cascadeBudgetTruncated,
|
|
1975
|
-
budgetDerivationDisabled: cascadeBudgetZone === "no-rescue-window",
|
|
1976
|
-
budgetDerivationDisabledReason: cascadeBudgetZone === "no-rescue-window"
|
|
1977
|
-
? "no-rescue-window"
|
|
1978
|
-
: undefined,
|
|
1979
|
-
neighbors: visibleNeighbors.slice(0, 10).map((n) => ({
|
|
1980
|
-
file: n.filePath.replace(/\\/g, "/").split("/").slice(-2).join("/"),
|
|
1981
|
-
diagnostics: n.diagnostics.length,
|
|
1982
|
-
})),
|
|
1983
|
-
},
|
|
1984
|
-
});
|
|
1985
|
-
const diagCount = visibleNeighbors.reduce((sum, n) => sum + n.diagnostics.length, 0);
|
|
1986
|
-
cascadeSessionStats.runs += 1;
|
|
1987
|
-
cascadeSessionStats.diagnosticsSurfaced += diagCount;
|
|
1988
|
-
cascadeSessionStats.coldSnapshotTouches += coldSnapshotPaths.length;
|
|
1989
|
-
if (!formatted) {
|
|
1990
|
-
// #1023: an indeterminate compute (degraded/cold/missing-node graph) must
|
|
1991
|
-
// NOT collapse into "no_neighbors" — that is the exact silent all-clear the
|
|
1992
|
-
// bug is about. Distinguish it by the marker threaded onto `impact`, never
|
|
1993
|
-
// by `visibleNeighbors.length === 0` alone (a healthy leaf is also empty).
|
|
1994
|
-
if (impact.indeterminate) {
|
|
1995
|
-
return {
|
|
1996
|
-
filePath,
|
|
1997
|
-
result: undefined,
|
|
1998
|
-
neighborCount: visibleNeighbors.length,
|
|
1999
|
-
diagnosticCount: diagCount,
|
|
2000
|
-
skipReason: "indeterminate",
|
|
2001
|
-
indeterminate: impact.indeterminate,
|
|
2002
|
-
selectedNeighborPaths: sortedNeighbors.slice(),
|
|
2003
|
-
};
|
|
2004
|
-
}
|
|
2005
|
-
const skipReason = visibleNeighbors.length === 0 ? "no_neighbors" : "clean";
|
|
2006
|
-
return {
|
|
2007
|
-
filePath,
|
|
2008
|
-
result: undefined,
|
|
2009
|
-
neighborCount: visibleNeighbors.length,
|
|
2010
|
-
diagnosticCount: diagCount,
|
|
2011
|
-
skipReason,
|
|
2012
|
-
};
|
|
2013
|
-
}
|
|
2014
|
-
getDiagnosticTracker().trackShown(visibleNeighbors.flatMap((n) => n.diagnostics));
|
|
2015
|
-
return {
|
|
2016
|
-
filePath,
|
|
2017
|
-
result: { filePath, impact, neighbors: visibleNeighbors, formatted },
|
|
2018
|
-
neighborCount: visibleNeighbors.length,
|
|
2019
|
-
diagnosticCount: diagCount,
|
|
2020
|
-
// #1023: even when some fallback neighbors surfaced, a degraded graph means
|
|
2021
|
-
// the dependent set is INCOMPLETE — carry the marker so the turn-end seam
|
|
2022
|
-
// still notes downstream impact was under-computed this turn.
|
|
2023
|
-
...(impact.indeterminate && { indeterminate: impact.indeterminate }),
|
|
2024
|
-
};
|
|
2025
|
-
}
|
|
2026
|
-
finally {
|
|
2027
|
-
// Keep the cache entry warm, but do not let a one-shot cascade leave an
|
|
2028
|
-
// idle handle behind. The next consumer re-arms it through touch.
|
|
2029
|
-
for (const key of reverseDepsTimersToRelease) {
|
|
2030
|
-
const entry = reverseDepsIndexCache.get(key);
|
|
2031
|
-
if (entry?.idleTimer !== undefined)
|
|
2032
|
-
clearTimeout(entry.idleTimer);
|
|
2033
|
-
if (entry)
|
|
2034
|
-
entry.idleTimer = undefined;
|
|
2035
|
-
}
|
|
2036
|
-
for (const [key, entry] of reverseDepsIndexCache) {
|
|
2037
|
-
if (entry.idleTimer !== undefined)
|
|
2038
|
-
clearTimeout(entry.idleTimer);
|
|
2039
|
-
entry.idleTimer = undefined;
|
|
2040
|
-
if (!reverseDepsTimersToRelease.has(key) &&
|
|
2041
|
-
reverseDepsEntriesAtStart.has(entry)) {
|
|
2042
|
-
reverseDepsTimersToRelease.add(key);
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
for (const timer of astGrepWarnDebounceTimers.values())
|
|
2046
|
-
clearTimeout(timer);
|
|
2047
|
-
astGrepWarnDebounceTimers.clear();
|
|
2048
|
-
releaseWorkspaceTopologyIdleTimers();
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
function diagnosticDeltaKey(diagnostic) {
|
|
2052
|
-
return [
|
|
2053
|
-
diagnostic.id,
|
|
2054
|
-
diagnostic.rule ?? "",
|
|
2055
|
-
diagnostic.line ?? 0,
|
|
2056
|
-
diagnostic.column ?? 0,
|
|
2057
|
-
diagnostic.message,
|
|
2058
|
-
].join(":");
|
|
2059
|
-
}
|
|
2060
|
-
function applyCascadeDeltaBaselines(neighbors) {
|
|
2061
|
-
return neighbors.map((neighbor) => {
|
|
2062
|
-
const baselineKey = `session.baseline.cascade.${normalizeMapKey(neighbor.filePath)}`;
|
|
2063
|
-
const previous = sessionFacts.getBoundedSessionFact(baselineKey);
|
|
2064
|
-
sessionFacts.setBoundedSessionFact(baselineKey, [...neighbor.diagnostics]);
|
|
2065
|
-
if (!previous)
|
|
2066
|
-
return neighbor;
|
|
2067
|
-
const before = new Set(previous.map(diagnosticDeltaKey));
|
|
2068
|
-
return {
|
|
2069
|
-
...neighbor,
|
|
2070
|
-
diagnostics: neighbor.diagnostics.filter((diagnostic) => !before.has(diagnosticDeltaKey(diagnostic))),
|
|
2071
|
-
};
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
|
-
/**
|
|
2075
|
-
* Returns `true` when at least one otherwise-eligible candidate was withheld
|
|
2076
|
-
* because its content binding was rejected (#1104) — the caller folds this
|
|
2077
|
-
* into the run-level `fallbackBindingRejected` flag for the honesty check.
|
|
2078
|
-
*/
|
|
2079
|
-
function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath,
|
|
2080
|
-
/** The run's shared display-policy seam (#3157) — see `cascadeDisplay` in
|
|
2081
|
-
* `computeCascadeForFile`. */
|
|
2082
|
-
cascadeDisplay, allowedPaths) {
|
|
2083
|
-
const now = Date.now();
|
|
2084
|
-
const seen = new Set(neighbors.map((n) => normalizeMapKey(n.filePath)));
|
|
2085
|
-
let bindingRejected = false;
|
|
2086
|
-
for (const [diagPath, entry] of allDiags) {
|
|
2087
|
-
const { diags, ts } = entry;
|
|
2088
|
-
const diagKey = normalizeMapKey(diagPath);
|
|
2089
|
-
if (diagKey === normalizedFileKey || seen.has(diagKey))
|
|
2090
|
-
continue;
|
|
2091
|
-
if (allowedPaths && !allowedPaths.has(diagKey))
|
|
2092
|
-
continue;
|
|
2093
|
-
if (primaryFilesThisTurn.has(diagKey))
|
|
2094
|
-
continue;
|
|
2095
|
-
if (isExternalOrVendorFile(diagPath, cwd))
|
|
2096
|
-
continue;
|
|
2097
|
-
if (isIgnoredCascadeNeighbor(diagPath, cwd))
|
|
2098
|
-
continue;
|
|
2099
|
-
// #1080: a KNOWN test-role file must not surface as a collateral fallback
|
|
2100
|
-
// neighbor either (the passive-snapshot path the graph/reference filters
|
|
2101
|
-
// above never reach). Ignore filtering (#297) stays separate and above.
|
|
2102
|
-
if (isTestRoleCollateral(diagPath))
|
|
2103
|
-
continue;
|
|
2104
|
-
if (!nodeFs.existsSync(diagPath))
|
|
2105
|
-
continue;
|
|
2106
|
-
if (now - ts > CASCADE_DIAGNOSTICS_TTL_MS)
|
|
2107
|
-
continue;
|
|
2108
|
-
// #1104: a TTL-fresh entry is not automatically trustworthy — consult
|
|
2109
|
-
// binding the same way the reconcile path (#1100) and the touch-error
|
|
2110
|
-
// fallback above do. `false` → skip (a stale/pre-fix-edit snapshot whose
|
|
2111
|
-
// server view diverged from current disk); "unknown"/`true` → unchanged
|
|
2112
|
-
// (the pre-#1104 fallback contract). Reading `.binding` triggers the lazy
|
|
2113
|
-
// disk verify — done ONLY when TTL-fresh, per the established discipline.
|
|
2114
|
-
const boundToDisk = readBoundToCurrentDisk(entry);
|
|
2115
|
-
if (boundToDisk === false) {
|
|
2116
|
-
bindingRejected = true;
|
|
2117
|
-
logCascade({
|
|
2118
|
-
phase: "neighbor_fallback",
|
|
2119
|
-
filePath,
|
|
2120
|
-
neighborFile: diagPath,
|
|
2121
|
-
fallbackUsed: false,
|
|
2122
|
-
metadata: { bindingState: bindingStateLabel(false) },
|
|
2123
|
-
});
|
|
2124
|
-
continue;
|
|
2125
|
-
}
|
|
2126
|
-
// #692: `source: "cascade"` dropped — see the doc comment above the
|
|
2127
|
-
// first cascade display call site in this file.
|
|
2128
|
-
// #3157 site 4 of 4: the degraded fallback. `cascadeDisplay` is passed in
|
|
2129
|
-
// rather than re-derived so this run's aggregate drop counters see it too.
|
|
2130
|
-
const errors = cascadeDisplay(diags, diagPath);
|
|
2131
|
-
if (errors.length === 0)
|
|
2132
|
-
continue;
|
|
2133
|
-
neighbors.push({
|
|
2134
|
-
filePath: diagPath,
|
|
2135
|
-
reason: "fallback",
|
|
2136
|
-
diagnostics: errors,
|
|
2137
|
-
lspTouched: false,
|
|
2138
|
-
});
|
|
2139
|
-
seen.add(diagKey);
|
|
2140
|
-
if (neighbors.length >= MAX_FILES)
|
|
2141
|
-
break;
|
|
2142
|
-
}
|
|
2143
|
-
return bindingRejected;
|
|
2144
|
-
}
|
|
2145
|
-
function shouldReadCascadeFromSnapshot(filePath) {
|
|
2146
|
-
return getServersForFileWithConfig(filePath).some((server) => server.autoPropagateDiagnostics === true);
|
|
2147
|
-
}
|
|
2148
|
-
function neighborReason(importerSet, callerSet, neighborPath) {
|
|
2149
|
-
if (importerSet.has(neighborPath))
|
|
2150
|
-
return "imports";
|
|
2151
|
-
if (callerSet.has(neighborPath))
|
|
2152
|
-
return "calls";
|
|
2153
|
-
return "references";
|
|
2154
|
-
}
|
|
2155
|
-
function formatCascadeResult(cwd, impact, neighbors, totalNeighbors,
|
|
2156
|
-
/** This run's aggregate policy-stack drops (#3157). */
|
|
2157
|
-
policySuppressed,
|
|
2158
|
-
/**
|
|
2159
|
-
* This run's aggregate `MAX_POLICY_INPUT_PER_FILE` truncation (#3157 round
|
|
2160
|
-
* 3, N1) — ERRORs the policy never LOOKED at.
|
|
2161
|
-
*/
|
|
2162
|
-
policyInputTruncated) {
|
|
2163
|
-
const diagnosticsBlock = formatCascadeNeighborDiagnostics(cwd, neighbors, {
|
|
2164
|
-
noun: "neighbor",
|
|
2165
|
-
includeReason: true,
|
|
2166
|
-
});
|
|
2167
|
-
// #3157 round 3, N1: an empty block is silence, and silence is a CLEAN
|
|
2168
|
-
// verdict to the agent. That is honest only when everything was looked at.
|
|
2169
|
-
// `policyInputTruncated > 0` means this run's own cost bound declined to
|
|
2170
|
-
// evaluate findings that exist — the #1459 coverage-gap case, not a verdict —
|
|
2171
|
-
// so it must speak even with nothing to render. `policySuppressed` does NOT
|
|
2172
|
-
// get the same treatment on purpose: those findings WERE evaluated and every
|
|
2173
|
-
// drop is the agent's own mark or its project's own rule policy, and the
|
|
2174
|
-
// quiet-window lane deliberately says nothing at all for that case
|
|
2175
|
-
// (`buildResolvedFoundCascadeRun` returns `undefined`). Speaking here and not
|
|
2176
|
-
// there would re-open the two-lane divergence #3157 exists to close.
|
|
2177
|
-
if (!diagnosticsBlock && policyInputTruncated === 0)
|
|
2178
|
-
return "";
|
|
2179
|
-
const impactHeader = formatImpactCascade(impact, RUNTIME_CONFIG.pipeline.cascadeMaxFiles);
|
|
2180
|
-
let out = !diagnosticsBlock
|
|
2181
|
-
? ""
|
|
2182
|
-
: impactHeader
|
|
2183
|
-
? `${impactHeader}\n${diagnosticsBlock}`
|
|
2184
|
-
: diagnosticsBlock;
|
|
2185
|
-
// A10: include truncated filenames so agent knows which files were cut
|
|
2186
|
-
const truncated = totalNeighbors - neighbors.length;
|
|
2187
|
-
if (truncated > 0) {
|
|
2188
|
-
const truncatedNames = impact.neighborFiles
|
|
2189
|
-
.slice(neighbors.length, neighbors.length + 3)
|
|
2190
|
-
.map((p) => toRunnerDisplayPath(cwd, p))
|
|
2191
|
-
.join(", ");
|
|
2192
|
-
const moreLabel = truncatedNames
|
|
2193
|
-
? `${truncated} more dependent file(s): ${truncatedNames}`
|
|
2194
|
-
: `${truncated} more dependent file(s)`;
|
|
2195
|
-
out += `\n... and ${moreLabel}`;
|
|
2196
|
-
}
|
|
2197
|
-
// #1616 / #3157: a delivery that still has something to say states what it
|
|
2198
|
-
// dropped, once per delivery — the same sentence the quiet-window cascade run
|
|
2199
|
-
// (`buildResolvedFoundCascadeRun`) and the late-auxiliary advisory render, so
|
|
2200
|
-
// the two cascade lanes do not disagree about whether a mark is worth
|
|
2201
|
-
// mentioning. Policy drops only: an aux drop is the file's own suppression
|
|
2202
|
-
// comment, which the per-edit dispatch path honours silently too, and it stays
|
|
2203
|
-
// in the `cascade_finding_policy` record. A run with NOTHING left returned ""
|
|
2204
|
-
// above and says nothing at all — silence is not a claim that a neighbour is
|
|
2205
|
-
// clean.
|
|
2206
|
-
// #3157 round 3, N1: the same shape and the same closing clause as the
|
|
2207
|
-
// "did not cover" line this file's renderer already uses for a neighbour no
|
|
2208
|
-
// scanner looked at (`clients/cascade-format.ts`) — the agent must not read
|
|
2209
|
-
// an empty or short block as a clean verdict when the bound cut findings
|
|
2210
|
-
// nobody evaluated.
|
|
2211
|
-
if (policyInputTruncated > 0) {
|
|
2212
|
-
if (out)
|
|
2213
|
-
out += "\n";
|
|
2214
|
-
out +=
|
|
2215
|
-
`⚠️ Cascade did not evaluate ${policyInputTruncated} finding(s) — the per-neighbor ` +
|
|
2216
|
-
`policy input is bounded at ${MAX_POLICY_INPUT_PER_FILE}; no findings does NOT mean clean here.`;
|
|
2217
|
-
}
|
|
2218
|
-
// Rides along whenever anything is rendered at all: when the block is empty
|
|
2219
|
-
// and the coverage line above is speaking, this is what explains WHY it is
|
|
2220
|
-
// empty. The empty-and-nothing-truncated case returned above.
|
|
2221
|
-
if (policySuppressed > 0) {
|
|
2222
|
-
out += `\nsuppressed by disposition: ${policySuppressed} finding(s) (marked false-positive or won't-fix).`;
|
|
2223
|
-
}
|
|
2224
|
-
return out;
|
|
2225
|
-
}
|
|
2226
|
-
/**
|
|
2227
|
-
* Run linting for a file using the declarative dispatch system
|
|
2228
|
-
*
|
|
2229
|
-
* @param filePath - Path to the file to lint
|
|
2230
|
-
* @param cwd - Project root directory
|
|
2231
|
-
* @param pi - Pi agent API (for flags)
|
|
2232
|
-
* @returns Output string to display to user
|
|
2233
|
-
*/
|
|
2234
|
-
export async function dispatchLint(filePath, cwd, pi, modifiedRanges, projectRoot) {
|
|
2235
|
-
// By default, only run BLOCKING rules for fast feedback on file write
|
|
2236
|
-
// Uses persistent sessionBaselines so delta mode actually filters
|
|
2237
|
-
// pre-existing issues after the first write.
|
|
2238
|
-
const ctx = createDispatchContext(filePath, cwd, pi, sessionFacts, true, modifiedRanges, projectRoot);
|
|
2239
|
-
sessionFacts.clearFileFactsFor(ctx.filePath);
|
|
2240
|
-
// #2243 item 2: release the pin when the dispatch settles, so the pin set
|
|
2241
|
-
// tracks in-flight dispatches rather than the last N files touched.
|
|
2242
|
-
try {
|
|
2243
|
-
const kind = ctx.kind;
|
|
2244
|
-
if (!kind)
|
|
2245
|
-
return "";
|
|
2246
|
-
const groups = withSpotbugsGroup(kind, getDispatchGroupsForKind(kind, pi), ctx);
|
|
2247
|
-
if (groups.length === 0)
|
|
2248
|
-
return "";
|
|
2249
|
-
await runProviders(ctx);
|
|
2250
|
-
const result = await dispatchForFile(ctx, groups, sessionRunnerRegistry);
|
|
2251
|
-
trackSessionSlopStats(ctx, result.diagnostics);
|
|
2252
|
-
return result.output;
|
|
2253
|
-
}
|
|
2254
|
-
finally {
|
|
2255
|
-
sessionFacts.endDispatchFor(ctx.filePath);
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
/**
|
|
2259
|
-
* Run linting and return full result (including diagnostics)
|
|
2260
|
-
*/
|
|
2261
|
-
export async function dispatchLintWithResult(filePath, cwd, pi, modifiedRanges, logContext, options) {
|
|
2262
|
-
// Default true preserves the per-edit fast path (errors only). Callers that
|
|
2263
|
-
// want the full picture (warnings + structural smells), e.g. the MCP review
|
|
2264
|
-
// facade, pass blockingOnly=false to run every runner.
|
|
2265
|
-
const ctx = createDispatchContext(filePath, cwd, pi, sessionFacts, options?.blockingOnly ?? true, modifiedRanges, options?.projectRoot, options?.writeIndex, options?.telemetryModel, options?.telemetryProvider);
|
|
2266
|
-
sessionFacts.clearFileFactsFor(ctx.filePath);
|
|
2267
|
-
// #2243 item 2: release the pin when the dispatch settles.
|
|
2268
|
-
try {
|
|
2269
|
-
const kind = ctx.kind;
|
|
2270
|
-
if (!kind) {
|
|
2271
|
-
return {
|
|
2272
|
-
diagnostics: [],
|
|
2273
|
-
blockers: [],
|
|
2274
|
-
warnings: [],
|
|
2275
|
-
baselineWarningCount: 0,
|
|
2276
|
-
fixed: [],
|
|
2277
|
-
resolvedCount: 0,
|
|
2278
|
-
output: "",
|
|
2279
|
-
blockerOutput: "",
|
|
2280
|
-
hasBlockers: false,
|
|
2281
|
-
};
|
|
2282
|
-
}
|
|
2283
|
-
const groups = withSpotbugsGroup(kind, getDispatchGroupsForKind(kind, pi), ctx);
|
|
2284
|
-
if (groups.length === 0) {
|
|
2285
|
-
return {
|
|
2286
|
-
diagnostics: [],
|
|
2287
|
-
blockers: [],
|
|
2288
|
-
warnings: [],
|
|
2289
|
-
baselineWarningCount: 0,
|
|
2290
|
-
fixed: [],
|
|
2291
|
-
resolvedCount: 0,
|
|
2292
|
-
output: "",
|
|
2293
|
-
blockerOutput: "",
|
|
2294
|
-
hasBlockers: false,
|
|
2295
|
-
};
|
|
2296
|
-
}
|
|
2297
|
-
await runProviders(ctx);
|
|
2298
|
-
const result = await dispatchForFile(ctx, groups, sessionRunnerRegistry);
|
|
2299
|
-
trackSessionSlopStats(ctx, result.diagnostics);
|
|
2300
|
-
// Schedule debounced ast-grep warning scan for jsts files.
|
|
2301
|
-
// Runs 2s after the last write — collapses rapid sequential edits into one scan.
|
|
2302
|
-
// Results are logged only, never surfaced to the agent.
|
|
2303
|
-
if (kind === "jsts" && logContext) {
|
|
2304
|
-
scheduleAstGrepWarningScan(filePath, cwd, pi, logContext);
|
|
2305
|
-
}
|
|
2306
|
-
return result;
|
|
2307
|
-
}
|
|
2308
|
-
finally {
|
|
2309
|
-
sessionFacts.endDispatchFor(ctx.filePath);
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
/**
|
|
2313
|
-
* Same real dispatch path as {@link dispatchLintWithResult} (real context, real
|
|
2314
|
-
* file-kind→runner selection, real `run()` → spawn → tool), but also returns
|
|
2315
|
-
* each runner's exact `RunnerResult` (status + `failureKind` + diagnostics) via
|
|
2316
|
-
* the `onRunnerResult` sink. The live tool-smoke harness (#209) uses this to
|
|
2317
|
-
* assert each supported tool spawned and exited cleanly without re-implementing
|
|
2318
|
-
* dispatch's selection/gating. Defaults to `blockingOnly: false` so every
|
|
2319
|
-
* applicable runner (not just blocking ones) executes.
|
|
2320
|
-
*/
|
|
2321
|
-
export async function dispatchLintDetailed(filePath, cwd, pi, options) {
|
|
2322
|
-
const empty = {
|
|
2323
|
-
diagnostics: [],
|
|
2324
|
-
blockers: [],
|
|
2325
|
-
warnings: [],
|
|
2326
|
-
baselineWarningCount: 0,
|
|
2327
|
-
fixed: [],
|
|
2328
|
-
resolvedCount: 0,
|
|
2329
|
-
output: "",
|
|
2330
|
-
blockerOutput: "",
|
|
2331
|
-
hasBlockers: false,
|
|
2332
|
-
};
|
|
2333
|
-
const ctx = createDispatchContext(filePath, cwd, pi, sessionFacts, options?.blockingOnly ?? false, options?.modifiedRanges, options?.projectRoot);
|
|
2334
|
-
sessionFacts.clearFileFactsFor(ctx.filePath);
|
|
2335
|
-
// #2243 item 2: release the pin when the dispatch settles.
|
|
2336
|
-
try {
|
|
2337
|
-
const kind = ctx.kind;
|
|
2338
|
-
if (!kind)
|
|
2339
|
-
return { result: empty, runners: [] };
|
|
2340
|
-
const groups = withSpotbugsGroup(kind, getDispatchGroupsForKind(kind, pi), ctx);
|
|
2341
|
-
if (groups.length === 0)
|
|
2342
|
-
return { result: empty, runners: [] };
|
|
2343
|
-
const runners = [];
|
|
2344
|
-
const sink = (runnerId, result) => {
|
|
2345
|
-
runners.push({ runnerId, result });
|
|
2346
|
-
};
|
|
2347
|
-
await runProviders(ctx);
|
|
2348
|
-
const result = await dispatchForFile(ctx, groups, sessionRunnerRegistry, sink);
|
|
2349
|
-
trackSessionSlopStats(ctx, result.diagnostics);
|
|
2350
|
-
return { result, runners };
|
|
2351
|
-
}
|
|
2352
|
-
finally {
|
|
2353
|
-
sessionFacts.endDispatchFor(ctx.filePath);
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
/**
|
|
2357
|
-
* Check if a file should be processed by the dispatcher
|
|
2358
|
-
* based on the file kind
|
|
2359
|
-
*/
|
|
2360
|
-
export function shouldDispatch(filePath) {
|
|
2361
|
-
const kind = detectFileKind(filePath);
|
|
2362
|
-
return kind !== undefined;
|
|
2363
|
-
}
|
|
2364
|
-
/**
|
|
2365
|
-
* Get list of available runners for a file
|
|
2366
|
-
*/
|
|
2367
|
-
export async function getAvailableRunners(filePath) {
|
|
2368
|
-
const kind = detectFileKind(filePath);
|
|
2369
|
-
if (!kind)
|
|
2370
|
-
return [];
|
|
2371
|
-
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2372
|
-
const pathForFilter = normalizedPath.startsWith("/")
|
|
2373
|
-
? normalizedPath
|
|
2374
|
-
: `/${normalizedPath}`;
|
|
2375
|
-
const runners = sessionRunnerRegistry.getForKind(kind, pathForFilter);
|
|
2376
|
-
return runners.map((r) => r.id);
|
|
2377
|
-
}
|