@herbertgao/pi-extensions 2026.9.6 → 2026.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  19. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  20. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  21. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  22. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  23. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  24. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  25. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  26. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  27. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  28. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  29. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  30. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  31. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  32. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  33. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  34. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  35. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  36. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  44. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  45. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  46. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  47. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  48. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  49. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  50. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  51. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  52. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  53. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  54. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  55. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  56. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  57. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  58. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  59. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  61. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  62. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  63. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  64. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  66. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  67. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  68. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  69. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  70. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  72. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  73. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  74. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  75. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  76. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  77. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  78. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  79. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  81. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  82. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  83. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  84. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  85. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  86. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  87. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  88. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  89. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  90. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  91. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  92. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  93. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  94. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  95. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  96. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  97. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  98. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  99. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  100. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  101. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  102. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  103. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  104. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  105. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  106. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  107. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  108. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  109. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  110. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  111. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  112. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  113. package/node_modules/pi-lens/dist/index.js +27416 -24153
  114. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  115. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  116. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  117. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  118. package/node_modules/pi-lens/docs/audit1.md +5 -5
  119. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  120. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  121. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  122. package/node_modules/pi-lens/docs/features.md +4 -4
  123. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  124. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  125. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  126. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  127. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  128. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  129. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  130. package/node_modules/pi-lens/docs/settings.md +7 -4
  131. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  132. package/node_modules/pi-lens/docs/usage.md +2 -0
  133. package/node_modules/pi-lens/package.json +1 -1
  134. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  135. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  136. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  137. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  138. package/package.json +5 -11
  139. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  140. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  141. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  142. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  143. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  144. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  145. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  146. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  147. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  148. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  149. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  150. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  151. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  152. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  153. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  154. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  155. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  156. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  157. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  158. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  159. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  160. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  161. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  162. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  163. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  164. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  165. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  166. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  167. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  168. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  169. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  170. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  171. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  172. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  173. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  174. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  175. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  176. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  177. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  178. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  179. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  180. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  181. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  182. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  183. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  184. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  185. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  186. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  187. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  188. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  189. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  190. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  191. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  192. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  193. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  194. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  195. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  196. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  197. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  198. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  199. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  200. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  201. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  202. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  203. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  204. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  205. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  206. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  207. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  208. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  209. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  210. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  211. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  212. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  213. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  214. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  215. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  216. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  217. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  218. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  219. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  220. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  221. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  222. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  223. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  224. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  225. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  226. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  227. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  228. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  229. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  230. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  231. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  232. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  233. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  234. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  235. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  236. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  237. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  238. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  239. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  240. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -19,7 +19,7 @@ import * as nodeFs from "node:fs";
19
19
  import * as nodePath from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { CASCADE_NEIGHBOUR_BUDGET, deriveCascadeNeighbourBudget, } from "../cascade-budget.js";
22
- import { formatCascadeNeighborDiagnostics } from "../cascade-format.js";
22
+ import { applyCascadeDisplayPolicy, formatCascadeNeighborDiagnostics, recordCascadeFindingPolicy, } from "../cascade-format.js";
23
23
  import { logCascade } from "../cascade-logger.js";
24
24
  import { getDiagnosticTracker } from "../diagnostic-tracker.js";
25
25
  import { classifyCascadeWaitTier, isTierAwareCascadeEnabled, recordOutstandingCascadeTouch, } from "../lsp/cascade-tier.js";
@@ -434,6 +434,25 @@ function ensureCascadeTurnScope(turnSeq) {
434
434
  }
435
435
  const MAX_PER_FILE = RUNTIME_CONFIG.pipeline.cascadeMaxDiagnosticsPerFile;
436
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;
437
456
  /**
438
457
  * The genuine language-server ERROR diagnostics from a cascade neighbor
439
458
  * re-check, ready to reconcile into the footer widget (#1093).
@@ -455,8 +474,14 @@ const MAX_FILES = RUNTIME_CONFIG.pipeline.cascadeMaxFiles;
455
474
  * entries untouched. This keeps `isLspErrorEntry`'s "tool === 'lsp' uniquely
456
475
  * identifies a genuine language-server entry" contract true for what we write.
457
476
  *
458
- * The DISPLAY list (`diags` at each call site) is deliberately left as-is
459
- * that is pre-existing cascade output behavior, out of scope for #1093.
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.
460
485
  */
461
486
  function cascadeReconcilableLspErrors(rawDiags, neighborPath) {
462
487
  return convertLspDiagnostics(rawDiags
@@ -732,7 +757,7 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
732
757
  // time that elapsed before turn_end began waiting.
733
758
  const cascadeStart = Date.now();
734
759
  try {
735
- const { hasBlockers = false, dbg, turnSeq = 0, writeSeq, seqState, turnEndCascadeSettleStart, fileContent, wordIndex, onWordIndexUpdated, } = options;
760
+ const { hasBlockers = false, dbg, turnSeq = 0, writeSeq, projectRoot, seqState, turnEndCascadeSettleStart, fileContent, wordIndex, onWordIndexUpdated, } = options;
736
761
  ensureCascadeTurnScope(turnSeq);
737
762
  if (hasBlockers) {
738
763
  logCascade({
@@ -1311,6 +1336,64 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1311
1336
  let passiveSnapshotHits = 0;
1312
1337
  let noLspConfigured = 0;
1313
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
+ };
1314
1397
  if (sortedNeighbors.length > 0) {
1315
1398
  const snapshotPaths = sortedNeighbors.filter(shouldReadCascadeFromSnapshot);
1316
1399
  const activePaths = sortedNeighbors.filter((n) => !shouldReadCascadeFromSnapshot(n));
@@ -1371,7 +1454,8 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1371
1454
  // cascade neighbor diagnostics are ephemeral display-only output
1372
1455
  // (never reconciled into persisted widget/dedup state), so the label
1373
1456
  // had no remaining purpose and is simply dropped rather than migrated.
1374
- const diags = convertLspDiagnostics(entry.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
1457
+ // #3157 site 1 of 4: the passive cold-snapshot hit.
1458
+ const diags = cascadeDisplay(entry.diags, neighborPath);
1375
1459
  producedLspData = true;
1376
1460
  passiveSnapshotHits++;
1377
1461
  const durationMs = Date.now() - neighborStart;
@@ -1623,12 +1707,12 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1623
1707
  // doc comment on the sibling call above) — dropped rather than
1624
1708
  // migrated to `scanOrigin` since cascade output never touches
1625
1709
  // persisted widget/dedup state.
1626
- // #1179: `.filter()` here operates on `rawDiags.diags`; the
1627
- // `inconclusive`/`binding` flags read above stay on the `rawDiags`
1628
- // wrapper and are unaffected by this copy (the shape-5 fix).
1629
- const diags = convertLspDiagnostics(rawDiags.diags
1630
- .filter((d) => d.severity === 1)
1631
- .slice(0, MAX_PER_FILE), neighborPath);
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);
1632
1716
  const durationMs = Date.now() - neighborStart;
1633
1717
  // #1899: the A5 same-write cache write stood here.
1634
1718
  if (diags.length === 0) {
@@ -1755,10 +1839,10 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1755
1839
  // #692: `source: "cascade"` dropped (see the doc comment above the
1756
1840
  // first cascade call site in this file) — no longer affects `rule`
1757
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.
1758
1844
  const diags = ttlFresh && !bindingRejected
1759
- ? convertLspDiagnostics(entry.diags
1760
- .filter((d) => d.severity === 1)
1761
- .slice(0, MAX_PER_FILE), neighborPath)
1845
+ ? cascadeDisplay(entry.diags, neighborPath)
1762
1846
  : [];
1763
1847
  neighbors.push({
1764
1848
  filePath: neighborPath,
@@ -1774,12 +1858,12 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1774
1858
  // candidates that had no configured LSP may use passive fallback; broadening
1775
1859
  // to unrelated cache entries would make the partial active result misleading.
1776
1860
  if (producedLspData && noLspCandidatePaths.size > 0) {
1777
- const bindingRejected = appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath, noLspCandidatePaths);
1861
+ const bindingRejected = appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath, cascadeDisplay, noLspCandidatePaths);
1778
1862
  if (bindingRejected)
1779
1863
  fallbackBindingRejected = true;
1780
1864
  }
1781
1865
  else if (!producedLspData) {
1782
- const bindingRejected = appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath);
1866
+ const bindingRejected = appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath, cascadeDisplay);
1783
1867
  if (bindingRejected)
1784
1868
  fallbackBindingRejected = true;
1785
1869
  if (neighbors.some((n) => n.reason === "fallback")) {
@@ -1791,8 +1875,23 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1791
1875
  });
1792
1876
  }
1793
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
+ });
1794
1893
  const visibleNeighbors = applyCascadeDeltaBaselines(neighbors);
1795
- const formatted = formatCascadeResult(cwd, impact, visibleNeighbors, impact.neighborFiles.length);
1894
+ const formatted = formatCascadeResult(cwd, impact, visibleNeighbors, impact.neighborFiles.length, policyCounts.suppressed, policyCounts.inputTruncated);
1796
1895
  // #1104 HONESTY: filtering a bound-false display candidate must not turn a
1797
1896
  // degraded/indeterminate cascade into a clean-looking one (same doctrine as
1798
1897
  // #1023's graph-degraded marker). If every candidate the degraded-fallback
@@ -1977,7 +2076,10 @@ function applyCascadeDeltaBaselines(neighbors) {
1977
2076
  * because its content binding was rejected (#1104) — the caller folds this
1978
2077
  * into the run-level `fallbackBindingRejected` flag for the honesty check.
1979
2078
  */
1980
- function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, filePath, allowedPaths) {
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) {
1981
2083
  const now = Date.now();
1982
2084
  const seen = new Set(neighbors.map((n) => normalizeMapKey(n.filePath)));
1983
2085
  let bindingRejected = false;
@@ -2022,8 +2124,10 @@ function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd, fi
2022
2124
  continue;
2023
2125
  }
2024
2126
  // #692: `source: "cascade"` dropped — see the doc comment above the
2025
- // first cascade `convertLspDiagnostics` call site in this file.
2026
- const errors = convertLspDiagnostics(diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), diagPath);
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);
2027
2131
  if (errors.length === 0)
2028
2132
  continue;
2029
2133
  neighbors.push({
@@ -2048,17 +2152,36 @@ function neighborReason(importerSet, callerSet, neighborPath) {
2048
2152
  return "calls";
2049
2153
  return "references";
2050
2154
  }
2051
- function formatCascadeResult(cwd, impact, neighbors, totalNeighbors) {
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) {
2052
2163
  const diagnosticsBlock = formatCascadeNeighborDiagnostics(cwd, neighbors, {
2053
2164
  noun: "neighbor",
2054
2165
  includeReason: true,
2055
2166
  });
2056
- if (!diagnosticsBlock)
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)
2057
2178
  return "";
2058
2179
  const impactHeader = formatImpactCascade(impact, RUNTIME_CONFIG.pipeline.cascadeMaxFiles);
2059
- let out = impactHeader
2060
- ? `${impactHeader}\n${diagnosticsBlock}`
2061
- : diagnosticsBlock;
2180
+ let out = !diagnosticsBlock
2181
+ ? ""
2182
+ : impactHeader
2183
+ ? `${impactHeader}\n${diagnosticsBlock}`
2184
+ : diagnosticsBlock;
2062
2185
  // A10: include truncated filenames so agent knows which files were cut
2063
2186
  const truncated = totalNeighbors - neighbors.length;
2064
2187
  if (truncated > 0) {
@@ -2071,6 +2194,33 @@ function formatCascadeResult(cwd, impact, neighbors, totalNeighbors) {
2071
2194
  : `${truncated} more dependent file(s)`;
2072
2195
  out += `\n... and ${moreLabel}`;
2073
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
+ }
2074
2224
  return out;
2075
2225
  }
2076
2226
  /**
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Per-rule path carve-outs (`ignores:` in a rule's own YAML, #965).
3
+ *
4
+ * ONE matcher, shared by every surface that decides whether a rule may fire on
5
+ * a path: the NAPI ast-grep runner, the tree-sitter runner (`ignore_paths`),
6
+ * and — since #3041 — the LSP output seam (`applyAuxiliarySuppressions`).
7
+ *
8
+ * #3041, measured against ast-grep 0.45.3: `ast-grep scan` applies a rule's
9
+ * `ignores` globs during its own project walk, but `ast-grep lsp` publishes
10
+ * per-document diagnostics WITHOUT applying them — the same rule on the same
11
+ * file is filtered by the CLI and unfiltered over LSP. So every pi-lens path
12
+ * that delivers ast-grep's LSP diagnostics has to apply the carve-out itself;
13
+ * the runner-side matcher below is what it applies.
14
+ */
15
+ import * as path from "node:path";
16
+ import { minimatch } from "../deps/minimatch.js";
17
+ import { buildEffectiveAstGrepCatalog } from "./ast-grep-catalog.js";
18
+ /**
19
+ * True when `filePath` is carved out of a rule by one of its glob `patterns`.
20
+ *
21
+ * The glob is matched against `filePath` relative to `root`, forward-slashed.
22
+ * Falls back to the absolute (slash-normalized) path when `filePath` isn't
23
+ * under `root` (e.g. an out-of-tree temp file), so a glob like `scripts/**`
24
+ * simply never matches rather than throwing.
25
+ */
26
+ export function isRuleIgnoredForPath(filePath, root, patterns) {
27
+ if (!patterns || patterns.length === 0)
28
+ return false;
29
+ const relative = path.relative(root, filePath);
30
+ const displayPath = (relative.startsWith("..") ? filePath : relative)
31
+ .split(path.sep)
32
+ .join("/");
33
+ return patterns.some((pattern) => minimatch(displayPath, pattern, { dot: true }));
34
+ }
35
+ /**
36
+ * Rule id → its `ignores` globs, for the effective ast-grep catalog at `root`.
37
+ *
38
+ * #3053: the catalog walk and its first-source-wins precedence used to be
39
+ * reimplemented here; both this function and the NAPI runner now read
40
+ * `buildEffectiveAstGrepCatalog` (clients/dispatch/ast-grep-catalog.ts), so
41
+ * the LSP seam carves out exactly the paths the runner carves out by
42
+ * construction rather than by two hand-aligned copies — see that module's
43
+ * docstring for the precedence rule and the within-source-duplicate decision
44
+ * this function inherits from it. The catalog's own loaders are cached, so
45
+ * this is ~0.09 ms per call on pi-lens's own catalog (17 rules with
46
+ * `ignores`, measured) — cheap enough to call per file rather than thread a
47
+ * preloaded map through every sweep.
48
+ *
49
+ * Keyed by the EXACT rule id, matching the runner: a `-js` twin (e.g.
50
+ * `no-console-except-error-js`) carries its own `ignores` in its own document,
51
+ * so normalizing the suffix here would apply one document's carve-out to the
52
+ * other's findings.
53
+ */
54
+ export function loadRuleIgnorePatterns(root) {
55
+ const patterns = new Map();
56
+ for (const [id, { rule }] of buildEffectiveAstGrepCatalog(root)
57
+ .effectiveRules) {
58
+ if (rule.ignores?.length)
59
+ patterns.set(id, rule.ignores);
60
+ }
61
+ return patterns;
62
+ }
@@ -12,16 +12,16 @@ import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
13
  import { incrementDegradationCount, recordDegradationOnce, } from "../../degradation-ledger.js";
14
14
  import { loadAstGrepNapi, } from "../../deps/ast-grep-napi.js";
15
- import { minimatch } from "../../deps/minimatch.js";
15
+ import { buildEffectiveAstGrepCatalog } from "../ast-grep-catalog.js";
16
+ import { isRuleIgnoredForPath } from "../rule-ignores.js";
16
17
  import { logLatency } from "../../latency-logger.js";
17
18
  import { hasAuxiliaryLspPublishedForRoot } from "../../lsp/index.js";
18
19
  import { clearPendingAuxiliaryCoverage, hasPendingAuxiliaryCoverage, recordNapiFallbackCoverage, } from "../../lsp/pending-aux-coverage.js";
19
- import { getAstGrepRuleSources, } from "../../sgconfig.js";
20
20
  import { hasEslintConfig } from "../../tool-policy.js";
21
21
  import { enabledAuxiliaryLspServerIds } from "../auxiliary-lsp.js";
22
22
  import { classifyDefect } from "../diagnostic-taxonomy.js";
23
23
  import { PRIORITY } from "../priorities.js";
24
- import { calculateRuleComplexity, isOverlyBroadPattern, isStructuredRule, loadYamlRules, loadYamlRulesFresh, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
24
+ import { calculateRuleComplexity, isOverlyBroadPattern, isStructuredRule, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
25
25
  const defaultUnsupportedLanguageLog = new Set();
26
26
  const UNSUPPORTED_RULE_ID_SAMPLE_SIZE = 5;
27
27
  /** Clear per-session unsupported-language telemetry dedupe. */
@@ -366,22 +366,6 @@ function explicitRuleFixSuggestion(rule) {
366
366
  function normalizeRuleId(ruleId) {
367
367
  return ruleId.replace(/-js$/, "");
368
368
  }
369
- /**
370
- * `filePath` relative to `root`, forward-slashed, for matching a rule's
371
- * `ignores` globs (#965). Falls back to the absolute (slash-normalized) path
372
- * when `filePath` isn't under `root` (e.g. an out-of-tree temp file), so a
373
- * glob like `scripts/**` simply never matches rather than throwing.
374
- */
375
- function relativeForIgnoreGlob(filePath, root) {
376
- const rel = path.relative(root, filePath);
377
- return (rel.startsWith("..") ? filePath : rel).split(path.sep).join("/");
378
- }
379
- function matchesRuleIgnores(filePath, root, patterns) {
380
- if (!patterns || patterns.length === 0)
381
- return false;
382
- const rel = relativeForIgnoreGlob(filePath, root);
383
- return patterns.some((pattern) => minimatch(rel, pattern, { dot: true }));
384
- }
385
369
  export function canHandle(filePath) {
386
370
  return BINDING_BY_EXTENSION.has(path.extname(filePath).toLowerCase());
387
371
  }
@@ -660,7 +644,7 @@ function duplicateRuleIds(rules) {
660
644
  .map(([id]) => id)
661
645
  .sort((a, b) => a.localeCompare(b));
662
646
  }
663
- function appendDuplicateRuleDiagnostics(diagnostics, seenRuleIds, duplicateIds, source, filePath, maxTotalDiagnostics) {
647
+ function appendDuplicateRuleDiagnostics(diagnostics, duplicateIds, source, filePath, maxTotalDiagnostics) {
664
648
  const sourceLabel = `${source.origin} ${source.tier} rules`;
665
649
  for (const ruleId of duplicateIds) {
666
650
  diagnostics.push({
@@ -678,7 +662,6 @@ function appendDuplicateRuleDiagnostics(diagnostics, seenRuleIds, duplicateIds,
678
662
  autoFixAvailable: false,
679
663
  fixSuggestion: `Give every rule in ${sourceLabel} a unique id`,
680
664
  });
681
- seenRuleIds.add(ruleId);
682
665
  if (diagnostics.length >= maxTotalDiagnostics)
683
666
  return true;
684
667
  }
@@ -723,7 +706,6 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
723
706
  const log = options.log;
724
707
  const unsupportedLanguageLog = options.unsupportedLanguageLog ?? defaultUnsupportedLanguageLog;
725
708
  const diagnostics = [];
726
- const seenRuleIds = new Set();
727
709
  const suppressLinterOverlap = kind === "jsts" && hasEslintConfig(cwd);
728
710
  const fileLang = ruleLanguageForFile(filePath);
729
711
  // Embedded `<script>` coverage (#2347): on an HTML file, every script body
@@ -811,23 +793,14 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
811
793
  });
812
794
  newlyUnsupported.clear();
813
795
  };
814
- // Shared with the raw sgconfig materializer so both surfaces walk the same
815
- // workspace-rooted sources in the same precedence order.
796
+ // Same walk and precedence the LSP-seam matcher uses
797
+ // (clients/dispatch/rule-ignores.ts), folded onto one derivation by #3053
798
+ // after #3046 shipped them as two independently-drifting copies.
816
799
  const ignoreRoot = options.projectRoot ?? cwd;
817
- const ruleSources = getAstGrepRuleSources(ignoreRoot);
818
- for (const source of ruleSources) {
819
- let rules;
820
- try {
821
- // Project rules are mutable during a session, so their cache fingerprints
822
- // relative paths and contents. Bundled catalogs are immutable per install.
823
- const loader = source.origin === "project" ? loadYamlRulesFresh : loadYamlRules;
824
- rules = loader(source.dir);
825
- }
826
- catch {
827
- continue;
828
- }
800
+ const catalog = buildEffectiveAstGrepCatalog(ignoreRoot);
801
+ for (const { source, rules } of catalog.sources) {
829
802
  const duplicates = duplicateRuleIds(rules);
830
- if (appendDuplicateRuleDiagnostics(diagnostics, seenRuleIds, duplicates, source, filePath, maxTotalDiagnostics)) {
803
+ if (appendDuplicateRuleDiagnostics(diagnostics, duplicates, source, filePath, maxTotalDiagnostics)) {
831
804
  flushUnsupportedRuleSkips();
832
805
  return diagnostics;
833
806
  }
@@ -835,16 +808,23 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
835
808
  for (const rule of rules) {
836
809
  if (duplicateSet.has(rule.id))
837
810
  continue;
838
- // Cross-layer collisions keep the first (higher-precedence) source.
839
- if (seenRuleIds.has(rule.id))
811
+ // Cross-layer collisions keep the first (higher-precedence) source
812
+ // resolved once by `buildEffectiveAstGrepCatalog`, not per-rule here.
813
+ // Compares the winning DOCUMENT (object identity), not merely its
814
+ // source: comparing `.source` alone can't tell a within-source
815
+ // duplicate's two copies apart (they share one source), so it gave
816
+ // no defense if `duplicateSet.has` above were ever the only guard
817
+ // standing (#3053 round 2 F1). Every `rule` iterated here is the
818
+ // same object instance the catalog recorded, since both read the
819
+ // identical `rules` arrays off `catalog.sources`.
820
+ if (catalog.effectiveRules.get(rule.id)?.rule !== rule)
840
821
  continue;
841
- seenRuleIds.add(rule.id);
842
822
  if (blockingOnly && rule.severity !== "error")
843
823
  continue;
844
824
  // Per-rule path carve-out (#965): a rule that's noise on CLI scripts or
845
825
  // a project's own logging sink (e.g. no-console-except-error firing
846
826
  // inside scripts/** or lib/logger.ts) opts out via `ignores`.
847
- if (matchesRuleIgnores(filePath, ignoreRoot, rule.ignores))
827
+ if (isRuleIgnoredForPath(filePath, ignoreRoot, rule.ignores))
848
828
  continue;
849
829
  if (suppressLinterOverlap &&
850
830
  LINTER_OVERLAP.has(normalizeRuleId(rule.id)) &&
@@ -202,7 +202,12 @@ const biomeCheckJsonRunner = {
202
202
  // Defer to ESLint/oxlint if the project has explicitly configured one —
203
203
  // biome runs as the default linter only when no alternative is present.
204
204
  if (!policy.hasBiomeConfig && policy.hasExplicitNonBiomeLinter) {
205
- return { status: "skipped", diagnostics: [], semantic: "none" };
205
+ return {
206
+ status: "skipped",
207
+ diagnostics: [],
208
+ semantic: "none",
209
+ skipReason: "configured-non-biome-linter",
210
+ };
206
211
  }
207
212
  const cmd = await resolveToolCommandWithInstallFallback(cwd, "biome");
208
213
  if (!cmd) {
@@ -3,15 +3,14 @@
3
3
  *
4
4
  * Runs `cargo clippy` for Rust files to catch common mistakes.
5
5
  */
6
- import { existsSync } from "node:fs";
7
- import { isAbsolute, join, resolve } from "node:path";
6
+ import { isAbsolute, resolve } from "node:path";
8
7
  import { rustClient } from "../../rust-client.js";
9
8
  import { safeSpawnAsync } from "../../safe-spawn.js";
10
9
  import { stripAnsi } from "../../sanitize.js";
11
10
  import { getLazyInstallAttempt, tryLazyInstall, } from "./utils/lazy-installer.js";
12
11
  import { describeInstallAttempt, logAvailabilityDecision, } from "./utils/availability-policy.js";
13
12
  import { PRIORITY } from "../priorities.js";
14
- import { resolveRunnerCwd } from "../../tool-cwd.js";
13
+ import { resolveRunnerCwdWithReason } from "../../tool-cwd.js";
15
14
  import { createCwdCachedProbe } from "./utils/runner-helpers.js";
16
15
  // Cached per-cwd `cargo clippy --version` probe (#120). Before this, the
17
16
  // probe fired on every Rust file save in a project where clippy was already
@@ -91,13 +90,14 @@ const rustClippyRunner = {
91
90
  }
92
91
  }
93
92
  // The package root (where Cargo.toml is), through the shared cwd seam
94
- // (#2894): `RUNNER_MARKERS["rust-clippy"]` is `["Cargo.toml"]`, so this
95
- // IS the walk this runner used to do with `findNearestContaining` — plus
93
+ // (#2894): the shared language marker vocabulary includes `Cargo.toml`, so
94
+ // this IS the walk this runner used to do with `findNearestContaining` — plus
96
95
  // the seam's dispatch-root and `$HOME` ceilings, which the hand-rolled
97
96
  // walk had neither of. The gate then asks about exactly the directory
98
97
  // cargo will run in, rather than about a separately-derived one.
99
- const cargoDir = resolveRunnerCwd(ctx, "rust-clippy");
100
- if (!existsSync(join(cargoDir, "Cargo.toml"))) {
98
+ const cargoResolution = resolveRunnerCwdWithReason(ctx, "rust-clippy");
99
+ const cargoDir = cargoResolution.cwd;
100
+ if (cargoResolution.marker !== "Cargo.toml") {
101
101
  return { status: "skipped", diagnostics: [], semantic: "none" };
102
102
  }
103
103
  // Run cargo clippy on the package
@@ -1,6 +1,5 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
1
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
+ import { findNearestContaining } from "../../path-utils.js";
4
3
  import { resolveRunnerCwd } from "../../tool-cwd.js";
5
4
  import { PRIORITY } from "../priorities.js";
6
5
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
@@ -10,16 +9,7 @@ const shfmt = createAvailabilityChecker("shfmt", ".exe");
10
9
  // reports genuine parse errors instead of nagging every unformatted shell file
11
10
  // against shfmt's built-in defaults (#211).
12
11
  function hasEditorConfig(cwd) {
13
- let current = path.resolve(cwd);
14
- while (true) {
15
- if (fs.existsSync(path.join(current, ".editorconfig")))
16
- return true;
17
- const parent = path.dirname(current);
18
- if (parent === current)
19
- break;
20
- current = parent;
21
- }
22
- return false;
12
+ return findNearestContaining(cwd, [".editorconfig"]) !== undefined;
23
13
  }
24
14
  /**
25
15
  * shfmt runner — checks shell script formatting.
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import * as path from "node:path";
8
8
  import { RuleCache } from "../../cache/rule-cache.js";
9
- import { minimatch } from "../../deps/minimatch.js";
9
+ import { isRuleIgnoredForPath } from "../rule-ignores.js";
10
10
  import { isTestFile } from "../../file-utils.js";
11
11
  import { buildOrUpdateGraph, computeImpactCascade, recordEntitySnapshotDiff, } from "../../review-graph/service.js";
12
12
  import { getSharedTreeSitterClient, isTreeSitterWasmAborted, markTreeSitterWasmAborted, resolveTreeSitterLanguage, } from "../../tree-sitter-shared.js";
@@ -292,25 +292,6 @@ function getTotalLinesChanged(ranges) {
292
292
  }
293
293
  /** Threshold: skip entity extraction for changes under 5 lines */
294
294
  const ENTITY_EXTRACTION_LINE_THRESHOLD = 5;
295
- /**
296
- * `filePath` relative to `root`, forward-slashed, for matching a rule's
297
- * `ignore_paths` globs. Falls back to the absolute (slash-normalized) path
298
- * when `filePath` isn't under `root` (e.g. an out-of-tree temp file), so a
299
- * glob like `scripts/**` simply never matches rather than throwing.
300
- */
301
- function relativeForIgnoreGlob(filePath, root) {
302
- const rel = path.relative(root, filePath);
303
- const normalized = (rel.startsWith("..") ? filePath : rel)
304
- .split(path.sep)
305
- .join("/");
306
- return normalized;
307
- }
308
- function matchesIgnorePaths(filePath, root, patterns) {
309
- if (!patterns || patterns.length === 0)
310
- return false;
311
- const rel = relativeForIgnoreGlob(filePath, root);
312
- return patterns.some((pattern) => minimatch(rel, pattern, { dot: true }));
313
- }
314
295
  const treeSitterRunner = {
315
296
  id: "tree-sitter",
316
297
  appliesTo: [
@@ -447,7 +428,7 @@ const treeSitterRunner = {
447
428
  const effectiveQueries = (ctx.blockingOnly
448
429
  ? languageQueries.filter((q) => q.inline_tier !== "review")
449
430
  : languageQueries).filter((q) => !(fileIsTest && q.skip_test_files) &&
450
- !matchesIgnorePaths(filePath, ignoreRoot, q.ignore_paths));
431
+ !isRuleIgnoredForPath(filePath, ignoreRoot, q.ignore_paths));
451
432
  logTreeSitter({
452
433
  phase: "queries_loaded",
453
434
  filePath,
@@ -1697,5 +1697,5 @@ export const sg = {
1697
1697
  };
1698
1698
  /** Shared cwd seam for runner probes and analysis children (#2777). */
1699
1699
  export function resolveRunnerCwd(ctx, tool) {
1700
- return resolveToolCwd("runner", tool, ctx.filePath, ctx);
1700
+ return resolveToolCwd("runner", tool, ctx.filePath, ctx).cwd;
1701
1701
  }
@@ -11,7 +11,10 @@
11
11
  * The dispatcher must handle these semantics consistently.
12
12
  */
13
13
  /** Closed telemetry taxonomy for expected runner skips. */
14
- export const RUNNER_SKIP_REASONS = ["no-files-matched"];
14
+ export const RUNNER_SKIP_REASONS = [
15
+ "no-files-matched",
16
+ "configured-non-biome-linter",
17
+ ];
15
18
  /** Runtime guard for untyped/plugin-provided runner results. */
16
19
  export function isRunnerSkipReason(value) {
17
20
  return RUNNER_SKIP_REASONS.some((reason) => reason === value);
@@ -97,6 +97,7 @@ export function publishDisposition(args) {
97
97
  line: args.line,
98
98
  anchor: args.anchor,
99
99
  reason: args.reason,
100
+ message: args.message,
100
101
  };
101
102
  busEmit(BUS_DISPOSITION_EVENT, payload);
102
103
  hasLoggedFailure = false;