pi-lens 4.4.2 → 4.4.4

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 (143) hide show
  1. package/CHANGELOG.md +306 -0
  2. package/README.md +13 -1
  3. package/dist/{chunk-VITUCP5A.js → chunk-24AKHFGP.js} +9 -7
  4. package/dist/{chunk-JSQFTSC6.js → chunk-2FN63LPV.js} +11 -11
  5. package/dist/{chunk-CF24OIYI.js → chunk-2KXTHF3M.js} +20 -20
  6. package/dist/{chunk-ESBQHFMJ.js → chunk-3DA2JDOL.js} +543 -349
  7. package/dist/{chunk-RWWYJ623.js → chunk-3KAZU3VR.js} +1 -1
  8. package/dist/chunk-3PTHXJXR.js +28 -0
  9. package/dist/chunk-3QED2P4V.js +31 -0
  10. package/dist/{chunk-N2H3ZPZY.js → chunk-3RKP3FMS.js} +1 -0
  11. package/dist/{chunk-XVF2VVXP.js → chunk-5HSRT26B.js} +2 -2
  12. package/dist/{chunk-XKIZ2HNN.js → chunk-5IUSNGOC.js} +18 -16
  13. package/dist/{chunk-EJIUXPBX.js → chunk-5OFV52AM.js} +1 -1
  14. package/dist/{chunk-AW7R2GWG.js → chunk-5VSF4UUQ.js} +12 -7
  15. package/dist/{chunk-NW7BOILN.js → chunk-5WRPDP2A.js} +18 -18
  16. package/dist/{chunk-7ULNCDCO.js → chunk-6DLWKFEX.js} +35 -34
  17. package/dist/{chunk-WOA4LN2R.js → chunk-7D6L4MTX.js} +2 -2
  18. package/dist/{chunk-GAEGEPOC.js → chunk-7HXE67BN.js} +2 -2
  19. package/dist/chunk-AGSS7277.js +84 -0
  20. package/dist/{chunk-R3PCY3YS.js → chunk-AMERCH6V.js} +13 -13
  21. package/dist/{chunk-WU567RSC.js → chunk-ATKJFDQN.js} +14 -10
  22. package/dist/{chunk-VSSBEUFT.js → chunk-AWSCPCN2.js} +16 -16
  23. package/dist/{chunk-VCHXCQFG.js → chunk-BWD5EPKO.js} +3 -3
  24. package/dist/{chunk-ODBNXWT4.js → chunk-CT4MN5IU.js} +1271 -262
  25. package/dist/{chunk-34WSVGEM.js → chunk-CUHSHSB6.js} +83 -5
  26. package/dist/{chunk-CHOW74WE.js → chunk-CYRKQRPM.js} +7 -10
  27. package/dist/{chunk-Q7DUK4LX.js → chunk-DEL2U6CM.js} +5 -5
  28. package/dist/{chunk-6C7JWPCP.js → chunk-DGUKIYSI.js} +26 -23
  29. package/dist/{chunk-PHDMFAKM.js → chunk-DUO2TU77.js} +2 -2
  30. package/dist/{chunk-MRXKYPCS.js → chunk-EBC36LNN.js} +2 -2
  31. package/dist/chunk-ED36RF6Y.js +17 -0
  32. package/dist/{chunk-76MWC2K6.js → chunk-EUXZEFTV.js} +90 -97
  33. package/dist/{chunk-QOZXJ4NA.js → chunk-FM4RDMKT.js} +11 -8
  34. package/dist/{chunk-66VMUL7Q.js → chunk-GAQ7OQG6.js} +31 -28
  35. package/dist/{chunk-Q6ZWTCLY.js → chunk-GL5LUMZE.js} +23 -24
  36. package/dist/{chunk-BKE7E7TC.js → chunk-GMX5OS7B.js} +9 -9
  37. package/dist/{chunk-ZHFZWTZV.js → chunk-GSLNCYGG.js} +11 -11
  38. package/dist/{chunk-ARJ6PLFH.js → chunk-GSM5UU7A.js} +26 -34
  39. package/dist/{chunk-724UL2TJ.js → chunk-GXFV6MYU.js} +3 -3
  40. package/dist/chunk-HAD7QRQI.js +30 -0
  41. package/dist/{chunk-QOBUHWHX.js → chunk-HRNHZA3N.js} +6 -6
  42. package/dist/{chunk-VWCXW2E7.js → chunk-HWCCCQNX.js} +1 -1
  43. package/dist/{chunk-7DYBLXYA.js → chunk-HYGZEMX6.js} +3 -3
  44. package/dist/chunk-IJYGIDF4.js +186 -0
  45. package/dist/{chunk-4W4VLAPJ.js → chunk-IQWN5JCP.js} +11 -11
  46. package/dist/{chunk-GZYBXISR.js → chunk-IUE7SSKV.js} +9 -9
  47. package/dist/{chunk-IBGIOBEE.js → chunk-IWDLU6K3.js} +68 -5
  48. package/dist/{chunk-XCJN5IJU.js → chunk-IYNJ6YPZ.js} +4 -4
  49. package/dist/{chunk-JAU72XNY.js → chunk-J64T55EI.js} +3 -3
  50. package/dist/{chunk-2BBLFX75.js → chunk-J6MPGQTS.js} +5 -5
  51. package/dist/chunk-JORED7EX.js +44 -0
  52. package/dist/{chunk-6SDEP3AJ.js → chunk-JUKMK5Y4.js} +8 -8
  53. package/dist/{chunk-IBRALRBR.js → chunk-JW3YWBVB.js} +15 -14
  54. package/dist/{chunk-L3BLXZG7.js → chunk-KAPJINKN.js} +30 -69
  55. package/dist/{chunk-2B73ZPOK.js → chunk-KN35K52N.js} +3 -3
  56. package/dist/{chunk-C34ZXZ7O.js → chunk-KUA4OJ5Y.js} +24 -11
  57. package/dist/chunk-KYUB6QDC.js +72 -0
  58. package/dist/{chunk-67WMNWEE.js → chunk-LLJNQKQW.js} +12 -10
  59. package/dist/chunk-LZSEQW6A.js +57 -0
  60. package/dist/{chunk-ZANLILYD.js → chunk-M2IFVFWU.js} +2 -2
  61. package/dist/chunk-M2UD3LPU.js +88 -0
  62. package/dist/{chunk-WOKKRCJ5.js → chunk-MBKQHGA7.js} +15 -14
  63. package/dist/{chunk-P7266HLM.js → chunk-MPNJLASA.js} +2 -2
  64. package/dist/{chunk-6GFRNVYR.js → chunk-MZGQNL3Y.js} +10 -10
  65. package/dist/{chunk-QZVW3AAK.js → chunk-N5FLEO7W.js} +33 -31
  66. package/dist/{chunk-3JNKMP3B.js → chunk-O4NWQT6I.js} +4 -4
  67. package/dist/{chunk-PFNSLWU5.js → chunk-OHKJ7PQF.js} +33 -31
  68. package/dist/{chunk-VRZP76YM.js → chunk-P63GXC37.js} +4 -1
  69. package/dist/{chunk-XTMJ7XDT.js → chunk-POMLXPMY.js} +1 -1
  70. package/dist/{chunk-QQNQ7SJW.js → chunk-QBFBRV2B.js} +10 -4
  71. package/dist/{chunk-3UUPUE3O.js → chunk-R3T7CLQ2.js} +84 -60
  72. package/dist/{chunk-23WOO7TX.js → chunk-R7YOCSNV.js} +10 -10
  73. package/dist/{chunk-2CD5RS6M.js → chunk-RKBXZN3A.js} +23 -2
  74. package/dist/{chunk-OG5UUPN7.js → chunk-RKKOYPRA.js} +14 -14
  75. package/dist/{chunk-52FUZI6K.js → chunk-RMLC67LF.js} +1 -1
  76. package/dist/{chunk-XMSXXAHW.js → chunk-ROYSX2S5.js} +2 -2
  77. package/dist/{chunk-PV6LR5P5.js → chunk-RVK72CZX.js} +9605 -14776
  78. package/dist/{chunk-FLKWCIZH.js → chunk-S2HEXIWI.js} +5 -5
  79. package/dist/{chunk-UZ3TUTRV.js → chunk-S67KYG37.js} +1 -1
  80. package/dist/{chunk-JWA5I3SR.js → chunk-SD4I7ZO4.js} +16 -18
  81. package/dist/{chunk-Z527N6YQ.js → chunk-SKAMFC7A.js} +12 -12
  82. package/dist/{chunk-MQLUAEOC.js → chunk-SRYEJNES.js} +2 -2
  83. package/dist/{chunk-BRGDSBJE.js → chunk-SYE6T2EB.js} +12 -12
  84. package/dist/{chunk-GXW6K4YJ.js → chunk-T57LYLZT.js} +1 -1
  85. package/dist/{chunk-ZXYDQ3TZ.js → chunk-T7FEBKXN.js} +48 -15
  86. package/dist/{chunk-IXXDFY73.js → chunk-TIQ2PKDW.js} +3 -3
  87. package/dist/{chunk-QILIYSJF.js → chunk-TLX72JTG.js} +1 -1
  88. package/dist/{chunk-TYM6OUIT.js → chunk-TOY6QKOH.js} +12 -12
  89. package/dist/{chunk-IZSHAIJ7.js → chunk-TRETHTQO.js} +12 -12
  90. package/dist/{chunk-3VPVG5SU.js → chunk-U44DJXQP.js} +10 -6
  91. package/dist/{chunk-PNQDPDRC.js → chunk-UA7ZXYDR.js} +1 -1
  92. package/dist/{chunk-OXXKQCSV.js → chunk-UBOQLVT2.js} +4 -4
  93. package/dist/{chunk-QGQ5CCCG.js → chunk-UMS2MFPZ.js} +2 -2
  94. package/dist/{chunk-QHJ6DQ2R.js → chunk-US7ECLD7.js} +55 -13
  95. package/dist/{chunk-EETY4LF7.js → chunk-UZ2XCLYS.js} +8 -8
  96. package/dist/chunk-V35JG4JB.js +2667 -0
  97. package/dist/{chunk-N3X6Q4YT.js → chunk-VOOAPROD.js} +3 -3
  98. package/dist/{chunk-SAEFPSB6.js → chunk-W66O5E25.js} +21 -11
  99. package/dist/{chunk-KBG47GHP.js → chunk-WMYE35PX.js} +15 -11
  100. package/dist/{chunk-5NGR67U5.js → chunk-XBJVAEEO.js} +32 -5
  101. package/dist/chunk-XHJBS436.js +4217 -0
  102. package/dist/{chunk-GBPP5P7X.js → chunk-XTBDCR27.js} +4 -4
  103. package/dist/{chunk-4UNXBNE2.js → chunk-XTFDJI3T.js} +11 -11
  104. package/dist/{chunk-UXGETRP4.js → chunk-XYURS6EN.js} +13 -12
  105. package/dist/{chunk-RPLINGVB.js → chunk-Y6GZHWSJ.js} +6 -6
  106. package/dist/{chunk-2ZGIZPDG.js → chunk-YCWYO6TQ.js} +41 -38
  107. package/dist/{chunk-66SXUJCA.js → chunk-YH5MVY2U.js} +10 -8
  108. package/dist/{chunk-AOHZU2BM.js → chunk-YNE3RP52.js} +1 -1
  109. package/dist/{chunk-GOYRYZ2R.js → chunk-ZHH37KZL.js} +72 -20
  110. package/dist/{chunk-ZGDGW2V3.js → chunk-ZIVJBOO3.js} +2 -2
  111. package/dist/{chunk-GHB6AYCP.js → chunk-ZVUL7QYY.js} +29 -28
  112. package/dist/index.js +11559 -6213
  113. package/dist/mcp/analyze-cli.js +10 -9
  114. package/dist/mcp/cli.js +41 -38
  115. package/dist/mcp/server.js +1142 -495
  116. package/dist/mcp/worker.js +61 -57
  117. package/dist/probes/installer.js +19 -19
  118. package/dist/workers/project-snapshot-persist-worker.js +28 -9
  119. package/dist/workers/review-graph-persist-worker.js +1 -1
  120. package/docs/configuration.md +11 -0
  121. package/docs/engineering-principles.md +32 -0
  122. package/docs/lsp-capability-matrix.md +1 -1
  123. package/docs/lsp-idle-eviction.md +15 -26
  124. package/docs/pi-lens-fixer.md +117 -13
  125. package/docs/pi-lens-merge-policy.md +109 -11
  126. package/docs/pi-lens-monitor.md +8 -2
  127. package/docs/pi-lens-retro.md +4 -1
  128. package/docs/pi-lens-reviewer.md +114 -3
  129. package/docs/pi-lens-subagent.md +27 -14
  130. package/docs/release-qa-baseline.md +3 -1
  131. package/docs/servercapabilities.md +9 -19
  132. package/package.json +7 -5
  133. package/scripts/download-grammars.js +40 -7
  134. package/scripts/lib/latency-log-phases.mjs +95 -0
  135. package/scripts/rpc-load-check.mjs +570 -81
  136. package/dist/chunk-3WA4HP4I.js +0 -55
  137. package/dist/chunk-466Z26MG.js +0 -30
  138. package/dist/chunk-5JM2524W.js +0 -84
  139. package/dist/chunk-CO2G5PHH.js +0 -84
  140. package/dist/chunk-LOP6GRPH.js +0 -31
  141. package/dist/chunk-UOPVJQ4N.js +0 -103
  142. package/dist/chunk-XPZPXCBV.js +0 -28
  143. /package/dist/{chunk-FC4RP4I2.js → chunk-6HWVI6MI.js} +0 -0
@@ -2,17 +2,7 @@ import { createRequire as __pilensCreateRequire } from "node:module"; const requ
2
2
  import {
3
3
  getLazyInstallAttempt,
4
4
  tryLazyInstall
5
- } from "./chunk-OXXKQCSV.js";
6
- import {
7
- stripAnsi
8
- } from "./chunk-FC4RP4I2.js";
9
- import {
10
- isTrivyEnabled,
11
- resolveSeverityFloor
12
- } from "./chunk-66SXUJCA.js";
13
- import {
14
- rustClient
15
- } from "./chunk-MRXKYPCS.js";
5
+ } from "./chunk-UBOQLVT2.js";
16
6
  import {
17
7
  findingsResult,
18
8
  finishParsedRun,
@@ -21,11 +11,29 @@ import {
21
11
  isRunnerClaimSource,
22
12
  isRunnerSkipReason,
23
13
  parseToolRun,
24
- skipUnlessToolRan
25
- } from "./chunk-2CD5RS6M.js";
14
+ reachedNoVerdict,
15
+ skipUnlessToolRan,
16
+ spanOverlapsModifiedRanges
17
+ } from "./chunk-RKBXZN3A.js";
18
+ import {
19
+ stripAnsi
20
+ } from "./chunk-6HWVI6MI.js";
26
21
  import {
27
22
  spawnFailedWithNoOutput
28
- } from "./chunk-UZ3TUTRV.js";
23
+ } from "./chunk-S67KYG37.js";
24
+ import {
25
+ computeImpactCascade
26
+ } from "./chunk-KN35K52N.js";
27
+ import {
28
+ goClient
29
+ } from "./chunk-ROYSX2S5.js";
30
+ import {
31
+ isTrivyEnabled,
32
+ resolveSeverityFloor
33
+ } from "./chunk-YH5MVY2U.js";
34
+ import {
35
+ rustClient
36
+ } from "./chunk-EBC36LNN.js";
29
37
  import {
30
38
  coveringLaneAvailable,
31
39
  createAvailabilityChecker,
@@ -33,6 +41,7 @@ import {
33
41
  getDispatchAvailabilityGeneration,
34
42
  lspPrimaryCoversFile,
35
43
  recordAvailabilityProbeOverrun,
44
+ resolveAdoptedRootCommand,
36
45
  resolveAvailableOrInstall,
37
46
  resolveCommandArgsWithInstallFallback,
38
47
  resolveLocalFirstAsync,
@@ -40,7 +49,7 @@ import {
40
49
  resolveToolCommand,
41
50
  resolveToolCommandWithInstallFallback,
42
51
  resolveVendorToolCommand
43
- } from "./chunk-GOYRYZ2R.js";
52
+ } from "./chunk-ZHH37KZL.js";
44
53
  import {
45
54
  biomeConfigArgs,
46
55
  findCompiledClassesDir,
@@ -59,47 +68,44 @@ import {
59
68
  hasYamllintConfig,
60
69
  markdownlintConfigArgs,
61
70
  ruffConfigArgs
62
- } from "./chunk-RPLINGVB.js";
71
+ } from "./chunk-Y6GZHWSJ.js";
63
72
  import {
64
73
  importFactProvider
65
- } from "./chunk-XCJN5IJU.js";
74
+ } from "./chunk-IYNJ6YPZ.js";
66
75
  import {
67
76
  functionFactProvider
68
- } from "./chunk-76MWC2K6.js";
77
+ } from "./chunk-EUXZEFTV.js";
69
78
  import {
70
79
  extractFactsFromTree
71
- } from "./chunk-GXW6K4YJ.js";
80
+ } from "./chunk-T57LYLZT.js";
72
81
  import {
73
- computeImpactCascade
74
- } from "./chunk-2B73ZPOK.js";
75
- import {
76
- goClient
77
- } from "./chunk-XMSXXAHW.js";
82
+ buildOrUpdateGraph,
83
+ clearReviewGraphWorkspaceCache,
84
+ fileContentProvider
85
+ } from "./chunk-XHJBS436.js";
78
86
  import {
79
87
  MAX_BLOCKING_RULE_COMPLEXITY,
80
- RUNTIME_CONFIG,
88
+ PRIMARY_UNAVAILABLE_CAUSE,
81
89
  applyDispositions,
82
90
  applyDispositionsMultiFile,
83
91
  buildEffectiveAstGrepCatalog,
84
- buildOrUpdateGraph,
85
92
  calculateRuleComplexity,
86
93
  classifyDefect,
87
94
  classifyDiagnostic,
88
95
  classifyDiagnosticTier,
89
96
  clearPendingAuxiliaryCoverage,
90
- clearReviewGraphWorkspaceCache,
97
+ combinePrimaryUnavailableCauses,
91
98
  enabledAuxiliaryLspServerIds,
92
- exceedsLspSyncLimits,
93
99
  fetchTsserverProjectIdentity,
94
- fileContentProvider,
95
100
  getLSPService,
96
- getRunnerTimeoutFloorMs,
97
101
  hasAnyDispositionMarks,
98
102
  hasAuxiliaryLspPublishedForRoot,
99
103
  hasPendingAuxiliaryCoverage,
100
104
  isOverlyBroadPattern,
101
105
  isRuleIgnoredForPath,
102
106
  isStructuredRule,
107
+ lineContentHash,
108
+ primaryUnavailableCause,
103
109
  recordDiagnostics,
104
110
  recordNapiFallbackCoverage,
105
111
  recordRunner,
@@ -107,24 +113,40 @@ import {
107
113
  touchCoverageGap,
108
114
  tryWarmAttachedCodeActions,
109
115
  tryWarmAttachedDiagnostics
110
- } from "./chunk-PV6LR5P5.js";
116
+ } from "./chunk-RVK72CZX.js";
117
+ import {
118
+ ANY_POSITION,
119
+ retainRecordedFindings
120
+ } from "./chunk-IWDLU6K3.js";
111
121
  import {
112
122
  mapWithConcurrency
113
123
  } from "./chunk-XIMYZ3JA.js";
114
124
  import {
115
125
  LSP_SERVERS,
126
+ disabledPrimaries,
116
127
  getAstGrepRuleSources,
117
- registerRunnerId
118
- } from "./chunk-ESBQHFMJ.js";
128
+ registerRunnerId,
129
+ resolveAnalysisRoot
130
+ } from "./chunk-3DA2JDOL.js";
119
131
  import {
120
132
  augmentPythonEnvironment,
121
133
  detectPythonEnvironment
122
- } from "./chunk-34WSVGEM.js";
134
+ } from "./chunk-CUHSHSB6.js";
123
135
  import {
124
136
  loadProjectSnapshot,
125
137
  readJsonCache,
126
138
  readJsonCacheAsync
127
- } from "./chunk-ZXYDQ3TZ.js";
139
+ } from "./chunk-T7FEBKXN.js";
140
+ import {
141
+ RUNTIME_CONFIG,
142
+ exceedsLspSyncLimits,
143
+ getRunnerTimeoutFloorMs,
144
+ loadAstGrepNapi
145
+ } from "./chunk-V35JG4JB.js";
146
+ import {
147
+ buildModuleGraph,
148
+ clearModuleGraphCache
149
+ } from "./chunk-XTBDCR27.js";
128
150
  import {
129
151
  BUNDLED_QUERIES_ROOT,
130
152
  classifyTreeSitterWasmError,
@@ -144,44 +166,44 @@ import {
144
166
  ruleFilesForLanguage,
145
167
  ruleSourceLanguages,
146
168
  walk
147
- } from "./chunk-QHJ6DQ2R.js";
148
- import {
149
- buildModuleGraph,
150
- clearModuleGraphCache
151
- } from "./chunk-GBPP5P7X.js";
169
+ } from "./chunk-US7ECLD7.js";
152
170
  import {
153
171
  WORD_INDEX_MAX_BYTES,
154
172
  deserializeWordIndex,
155
173
  logWordIndex,
156
174
  removeWordIndexDocumentAsync,
157
175
  scheduleWordIndexPersist,
176
+ setWordIndexPublishedListener,
158
177
  updateWordIndexDocumentForEdit
159
- } from "./chunk-SAEFPSB6.js";
178
+ } from "./chunk-W66O5E25.js";
160
179
  import {
161
180
  PathKeyedMap
162
181
  } from "./chunk-Z3CCVDUB.js";
163
182
  import {
164
183
  WARM_CODE_ACTION_LOOKUP_LIMIT,
165
- contentHash,
166
- resolveRunnerPath
167
- } from "./chunk-L3BLXZG7.js";
184
+ contentHash
185
+ } from "./chunk-KAPJINKN.js";
186
+ import {
187
+ resolveRunnerPath,
188
+ toRunnerDisplayPath
189
+ } from "./chunk-AGSS7277.js";
168
190
  import {
169
191
  isSpawnableCommand
170
- } from "./chunk-66VMUL7Q.js";
192
+ } from "./chunk-GAQ7OQG6.js";
171
193
  import {
172
194
  logRunnerAdvisoryOnce,
173
195
  resolveRunnerCwd,
174
196
  resolveRunnerCwdWithReason
175
- } from "./chunk-Q7DUK4LX.js";
197
+ } from "./chunk-DEL2U6CM.js";
176
198
  import {
177
199
  findLocalBinUpwards,
178
200
  localBinPath
179
- } from "./chunk-AW7R2GWG.js";
201
+ } from "./chunk-5VSF4UUQ.js";
180
202
  import {
181
203
  createAvailabilityProbeFlight,
182
204
  getIsolatedNpxSpawnOptions,
183
205
  probeToolAsync
184
- } from "./chunk-RWWYJ623.js";
206
+ } from "./chunk-3KAZU3VR.js";
185
207
  import {
186
208
  classifyProbeFailure,
187
209
  createAvailabilityLatch,
@@ -191,12 +213,12 @@ import {
191
213
  logAvailabilityDecision,
192
214
  startHostStallSampler,
193
215
  transientRetryDelayMs
194
- } from "./chunk-VRZP76YM.js";
216
+ } from "./chunk-P63GXC37.js";
195
217
  import {
196
218
  getLspCapableKinds,
197
219
  getPrimaryDispatchGroup,
198
220
  resolveLanguageRootForFile
199
- } from "./chunk-QOBUHWHX.js";
221
+ } from "./chunk-HRNHZA3N.js";
200
222
  import {
201
223
  getGlobalPiLensDir,
202
224
  getProjectDataDir,
@@ -206,25 +228,25 @@ import {
206
228
  noteSpawnTimeout,
207
229
  resolvePiLensFlag,
208
230
  safeSpawnAsync
209
- } from "./chunk-C34ZXZ7O.js";
231
+ } from "./chunk-KUA4OJ5Y.js";
210
232
  import {
211
233
  detectFileKind
212
- } from "./chunk-PHDMFAKM.js";
234
+ } from "./chunk-DUO2TU77.js";
213
235
  import {
214
236
  classifyGeneratedOrArtifactDetailed,
215
237
  detectFileRole,
216
238
  loadPiLensProjectConfig,
217
239
  resolvePiLensConfig
218
- } from "./chunk-2BBLFX75.js";
240
+ } from "./chunk-J6MPGQTS.js";
219
241
  import {
220
242
  findNearestDirWithAnyBasename,
221
243
  findNearestDirWithMarker,
222
244
  getPiLensGlobalConfigPath
223
- } from "./chunk-XVF2VVXP.js";
245
+ } from "./chunk-5HSRT26B.js";
224
246
  import {
225
247
  getProjectTrustState,
226
248
  projectTrustDenialReason
227
- } from "./chunk-ZANLILYD.js";
249
+ } from "./chunk-M2IFVFWU.js";
228
250
  import {
229
251
  killedForOutputCap,
230
252
  truncatedByOutputCap,
@@ -233,27 +255,28 @@ import {
233
255
  import {
234
256
  incrementDegradationCount,
235
257
  recordDegradationOnce
236
- } from "./chunk-JAU72XNY.js";
258
+ } from "./chunk-J64T55EI.js";
237
259
  import {
238
260
  createSubsystemLogger,
239
261
  logExtension
240
- } from "./chunk-XTMJ7XDT.js";
262
+ } from "./chunk-POMLXPMY.js";
241
263
  import {
242
264
  logLatency,
243
265
  phaseFinished,
244
266
  phaseStarted
245
- } from "./chunk-QQNQ7SJW.js";
267
+ } from "./chunk-QBFBRV2B.js";
246
268
  import {
247
269
  createNdjsonLogger,
248
270
  getGlobalPiLensLogDir,
249
271
  getMaxLogSizeMB,
250
272
  isTestMode
251
- } from "./chunk-AOHZU2BM.js";
273
+ } from "./chunk-YNE3RP52.js";
252
274
  import {
253
275
  FactStore,
254
276
  setFactStoreEvictionReporter
255
- } from "./chunk-CHOW74WE.js";
277
+ } from "./chunk-CYRKQRPM.js";
256
278
  import {
279
+ BoundedFifoMap,
257
280
  BoundedLruCache
258
281
  } from "./chunk-ABBOA7UD.js";
259
282
  import {
@@ -266,7 +289,7 @@ import {
266
289
  normalizeMapKey,
267
290
  pathsEqual,
268
291
  toProjectRelativePath
269
- } from "./chunk-N2H3ZPZY.js";
292
+ } from "./chunk-3RKP3FMS.js";
270
293
 
271
294
  // dist/clients/mcp/analyze.js
272
295
  import * as fs19 from "node:fs";
@@ -885,6 +908,62 @@ function createDiagnosticLogger() {
885
908
  import * as fs3 from "node:fs";
886
909
  import * as path4 from "node:path";
887
910
 
911
+ // dist/clients/line-alignment.js
912
+ var MAX_OWED_LINE_TEXTS = 4096;
913
+ function lineHashOf(line) {
914
+ return Number.parseInt(lineContentHash(line), 16);
915
+ }
916
+ function stampLineHashes(findings, content, isOwn) {
917
+ if (content === void 0)
918
+ return [...findings];
919
+ let lines;
920
+ return findings.map((d) => {
921
+ if (d.lineHash !== void 0 || d.line === void 0 || !isOwn(d))
922
+ return d;
923
+ lines ??= content.split(/\r?\n/);
924
+ const text = lines[d.line - 1];
925
+ return text === void 0 ? d : { ...d, lineHash: lineHashOf(text) };
926
+ });
927
+ }
928
+ function linesOfRanges(ranges) {
929
+ const lines = [];
930
+ for (const { start, end } of ranges)
931
+ for (let line = start; line <= end; line += 1)
932
+ lines.push(line);
933
+ return lines;
934
+ }
935
+ function rangesOfLines(lines) {
936
+ const sorted = [...new Set(lines)].sort((a, b) => a - b);
937
+ const ranges = [];
938
+ for (const line of sorted) {
939
+ const last = ranges.at(-1);
940
+ if (last && line === last.end + 1)
941
+ last.end = line;
942
+ else
943
+ ranges.push({ start: line, end: line });
944
+ }
945
+ return ranges;
946
+ }
947
+ function owedContentOf(ranges, lineHashes) {
948
+ if (lineHashes === void 0)
949
+ return "whole-file";
950
+ const texts = /* @__PURE__ */ new Set();
951
+ for (const line of linesOfRanges(ranges)) {
952
+ const hash = lineHashes[line - 1];
953
+ if (hash !== void 0)
954
+ texts.add(hash);
955
+ if (texts.size > MAX_OWED_LINE_TEXTS)
956
+ return "whole-file";
957
+ }
958
+ return Uint32Array.from(texts).sort();
959
+ }
960
+ function owedByContentOnly(anchor) {
961
+ const owedContent = new Map(anchor?.owedContent ?? []);
962
+ for (const [runnerId, ranges] of anchor?.owedRanges ?? [])
963
+ owedContent.set(runnerId, owedContentOf(ranges, anchor?.lineHashes));
964
+ return owedContent.size > 0 ? { lineHashes: void 0, owedRanges: /* @__PURE__ */ new Map(), owedContent } : void 0;
965
+ }
966
+
888
967
  // dist/clients/dispatch/rule-id-normalize.js
889
968
  import * as fs2 from "node:fs";
890
969
  import * as path3 from "node:path";
@@ -1380,8 +1459,8 @@ async function drainPendingRunnerFindings(maxWaitMs = 2e3, currentSession, curre
1380
1459
  if (maxWaitMs === 0)
1381
1460
  await Promise.resolve();
1382
1461
  if (maxWaitMs > 0 && current.some((entry) => !entry.settled)) {
1383
- await new Promise((resolve44) => {
1384
- const timer = setTimeout(resolve44, maxWaitMs);
1462
+ await new Promise((resolve45) => {
1463
+ const timer = setTimeout(resolve45, maxWaitMs);
1385
1464
  timer.unref?.();
1386
1465
  });
1387
1466
  }
@@ -1420,6 +1499,54 @@ function pendingRunnerFindingsSize() {
1420
1499
  return pending.length;
1421
1500
  }
1422
1501
 
1502
+ // dist/clients/dispatch/adopted-root-runners.js
1503
+ var ADOPTED_ROOT_RUNNER_ALLOWLIST = [
1504
+ "fish-indent",
1505
+ "php-lint"
1506
+ ];
1507
+ var ADMITTED = new Set(ADOPTED_ROOT_RUNNER_ALLOWLIST);
1508
+ function isAdoptedRootRunnerAdmitted(runnerId) {
1509
+ return ADMITTED.has(runnerId);
1510
+ }
1511
+ function filterGroupsForAdoptedRoot(groups) {
1512
+ const filtered = [];
1513
+ for (const group of groups) {
1514
+ const runnerIds = [];
1515
+ for (const runnerId of group.runnerIds) {
1516
+ if (isAdoptedRootRunnerAdmitted(runnerId))
1517
+ runnerIds.push(runnerId);
1518
+ }
1519
+ if (runnerIds.length === 0)
1520
+ continue;
1521
+ filtered.push(runnerIds.length === group.runnerIds.length ? group : { ...group, runnerIds });
1522
+ }
1523
+ return filtered;
1524
+ }
1525
+
1526
+ // dist/clients/dispatch/lane-copies.js
1527
+ function semanticRank(semantic) {
1528
+ if (semantic === "blocking")
1529
+ return 4;
1530
+ if (semantic === "warning")
1531
+ return 3;
1532
+ if (semantic === "fixed")
1533
+ return 2;
1534
+ if (semantic === "silent")
1535
+ return 1;
1536
+ return 0;
1537
+ }
1538
+ function laneKey(d) {
1539
+ return `${d.line ?? 1}\0${normalizeRuleId(d.rule ?? "")}`;
1540
+ }
1541
+ function laneCopyStandsFor(lane) {
1542
+ const rankByKey = /* @__PURE__ */ new Map();
1543
+ for (const d of lane) {
1544
+ const key2 = laneKey(d);
1545
+ rankByKey.set(key2, Math.max(rankByKey.get(key2) ?? 0, semanticRank(d.semantic)));
1546
+ }
1547
+ return (d) => (rankByKey.get(laneKey(d)) ?? -1) >= semanticRank(d.semantic);
1548
+ }
1549
+
1423
1550
  // dist/clients/dispatch/rule-policy.js
1424
1551
  function resolvedRulePolicyMap(root) {
1425
1552
  const resolved = resolvePiLensConfig({
@@ -1551,6 +1678,58 @@ function getToolProfile(tool, defectClass) {
1551
1678
  return TOOL_PROFILE_MAP[t] ?? DEFAULT_TOOL_PROFILE;
1552
1679
  }
1553
1680
 
1681
+ // dist/clients/dispatch/pre-edit-seed.js
1682
+ var PRE_EDIT_SEED_RUNNER_IDS = /* @__PURE__ */ new Set([
1683
+ "actionlint",
1684
+ "biome-check-json",
1685
+ "credo",
1686
+ "detekt",
1687
+ "eslint",
1688
+ "golangci-lint",
1689
+ "hadolint",
1690
+ "htmlhint",
1691
+ "ktlint",
1692
+ "markdownlint",
1693
+ "oxlint",
1694
+ "psscriptanalyzer",
1695
+ "rubocop",
1696
+ "ruff-lint",
1697
+ "rust-clippy",
1698
+ "shellcheck",
1699
+ "spellcheck",
1700
+ "sqlfluff",
1701
+ "stylelint",
1702
+ "swiftlint",
1703
+ "tflint",
1704
+ "vale",
1705
+ "yamllint"
1706
+ ]);
1707
+ function preEditSeedFactKey(filePath) {
1708
+ return `session.preEdit.${filePath}`;
1709
+ }
1710
+ function classifyPreExisting(seed, current, content, answered) {
1711
+ let lines;
1712
+ const hashAt = (line) => {
1713
+ if (line === void 0)
1714
+ return void 0;
1715
+ lines ??= content.split(/\r?\n/);
1716
+ const text = lines[line - 1];
1717
+ return text === void 0 ? void 0 : lineHashOf(text);
1718
+ };
1719
+ const candidates = current.filter((d) => d.runnerId !== void 0 && PRE_EDIT_SEED_RUNNER_IDS.has(d.runnerId));
1720
+ const { kept } = retainRecordedFindings(seed, candidates, {
1721
+ positionOf: () => ANY_POSITION,
1722
+ keepUnobserved: (d) => d.runnerId === void 0 || !answered.has(d.runnerId),
1723
+ matchesAnywhere: (prior, observed2) => prior.lineHash !== void 0 && prior.lineHash === hashAt(observed2.line)
1724
+ });
1725
+ const observed = new Set(candidates);
1726
+ const preExisting = kept.filter((d) => observed.has(d));
1727
+ return {
1728
+ preExisting,
1729
+ nextSeed: stampLineHashes(kept, content, (d) => observed.has(d))
1730
+ };
1731
+ }
1732
+
1554
1733
  // dist/clients/dispatch/utils/format-utils.js
1555
1734
  var EMOJI = {
1556
1735
  blocking: "\u{1F534}",
@@ -1805,6 +1984,9 @@ function createDispatchContext(filePath, cwd, pi, facts, blockingOnly, modifiedR
1805
1984
  }
1806
1985
  };
1807
1986
  }
1987
+ function deltaBaselineFactKey(filePath) {
1988
+ return `session.baseline.${filePath}`;
1989
+ }
1808
1990
  function filterDelta(after, before, keyFn) {
1809
1991
  const beforeSet = new Set((before ?? []).map(keyFn));
1810
1992
  const afterSet = new Set(after.map(keyFn));
@@ -1812,17 +1994,6 @@ function filterDelta(after, before, keyFn) {
1812
1994
  const newItems = after.filter((d) => !beforeSet.has(keyFn(d)));
1813
1995
  return { new: newItems, fixed };
1814
1996
  }
1815
- function semanticRank(semantic) {
1816
- if (semantic === "blocking")
1817
- return 4;
1818
- if (semantic === "warning")
1819
- return 3;
1820
- if (semantic === "fixed")
1821
- return 2;
1822
- if (semantic === "silent")
1823
- return 1;
1824
- return 0;
1825
- }
1826
1997
  function toolPriority(tool, defectClass) {
1827
1998
  return getToolProfile(tool, defectClass).dedupPriority;
1828
1999
  }
@@ -1921,14 +2092,14 @@ function promoteDeltaUnusedToBlockers(diagnostics) {
1921
2092
  }
1922
2093
  function buildCoverageNotice(ctx, runnerLatencies, dedupe) {
1923
2094
  if (!ctx.kind)
1924
- return void 0;
2095
+ return {};
1925
2096
  const lspEnabled = !ctx.pi.getFlag("no-lsp");
1926
2097
  const primary2 = getPrimaryDispatchGroup(ctx.kind, lspEnabled);
1927
2098
  if (!primary2 || primary2.runnerIds.length === 0)
1928
- return void 0;
2099
+ return {};
1929
2100
  const relevant = runnerLatencies.filter((r) => primary2.runnerIds.includes(r.runnerId));
1930
2101
  if (relevant.length === 0)
1931
- return void 0;
2102
+ return {};
1932
2103
  const unconfirmedServerIds = [
1933
2104
  ...new Set(relevant.flatMap((r) => r.unconfirmedServerIds ?? []))
1934
2105
  ];
@@ -1945,7 +2116,7 @@ function buildCoverageNotice(ctx, runnerLatencies, dedupe) {
1945
2116
  const onceKey2 = `${ctx.kind}:${ctx.filePath}:${dedupeSet(silentServerIds)}|${dedupeSet(deferredServerIds)}`;
1946
2117
  if (dedupe) {
1947
2118
  if (coverageNoticeSeen.has(onceKey2))
1948
- return void 0;
2119
+ return {};
1949
2120
  coverageNoticeSeen.add(onceKey2);
1950
2121
  }
1951
2122
  const coverageParts = [];
@@ -1956,20 +2127,22 @@ function buildCoverageNotice(ctx, runnerLatencies, dedupe) {
1956
2127
  coverageParts.push(`coverage: ${markerFor(silentServerIds)} silent \u2014 diagnostics are incomplete (not a clean result).`);
1957
2128
  }
1958
2129
  return {
1959
- id: `coverage-partial:${ctx.kind}:${path4.basename(ctx.filePath)}`,
1960
- message: coverageParts.join("\n"),
1961
- filePath: ctx.filePath,
1962
- severity: "warning",
1963
- semantic: "warning",
1964
- tool: "pi-lens"
2130
+ notice: {
2131
+ id: `coverage-partial:${ctx.kind}:${path4.basename(ctx.filePath)}`,
2132
+ message: coverageParts.join("\n"),
2133
+ filePath: ctx.filePath,
2134
+ severity: "warning",
2135
+ semantic: "warning",
2136
+ tool: "pi-lens"
2137
+ }
1965
2138
  };
1966
2139
  }
1967
2140
  const primaryHasCoverage = relevant.some(hasUsableResult);
1968
2141
  if (primaryHasCoverage)
1969
- return void 0;
2142
+ return {};
1970
2143
  const primaryStillInFlight = relevant.some((r) => r.status === "pending" || r.status === "deferred");
1971
2144
  if (primaryStillInFlight)
1972
- return void 0;
2145
+ return {};
1973
2146
  const plan = getToolPlan(ctx.kind);
1974
2147
  const fallbackRunnerIds = new Set((plan?.groups ?? []).filter((group) => !group.runnerIds.every((runnerId) => primary2.runnerIds.includes(runnerId))).flatMap((group) => group.runnerIds).filter((runnerId) => !primary2.runnerIds.includes(runnerId)));
1975
2148
  const STRUCTURAL_RUNNERS = /* @__PURE__ */ new Set([
@@ -1982,20 +2155,48 @@ function buildCoverageNotice(ctx, runnerLatencies, dedupe) {
1982
2155
  const TYPE_CAPABLE_FALLBACK_RUNNERS = /* @__PURE__ */ new Set(["mypy"]);
1983
2156
  const anyFallbackHasCoverage = runnerLatencies.some((r) => fallbackRunnerIds.has(r.runnerId) && !STRUCTURAL_RUNNERS.has(r.runnerId) && hasUsableResult(r) && (!plan?.capabilities.includes("types") || TYPE_CAPABLE_FALLBACK_RUNNERS.has(r.runnerId)));
1984
2157
  if (anyFallbackHasCoverage)
1985
- return void 0;
2158
+ return {};
2159
+ const cause = combinePrimaryUnavailableCauses(relevant.flatMap((r) => r.primaryUnavailableCause ?? []));
2160
+ const unconfirmedStatus = cause ? `no language server answered (${PRIMARY_UNAVAILABLE_CAUSE[cause]})` : "not fully checked (a language tool is missing, timed out, or failed to run)";
2161
+ if (cause === "disabled" || cause === "disabledFallback") {
2162
+ const disabledIds = [
2163
+ ...new Set(relevant.flatMap((r) => r.disabledPrimaryServerIds ?? []))
2164
+ ].sort((a, b) => Number(a > b) - Number(a < b));
2165
+ const onceKeys = disabledIds.map((id) => `disabled-primary:${id}`);
2166
+ if (dedupe) {
2167
+ if (onceKeys.every((key2) => coverageNoticeSeen.has(key2)))
2168
+ return { unconfirmedStatus };
2169
+ for (const key2 of onceKeys)
2170
+ coverageNoticeSeen.add(key2);
2171
+ }
2172
+ return {
2173
+ notice: {
2174
+ id: `coverage-disabled:${ctx.kind}:${disabledIds.join(",")}`,
2175
+ message: cause === "disabled" ? `Pi-lens ${ctx.kind} analysis unconfirmed \u2014 Primary LSP (${disabledIds.join(", ")}) is disabled in config by lsp.disabledServers, so no language server checks these files this session (not a clean result).` : `Pi-lens ${ctx.kind} analysis unconfirmed \u2014 Primary LSP (${disabledIds.join(", ")}) is disabled in config by lsp.disabledServers and its fallback did not answer, so no language server checked these files (not a clean result).`,
2176
+ filePath: ctx.filePath,
2177
+ severity: "warning",
2178
+ semantic: "warning",
2179
+ tool: "pi-lens"
2180
+ },
2181
+ unconfirmedStatus
2182
+ };
2183
+ }
1986
2184
  const onceKey = `${ctx.kind}:${ctx.filePath}`;
1987
2185
  if (dedupe) {
1988
2186
  if (coverageNoticeSeen.has(onceKey))
1989
- return void 0;
2187
+ return { unconfirmedStatus };
1990
2188
  coverageNoticeSeen.add(onceKey);
1991
2189
  }
1992
2190
  return {
1993
- id: `coverage-unavailable:${ctx.kind}:${path4.basename(ctx.filePath)}`,
1994
- message: `Pi-lens ${ctx.kind} analysis unavailable \u2014 a language tool is missing, timed out, or failed to run, or the LSP server isn't ready yet, so this file was not fully checked (not a clean result).`,
1995
- filePath: ctx.filePath,
1996
- severity: "warning",
1997
- semantic: "warning",
1998
- tool: "pi-lens"
2191
+ notice: {
2192
+ id: `coverage-unavailable:${ctx.kind}:${path4.basename(ctx.filePath)}`,
2193
+ message: `Pi-lens ${ctx.kind} analysis unavailable \u2014 a language tool is missing, timed out, or failed to run, or the LSP server isn't ready yet, so this file was not fully checked (not a clean result).`,
2194
+ filePath: ctx.filePath,
2195
+ severity: "warning",
2196
+ semantic: "warning",
2197
+ tool: "pi-lens"
2198
+ },
2199
+ unconfirmedStatus
1999
2200
  };
2000
2201
  }
2001
2202
  var latencyReports = [];
@@ -2008,10 +2209,29 @@ function clearCoverageNoticeState() {
2008
2209
  coverageNoticeSeen.clear();
2009
2210
  generatedSkipRecorded.clear();
2010
2211
  }
2212
+ function runnerDispatchContext(ctx, runnerId) {
2213
+ const owed = ctx.owedRanges?.get(runnerId);
2214
+ if (!owed?.length || !ctx.modifiedRanges)
2215
+ return ctx;
2216
+ return {
2217
+ ...ctx,
2218
+ modifiedRanges: rangesOfLines([
2219
+ ...linesOfRanges(ctx.modifiedRanges),
2220
+ ...linesOfRanges(owed)
2221
+ ])
2222
+ };
2223
+ }
2011
2224
  async function runGroup(ctx, group, registry, onRunnerResult) {
2012
2225
  const diagnostics = [];
2013
2226
  const latencies = [];
2014
2227
  let hadBlocker = false;
2228
+ const outOfEditScope = [];
2229
+ const inconclusiveRunnerIds = [];
2230
+ const inconclusiveServerIds = [];
2231
+ const collectLaterRunnerIds = [];
2232
+ const editScopedRunnerIds = [];
2233
+ const answeredRunnerIds = [];
2234
+ const coveredRunners = [];
2015
2235
  const runnerIds = group.filterKinds ? group.runnerIds.filter((id) => {
2016
2236
  const runner = registry.get(id);
2017
2237
  return runner && ctx.kind && group.filterKinds?.includes(ctx.kind);
@@ -2093,6 +2313,7 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
2093
2313
  } catch (error) {
2094
2314
  ctx.log(`Runner ${runner.id} precondition failed: ${error}`);
2095
2315
  shouldRun = false;
2316
+ inconclusiveRunnerIds.push(runner.id);
2096
2317
  }
2097
2318
  }
2098
2319
  if (!shouldRun) {
@@ -2117,10 +2338,11 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
2117
2338
  continue;
2118
2339
  }
2119
2340
  const projectRoot = ctx.projectRoot ?? ctx.cwd;
2341
+ const runnerCtx = runnerDispatchContext(ctx, runner.id);
2120
2342
  const observedTier = ctx.writeIndex === void 0 ? "inline" : classifyObservedRunner(projectRoot, runner.id);
2121
2343
  if (observedTier === "collect-later") {
2122
2344
  const markedAtMs = Date.now();
2123
- const deferred = runRunner(ctx, runner, semantic).then((result2) => {
2345
+ const deferred = runRunner(runnerCtx, runner, semantic).then((result2) => {
2124
2346
  const durationMs = Date.now() - markedAtMs;
2125
2347
  const tier2 = observeRunnerLatency({
2126
2348
  projectRoot,
@@ -2187,6 +2409,8 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
2187
2409
  semantic
2188
2410
  });
2189
2411
  recordRunner(ctx.filePath, runner.id, "pending", 0, 0, ctx.writeIndex);
2412
+ inconclusiveRunnerIds.push(runner.id);
2413
+ collectLaterRunnerIds.push(runner.id);
2190
2414
  logLatency({
2191
2415
  type: "runner",
2192
2416
  filePath: ctx.filePath,
@@ -2199,7 +2423,7 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
2199
2423
  });
2200
2424
  continue;
2201
2425
  }
2202
- const result = await runRunner(ctx, runner, semantic);
2426
+ const result = await runRunner(runnerCtx, runner, semantic);
2203
2427
  onRunnerResult?.(runnerId, result);
2204
2428
  const runnerEnd = Date.now();
2205
2429
  const duration = runnerEnd - runnerStart;
@@ -2254,6 +2478,12 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
2254
2478
  },
2255
2479
  ...result.deferredServerIds !== void 0 && {
2256
2480
  deferredServerIds: result.deferredServerIds
2481
+ },
2482
+ ...result.primaryUnavailableCause !== void 0 && {
2483
+ primaryUnavailableCause: result.primaryUnavailableCause
2484
+ },
2485
+ ...result.disabledPrimaryServerIds !== void 0 && {
2486
+ disabledPrimaryServerIds: result.disabledPrimaryServerIds
2257
2487
  }
2258
2488
  });
2259
2489
  logLatency({
@@ -2273,7 +2503,28 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
2273
2503
  })) : void 0,
2274
2504
  metadata: failureRowMetadata(result) ?? (skipReason ? { skipReason, ...claimSource !== void 0 && { claimSource } } : void 0)
2275
2505
  });
2276
- recordRunner(ctx.filePath, runnerId, result.status, result.diagnostics.length, duration, ctx.writeIndex);
2506
+ if (!ctx.preEditCapture)
2507
+ recordRunner(ctx.filePath, runnerId, result.status, result.diagnostics.length, duration, ctx.writeIndex);
2508
+ for (const d of result.diagnostics)
2509
+ d.runnerId ??= runnerId;
2510
+ for (const d of result.outOfEditScope ?? []) {
2511
+ d.runnerId ??= runnerId;
2512
+ outOfEditScope.push(d);
2513
+ }
2514
+ if (reachedNoVerdict(result))
2515
+ inconclusiveRunnerIds.push(runnerId);
2516
+ else {
2517
+ answeredRunnerIds.push(runnerId);
2518
+ if (result.coveredBy)
2519
+ coveredRunners.push({
2520
+ runnerId,
2521
+ lane: result.coveredBy,
2522
+ ran: result.skipReason !== "covered-by-primary"
2523
+ });
2524
+ if (result.editScopedVerdict === true)
2525
+ editScopedRunnerIds.push(runnerId);
2526
+ }
2527
+ inconclusiveServerIds.push(...result.unconfirmedServerIds ?? [], ...result.deferredServerIds ?? []);
2277
2528
  diagnostics.push(...result.diagnostics);
2278
2529
  const resultSemantic = result.semantic ?? semantic;
2279
2530
  if (resultSemantic === "blocking" && result.diagnostics.length > 0 || result.diagnostics.some((d) => d.semantic === "blocking")) {
@@ -2283,22 +2534,50 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
2283
2534
  break;
2284
2535
  }
2285
2536
  }
2286
- return { diagnostics, latencies, hadBlocker };
2537
+ return {
2538
+ diagnostics,
2539
+ latencies,
2540
+ hadBlocker,
2541
+ outOfEditScope,
2542
+ inconclusiveRunnerIds,
2543
+ inconclusiveServerIds,
2544
+ collectLaterRunnerIds,
2545
+ editScopedRunnerIds,
2546
+ answeredRunnerIds,
2547
+ coveredRunners
2548
+ };
2287
2549
  }
2288
2550
  async function dispatchForFile(ctx, groups, registry, onRunnerResult, options) {
2289
2551
  const _overallStart = Date.now();
2290
2552
  if (ctx.analysisRootMode === "adopted") {
2291
- return {
2292
- diagnostics: [],
2293
- blockers: [],
2294
- warnings: [],
2295
- baselineWarningCount: 0,
2296
- fixed: [],
2297
- resolvedCount: 0,
2298
- output: "",
2299
- blockerOutput: "",
2300
- hasBlockers: false
2301
- };
2553
+ const planned = groups.flatMap((g) => g.runnerIds);
2554
+ groups = filterGroupsForAdoptedRoot(groups);
2555
+ const admitted = new Set(groups.flatMap((g) => g.runnerIds));
2556
+ logLatency({
2557
+ type: "phase",
2558
+ filePath: ctx.filePath,
2559
+ phase: "dispatch_adopted_root_allowlist",
2560
+ durationMs: 0,
2561
+ metadata: {
2562
+ admissionList: ADOPTED_ROOT_RUNNER_ALLOWLIST.join(","),
2563
+ admitted: groups.flatMap((g) => g.runnerIds).join(","),
2564
+ refused: planned.flatMap((id) => admitted.has(id) ? [] : [id]).join(","),
2565
+ noAdmittedRunner: groups.length === 0
2566
+ }
2567
+ });
2568
+ if (groups.length === 0) {
2569
+ return {
2570
+ diagnostics: [],
2571
+ blockers: [],
2572
+ warnings: [],
2573
+ baselineWarningCount: 0,
2574
+ fixed: [],
2575
+ resolvedCount: 0,
2576
+ output: "",
2577
+ blockerOutput: "",
2578
+ hasBlockers: false
2579
+ };
2580
+ }
2302
2581
  }
2303
2582
  if (ctx.fileRole === "generated") {
2304
2583
  const evidence = ctx.generatedEvidence;
@@ -2340,7 +2619,8 @@ async function dispatchForFile(ctx, groups, registry, onRunnerResult, options) {
2340
2619
  metadata: {
2341
2620
  groupCount: groups.length,
2342
2621
  kind: ctx.kind,
2343
- runners: allRunnerIds.join(",")
2622
+ runners: allRunnerIds.join(","),
2623
+ ...ctx.preEditCapture && { preEditCapture: true }
2344
2624
  }
2345
2625
  });
2346
2626
  const groupResults = await Promise.all(groups.map((group) => runGroup(ctx, group, registry, onRunnerResult)));
@@ -2352,17 +2632,51 @@ async function dispatchForFile(ctx, groups, registry, onRunnerResult, options) {
2352
2632
  reason: "dispatchForFile requires an absolute ctx.filePath to key the delta baseline (refs #2489)"
2353
2633
  });
2354
2634
  }
2355
- const baselineAbsKey = `session.baseline.${ctx.filePath}`;
2635
+ const baselineAbsKey = deltaBaselineFactKey(ctx.filePath);
2356
2636
  const previousBaseline = ctx.deltaMode && baselinePathSafe ? ctx.facts.getBoundedSessionFact(baselineAbsKey) : void 0;
2357
2637
  const baselineWarnings = previousBaseline?.filter((d) => d.semantic === "warning" || d.semantic === "none");
2358
2638
  const baselineWarningCount = baselineWarnings?.length ?? 0;
2359
- for (const { diagnostics: groupDiags, latencies, hadBlocker } of groupResults) {
2639
+ const outOfEditScope = [];
2640
+ const inconclusiveRunnerIds = /* @__PURE__ */ new Set();
2641
+ const inconclusiveServerIds = /* @__PURE__ */ new Set();
2642
+ const collectLaterRunnerIds = /* @__PURE__ */ new Set();
2643
+ const editScopedRunnerIds = [];
2644
+ const answeredRunnerIds = /* @__PURE__ */ new Set();
2645
+ const coveredRunners = [];
2646
+ for (const { diagnostics: groupDiags, latencies, hadBlocker, outOfEditScope: groupOutOfScope, inconclusiveRunnerIds: groupRunnerIds, inconclusiveServerIds: groupServerIds, collectLaterRunnerIds: groupCollectLater, editScopedRunnerIds: groupEditScoped, answeredRunnerIds: groupAnswered, coveredRunners: groupCovered } of groupResults) {
2360
2647
  runnerLatencies.push(...latencies);
2648
+ outOfEditScope.push(...groupOutOfScope);
2649
+ for (const id of groupRunnerIds)
2650
+ inconclusiveRunnerIds.add(id);
2651
+ for (const id of groupServerIds)
2652
+ inconclusiveServerIds.add(id);
2653
+ for (const id of groupCollectLater)
2654
+ collectLaterRunnerIds.add(id);
2655
+ editScopedRunnerIds.push(...groupEditScoped);
2656
+ for (const id of groupAnswered)
2657
+ answeredRunnerIds.add(id);
2658
+ coveredRunners.push(...groupCovered);
2361
2659
  allDiagnostics.push(...groupDiags);
2362
2660
  if (hadBlocker)
2363
2661
  stopped = true;
2364
2662
  }
2365
- const dedupedDiagnostics = dedupeOverlappingDiagnostics(allDiagnostics);
2663
+ const voidedCovers = /* @__PURE__ */ new Set();
2664
+ const laneCopyByRunner = /* @__PURE__ */ new Map();
2665
+ for (const { runnerId, lane, ran } of coveredRunners) {
2666
+ const silent = !answeredRunnerIds.has(lane.runnerId) || lane.serverId !== void 0 && inconclusiveServerIds.has(lane.serverId);
2667
+ if (ran) {
2668
+ if (silent)
2669
+ continue;
2670
+ laneCopyByRunner.set(runnerId, laneCopyStandsFor(allDiagnostics.flatMap((d) => d.runnerId === lane.runnerId && (lane.serverId === void 0 || d.serverId === lane.serverId) ? [d] : [])));
2671
+ continue;
2672
+ }
2673
+ if (!silent)
2674
+ continue;
2675
+ voidedCovers.add(runnerId);
2676
+ inconclusiveRunnerIds.add(runnerId);
2677
+ }
2678
+ const deliveredDiagnostics = laneCopyByRunner.size === 0 ? allDiagnostics : allDiagnostics.flatMap((d) => laneCopyByRunner.get(d.runnerId ?? "")?.(d) ? [] : [d]);
2679
+ const dedupedDiagnostics = dedupeOverlappingDiagnostics(deliveredDiagnostics);
2366
2680
  const fileContent = ctx.facts.getFileFact(ctx.filePath, "file.content") ?? "";
2367
2681
  const rulePolicy = ctx.rulePolicy ?? rulePolicyMapFromConfig(loadPiLensProjectConfig(ctx.projectRoot ?? ctx.cwd).rules);
2368
2682
  const applyOutputFilters = (diags) => {
@@ -2377,11 +2691,33 @@ async function dispatchForFile(ctx, groups, registry, onRunnerResult, options) {
2377
2691
  };
2378
2692
  let visibleDiagnostics = applyOutputFilters(dedupedDiagnostics);
2379
2693
  let resolvedCount = 0;
2694
+ const stillObserved = outOfEditScope.length > 0 ? applyOutputFilters(outOfEditScope).flatMap((d) => d.semantic === "blocking" ? [d] : []) : [];
2695
+ let heldBackBlockers = [];
2380
2696
  if (ctx.deltaMode && previousBaseline) {
2381
2697
  const filtered = filterDelta(visibleDiagnostics, applyOutputFilters(previousBaseline), (d) => d.id);
2698
+ const isNew = new Set(filtered.new);
2699
+ heldBackBlockers = visibleDiagnostics.filter((d) => d.semantic === "blocking" && !isNew.has(d));
2700
+ stillObserved.push(...promoteDeltaUnusedToBlockers(visibleDiagnostics.flatMap((d) => !isNew.has(d) && d.semantic !== "blocking" && isUnusedValueDiagnostic(d) ? [d] : [])));
2382
2701
  visibleDiagnostics = promoteDeltaUnusedToBlockers(filtered.new);
2383
2702
  resolvedCount = filtered.fixed.length;
2384
2703
  }
2704
+ const seedKey = preEditSeedFactKey(ctx.filePath);
2705
+ const preEditSeed = ctx.deltaMode && baselinePathSafe ? ctx.facts.getBoundedSessionFact(seedKey) : void 0;
2706
+ const seedContent = ctx.facts.getFileFact(ctx.filePath, "file.content");
2707
+ let preExisting = [];
2708
+ if (preEditSeed && seedContent !== void 0) {
2709
+ const classified = classifyPreExisting(preEditSeed, [
2710
+ ...visibleDiagnostics.filter((d) => d.semantic === "blocking"),
2711
+ ...heldBackBlockers
2712
+ ], seedContent, new Set([...answeredRunnerIds].filter((id) => !inconclusiveRunnerIds.has(id))));
2713
+ if (classified.preExisting.length > 0) {
2714
+ const held = new Set(classified.preExisting);
2715
+ visibleDiagnostics = visibleDiagnostics.filter((d) => !held.has(d));
2716
+ heldBackBlockers = heldBackBlockers.filter((d) => !held.has(d));
2717
+ preExisting = classified.preExisting;
2718
+ }
2719
+ ctx.facts.setBoundedSessionFact(seedKey, classified.nextSeed);
2720
+ }
2385
2721
  if (ctx.deltaMode && baselinePathSafe) {
2386
2722
  ctx.facts.setBoundedSessionFact(baselineAbsKey, [...dedupedDiagnostics]);
2387
2723
  }
@@ -2393,21 +2729,21 @@ async function dispatchForFile(ctx, groups, registry, onRunnerResult, options) {
2393
2729
  provider: ctx.telemetryProvider
2394
2730
  };
2395
2731
  if (fixedItems.length > 0) {
2396
- import("./chunk-6SDEP3AJ.js").then(({ appendToWorklog }) => {
2732
+ import("./chunk-JUKMK5Y4.js").then(({ appendToWorklog }) => {
2397
2733
  appendToWorklog(ctx.cwd, fixedItems, true, worklogIdentity);
2398
2734
  }).catch(() => {
2399
2735
  });
2400
2736
  }
2401
2737
  const fixableWarnings = warnings.filter((d) => d.fixable);
2402
2738
  if (fixableWarnings.length > 0) {
2403
- import("./chunk-6SDEP3AJ.js").then(({ appendToWorklog }) => {
2739
+ import("./chunk-JUKMK5Y4.js").then(({ appendToWorklog }) => {
2404
2740
  appendToWorklog(ctx.cwd, fixableWarnings, false, worklogIdentity);
2405
2741
  }).catch(() => {
2406
2742
  });
2407
2743
  }
2408
2744
  const inlineBlockers = blockers;
2409
2745
  const inlineFixed = fixedItems;
2410
- const coverageNotice = buildCoverageNotice(ctx, runnerLatencies, options?.dedupeCoverageNotice ?? true);
2746
+ const { notice: coverageNotice, unconfirmedStatus } = buildCoverageNotice(ctx, runnerLatencies, options?.dedupeCoverageNotice ?? true);
2411
2747
  const blockerOutput = formatDiagnostics(inlineBlockers, "blocking");
2412
2748
  let output = blockerOutput;
2413
2749
  output += formatDiagnostics(inlineFixed, "fixed");
@@ -2463,7 +2799,19 @@ async function dispatchForFile(ctx, groups, registry, onRunnerResult, options) {
2463
2799
  // (above, `metadata: { groupCount, kind, runners }`) fires before the
2464
2800
  // baseline read ever runs and cannot carry either value.
2465
2801
  baselineHit: previousBaseline !== void 0,
2466
- baselineWarningCount
2802
+ baselineWarningCount,
2803
+ // #4484: blockers still observed that the delta did not call new.
2804
+ heldBackBlockers: heldBackBlockers.length,
2805
+ // #4483: whether a pre-edit seed was read, and the blocking linter
2806
+ // findings it reported as pre-existing.
2807
+ preEditSeed: preEditSeed !== void 0,
2808
+ preExisting: preExisting.length,
2809
+ ...ctx.preEditCapture && { preEditCapture: true },
2810
+ // #4484 r2: findings that only keep a recorded blocker pending, and
2811
+ // the sources whose silence is no evidence.
2812
+ stillObserved: stillObserved.length,
2813
+ inconclusiveRunners: inconclusiveRunnerIds.size,
2814
+ inconclusiveServers: inconclusiveServerIds.size
2467
2815
  }
2468
2816
  });
2469
2817
  return {
@@ -2476,7 +2824,21 @@ async function dispatchForFile(ctx, groups, registry, onRunnerResult, options) {
2476
2824
  resolvedCount,
2477
2825
  output,
2478
2826
  blockerOutput,
2479
- hasBlockers: blockers.length > 0
2827
+ hasBlockers: blockers.length > 0,
2828
+ ...heldBackBlockers.length > 0 && { heldBackBlockers },
2829
+ ...preExisting.length > 0 && { preExisting },
2830
+ ...stillObserved.length > 0 && { stillObserved },
2831
+ ...editScopedRunnerIds.some((id) => !voidedCovers.has(id)) && {
2832
+ editScopedRunnerIds: editScopedRunnerIds.flatMap((id) => voidedCovers.has(id) ? [] : [id])
2833
+ },
2834
+ ...(inconclusiveRunnerIds.size > 0 || inconclusiveServerIds.size > 0) && {
2835
+ inconclusiveSources: {
2836
+ runnerIds: [...inconclusiveRunnerIds],
2837
+ serverIds: [...inconclusiveServerIds],
2838
+ collectLaterRunnerIds: [...collectLaterRunnerIds]
2839
+ }
2840
+ },
2841
+ ...unconfirmedStatus !== void 0 && { unconfirmedStatus }
2480
2842
  };
2481
2843
  }
2482
2844
  var RUNNER_TIMEOUT_MS = RUNTIME_CONFIG.dispatch.runnerTimeoutMs;
@@ -2557,6 +2919,16 @@ var DEFAULT_NEIGHBOUR_BUDGET = 40;
2557
2919
  var CASCADE_NEIGHBOUR_BUDGET = Math.max(MIN_NEIGHBOUR_BUDGET, Number.parseInt(process.env.PI_LENS_CASCADE_NEIGHBOUR_BUDGET ?? "40", 10) || DEFAULT_NEIGHBOUR_BUDGET);
2558
2920
  var NEIGHBOUR_BUDGET_OVERRIDDEN = process.env.PI_LENS_CASCADE_NEIGHBOUR_BUDGET !== void 0 && CASCADE_NEIGHBOUR_BUDGET !== DEFAULT_NEIGHBOUR_BUDGET;
2559
2921
 
2922
+ // dist/clients/dispatch/runners/utils.js
2923
+ import * as fs4 from "node:fs";
2924
+ function readFileContent(filePath) {
2925
+ try {
2926
+ return fs4.readFileSync(filePath, "utf-8");
2927
+ } catch {
2928
+ return void 0;
2929
+ }
2930
+ }
2931
+
2560
2932
  // dist/clients/dispatch/utils/lsp-diagnostics.js
2561
2933
  function convertLspDiagnostics(diags, filePath, options = {}) {
2562
2934
  const tool = options.tool ?? "lsp";
@@ -2585,7 +2957,10 @@ function convertLspDiagnostics(diags, filePath, options = {}) {
2585
2957
  autoFixAvailable: false,
2586
2958
  fixKind: hasSuggestion ? "suggestion" : void 0,
2587
2959
  fixSuggestion: options.fixSuggestionByIndex?.get(idx),
2588
- scanOrigin: options.scanOrigin
2960
+ scanOrigin: options.scanOrigin,
2961
+ // #4484 r2: the publishing server, so a run that leaves it
2962
+ // unconfirmed keeps its recorded blockers. Never identity.
2963
+ ...d.serverId !== void 0 && { serverId: d.serverId }
2589
2964
  };
2590
2965
  });
2591
2966
  }
@@ -2599,7 +2974,8 @@ function applyFindingPolicy(diagnostics, options) {
2599
2974
  root: options.cwd,
2600
2975
  filePath: options.filePath
2601
2976
  }),
2602
- inlineKept
2977
+ inlineKept,
2978
+ disposed
2603
2979
  };
2604
2980
  }
2605
2981
  function loadProjectRulePolicyMap(cwd) {
@@ -2664,6 +3040,28 @@ function applyPushedFindingPolicy(findings, options) {
2664
3040
  });
2665
3041
  return { kept, suppressed: findings.length - kept.length };
2666
3042
  }
3043
+ function applyPushedFindingPolicyByFile(findings, cwd) {
3044
+ const byFile = /* @__PURE__ */ new Map();
3045
+ for (const finding of findings) {
3046
+ const group = byFile.get(finding.filePath);
3047
+ if (group)
3048
+ group.push(finding);
3049
+ else
3050
+ byFile.set(finding.filePath, [finding]);
3051
+ }
3052
+ const kept = [];
3053
+ let suppressed = 0;
3054
+ for (const [filePath, group] of byFile) {
3055
+ const policy = applyPushedFindingPolicy(group, {
3056
+ cwd,
3057
+ filePath,
3058
+ content: readFileContent(filePath)
3059
+ });
3060
+ kept.push(...policy.kept);
3061
+ suppressed += policy.suppressed;
3062
+ }
3063
+ return { kept, suppressed };
3064
+ }
2667
3065
  function expandRenderedIdentities(rendered) {
2668
3066
  const out = [];
2669
3067
  const seen = /* @__PURE__ */ new Set();
@@ -2774,6 +3172,12 @@ function formatCacheAgeLabel(scannedAt, nowMs = Date.now()) {
2774
3172
  const remMinutes = ageMinutes % 60;
2775
3173
  return remMinutes === 0 ? `scanned ${ageHours}h ago` : `scanned ${ageHours}h ${remMinutes}m ago`;
2776
3174
  }
3175
+ function dispositionSuppressedLine(suppressed, breakdown) {
3176
+ if (suppressed <= 0)
3177
+ return void 0;
3178
+ const lanes = breakdown ? ` (${breakdown})` : "";
3179
+ return `suppressed by disposition: ${suppressed} finding(s)${lanes} dropped from this result because they're marked false-positive/won't-fix, disabled by \`.pi-lens.json\` rule policy, or suppressed by an inline \`pi-lens-ignore\` comment. Not a clean verdict for those locations \u2014 they were found, then intentionally hidden.`;
3180
+ }
2777
3181
  function admitsProjectWideFindings(sessionRole) {
2778
3182
  return sessionRole === "primary";
2779
3183
  }
@@ -2887,6 +3291,17 @@ var DELIVERY_SURFACES = {
2887
3291
  { evidenceMin: 2 }
2888
3292
  ),
2889
3293
  "runtime-turn:cascade-blocker": labeled(RUNTIME_TURN_FILE, "Turn-end \u{1F9EA} cascade neighbor blocker.", "Cascade results settle synchronously this turn where possible (`settleCascadeRuns`), but an unsettled compute can carry over to a later turn (bounded by a carry cap) \u2014 a carried-over result renders with an explicit `(carried N turns \xB7 scanned Xm ago)` label (#3167, #3168 F3), so the agent can tell it from a fresh observation.", "live", ["cascadeCarrySuffix("], { evidenceMin: 1 }),
3294
+ // #4487: cascade neighbour errors a cascade showed in an EARLIER turn and
3295
+ // no later cascade or edit has shown gone. A cached claim, not a fresh
3296
+ // observation, so each entry passes the freshness gate against its own
3297
+ // read bound (`checkedAt`) before it renders (#4495 review F1, round 3):
3298
+ // a deleted neighbour is forgotten; one written after the check by
3299
+ // something pi never saw is named once as changed and not re-checked.
3300
+ // Survivors carry the `(scanned Xm ago)` age of that read. The evidence is
3301
+ // the two RENDERED lists, so the binding chain walks each back to the gate
3302
+ // call's own arms (a render of the ungated candidates reds, #4495 r2
3303
+ // verify census note).
3304
+ "runtime-turn:cascade-pending-errors": gated(RUNTIME_TURN_FILE, "Turn-end \u{1F4D0} list of cascade neighbour errors shown in an earlier turn and not re-checked since.", ["gateFindingsByPathFreshness"], ["unresolvedNeighbors", "changedShown"]),
2890
3305
  "runtime-turn:cascade-coverage-advisory": labeled(RUNTIME_TURN_FILE, "Turn-end cascade-coverage-gap advisories (graph/binding/budget).", "Explains what the cascade check could NOT confirm this turn \u2014 not a finding with a cited path, an absence-of-coverage disclosure computed from this turn's own indeterminate-run list. An advisory computed from a carried indeterminate run is labeled `(carried N turns \xB7 <age>)` (#3167, #3168 F4 \u2014 dropped on mixed carried/fresh buckets). The age half reads `scan age unknown` on this surface today: no indeterminate-run producer stamps `observedAt` (only the resolved-found plumb does, `clients/cascade-format.ts`), and one unstamped carried run collapses the whole bucket's age rather than claiming the stamped runs' (#3168 F13). The carry COUNT is always real.", "live", ["withCarryLabel("], { evidenceMin: 3 }),
2891
3306
  // #3102: the cold-neighbour cascade run is BUILT here, in the quiet-window
2892
3307
  // reconcile (`onResolvedFound` in index.ts), a turn earlier than the
@@ -2975,16 +3390,185 @@ var DELIVERY_SURFACES = {
2975
3390
  "read-guard-tool-lines:preflight-errors": labeled("clients/read-guard-tool-lines.ts", "Read-guard hashline BLOCKED / RE-READ REQUIRED / PARTIAL APPLY / ALREADY APPLIED preflight errors (#2402).", "Computed fresh per edit attempt and returned as that attempt's rejection \u2014 no cached findings are replayed, so there is no staleness window. The PARTIAL APPLY and ALREADY APPLIED variants describe the same attempt's commit outcome; applied-edit recognition reads RuntimeCoordinator's session-scoped record, which resets with the session.", "live"),
2976
3391
  "mutating-tool:adapter-preflight-errors": labeled("clients/mutating-tool.ts", "Shape-adapter BLOCKED preflight errors for hashline edit inputs the adapter recognized but could not resolve to a range (#2423).", "Computed fresh from the tool input on each attempt and returned as that attempt's rejection, so there is no staleness window. The adapters read only the call's own arguments; nothing cached is replayed.", "live"),
2977
3392
  "tool-call:duplicate-export-blocker": labeled("clients/runtime-tool-call.ts", "Duplicate-export STOP rejection returned inline with the failed edit.", "Synchronous preflight rejection computed per edit attempt; no stored state.", "live"),
2978
- "agent-behavior:thrashing-notice": labeled("clients/agent-behavior-client.ts", "In-result thrashing/blind-write detection notice.", "Ephemeral notice computed at detection time inside the same tool result \u2014 no persistence and no cache, so nothing can be stale.", "live")
3393
+ "agent-behavior:thrashing-notice": labeled("clients/agent-behavior-client.ts", "In-result thrashing/blind-write detection notice.", "Ephemeral notice computed at detection time inside the same tool result \u2014 no persistence and no cache, so nothing can be stale.", "live"),
3394
+ // ── #1634 enumeration round: the rest of the agent-facing sinks ─────────
3395
+ // Found by the sink census in `tests/clients/finding-delivery-gate.test.ts`,
3396
+ // which derives every production file holding an agent-facing sink (a tool
3397
+ // result's text block, a context or hook message, a tool_call block, a
3398
+ // custom entry, a status/widget line) and fails on one that is neither a
3399
+ // surface `file` here nor classified there.
3400
+ "lsp-navigation:workspace-diagnostics-path": gated(
3401
+ "tools/lsp-navigation.ts",
3402
+ "`lsp_navigation workspaceDiagnostics` with a path: the server's answer for that file, read after a touch that waits for diagnostics.",
3403
+ ["applyProbeFindingPolicy"],
3404
+ // The path read and the tracked snapshot below are the two calls.
3405
+ ["applyProbeFindingPolicy("],
3406
+ { evidenceMin: 2 }
3407
+ ),
3408
+ "lsp-navigation:tracked-snapshot": labeled("tools/lsp-navigation.ts", "`lsp_navigation workspaceDiagnostics` with no path: each tracked file's last client publish.", "A replay of what each language client last published, with no re-check of the file, so every row states its age. The rows also pass the same finding policy as the path read (`readFileContent` supplies the current content), so the label and the disposition filter compose.", "LSP client publish ts", ["lag: formatCacheAgeLabel(ts)", "readFileContent(trackedFile)"]),
3409
+ "runtime-context:persisted-findings": gated(
3410
+ "clients/runtime-context.ts",
3411
+ "Persisted turn-end and test-runner findings replayed into the next context message, the MCP turn_end tool and the Stop hook.",
3412
+ ["validateAdvisoryProvenance"],
3413
+ // peekTurnEndFindings, consumeTurnEndFindings, peekTestFindings.
3414
+ ["validateAdvisoryProvenance(findings.data, cwd, runtime)"],
3415
+ { evidenceMin: 3 }
3416
+ ),
3417
+ "index:turn-summary": labeled("index.ts", "One-line turn summary custom entry (`--lens-turn-summary`).", "Counts drained from this turn's own accumulator (`runtime.turnSummary.consume`) at the quiet window; nothing is replayed from a cache.", "live"),
3418
+ "lens-engine:project-scan": gated("clients/lens-engine.ts", "`pilens_project_scan` (MCP): a fresh cheap project scan.", ["applyPushedFindingPolicyByFile"], ["applyPushedFindingPolicyByFile(scan.diagnostics, cwd)"]),
3419
+ "mcp-server:analyze": labeled("mcp/server.ts", "`pilens_analyze` (MCP): one file's dispatch result.", "A dispatch run for this call, warm or in a fresh worker; the dispatcher applies the finding policy before it returns.", "live"),
3420
+ "mcp-analyze-cli:post-edit-report": labeled("mcp/analyze-cli.ts", "Claude Code PostToolUse hook report (`analyze-cli --hook`).", "Same per-call dispatch as `pilens_analyze`; its Stop-hook output is the `runtime-context:persisted-findings` replay or a fresh turn-end pass.", "live")
2979
3421
  };
2980
3422
 
2981
3423
  // dist/clients/cascade-format.js
3424
+ function formatCascadeNeighborDiagnostics(cwd, neighbors, options = {}) {
3425
+ const withErrors = neighbors.filter((n) => n.diagnostics.length > 0);
3426
+ const inconclusive = neighbors.filter((n) => n.inconclusive === true && n.diagnostics.length === 0);
3427
+ const uncovered = neighbors.filter((n) => n.diagnostics.length === 0 && n.inconclusive !== true && (n.unconfirmedServerIds?.length ?? 0) > 0);
3428
+ if (withErrors.length === 0 && inconclusive.length === 0 && uncovered.length === 0) {
3429
+ return "";
3430
+ }
3431
+ const noun = options.noun ?? "neighbor";
3432
+ let out = withErrors.length > 0 ? `\u{1F4D0} Cascade errors in ${withErrors.length} ${noun} file(s) \u2014 fix before finishing turn:` : "";
3433
+ for (const neighbor of withErrors) {
3434
+ const display = toRunnerDisplayPath(cwd, neighbor.filePath);
3435
+ const reason = options.includeReason ? ` reason="${neighbor.reason}"` : "";
3436
+ out += `
3437
+ <diagnostics file="${display}"${reason}>`;
3438
+ for (const d of neighbor.diagnostics) {
3439
+ const line = d.line ?? 1;
3440
+ const col = d.column ?? 1;
3441
+ const rule = d.rule ? ` rule=${d.rule}` : "";
3442
+ const since = options.stillPresentSince?.(neighbor, d);
3443
+ const label = since === void 0 ? "" : ` (still present since turn ${since})`;
3444
+ out += `
3445
+ line ${line}, col ${col}${rule}: ${d.message.split("\n")[0].slice(0, 100)}${label}`;
3446
+ }
3447
+ out += "\n</diagnostics>";
3448
+ }
3449
+ if (inconclusive.length > 0) {
3450
+ if (out)
3451
+ out += "\n";
3452
+ out += `\u26A0\uFE0F Cascade diagnostics inconclusive for ${inconclusive.length} ${noun} file(s) \u2014 no clean result was confirmed:`;
3453
+ for (const neighbor of inconclusive) {
3454
+ out += `
3455
+ ${toRunnerDisplayPath(cwd, neighbor.filePath)}`;
3456
+ }
3457
+ }
3458
+ if (uncovered.length > 0) {
3459
+ if (out)
3460
+ out += "\n";
3461
+ out += `\u26A0\uFE0F Cascade scanners did not cover ${uncovered.length} ${noun} file(s) \u2014 no findings does NOT mean clean here:`;
3462
+ for (const neighbor of uncovered) {
3463
+ const servers = (neighbor.unconfirmedServerIds ?? []).join(", ");
3464
+ out += `
3465
+ ${toRunnerDisplayPath(cwd, neighbor.filePath)} (not scanned by ${servers})`;
3466
+ }
3467
+ }
3468
+ return out;
3469
+ }
3470
+ function composeCascadeSection(block, layout, unansweredPrimaryNote = "") {
3471
+ const diagnosticsBlock = block && unansweredPrimaryNote ? `${block}
3472
+ ${unansweredPrimaryNote}` : block || unansweredPrimaryNote;
3473
+ if (!diagnosticsBlock && !layout.coverageLine)
3474
+ return "";
3475
+ let out = !diagnosticsBlock ? "" : layout.header ? `${layout.header}
3476
+ ${diagnosticsBlock}` : diagnosticsBlock;
3477
+ if (layout.truncationLine)
3478
+ out += `
3479
+ ${layout.truncationLine}`;
3480
+ if (layout.coverageLine) {
3481
+ if (out)
3482
+ out += "\n";
3483
+ out += layout.coverageLine;
3484
+ }
3485
+ if (layout.policySuppressed > 0) {
3486
+ out += `
3487
+ suppressed by disposition: ${layout.policySuppressed} finding(s) (marked false-positive or won't-fix).`;
3488
+ }
3489
+ return out;
3490
+ }
2982
3491
  function cascadeCarrySuffix(carriedTurns, observedAt) {
2983
3492
  if (carriedTurns === void 0 || carriedTurns < 1)
2984
3493
  return void 0;
2985
3494
  const noun = carriedTurns === 1 ? "turn" : "turns";
2986
3495
  return `(carried ${carriedTurns} ${noun} \xB7 ${formatCacheAgeLabel(observedAt)})`;
2987
3496
  }
3497
+ var cascadeUnansweredPrimariesDelivered = /* @__PURE__ */ new Set();
3498
+ function resetCascadeUnansweredPrimariesDelivered() {
3499
+ cascadeUnansweredPrimariesDelivered.clear();
3500
+ }
3501
+ function markCascadeUnansweredPrimariesDelivered(serverIds) {
3502
+ for (const id of serverIds)
3503
+ cascadeUnansweredPrimariesDelivered.add(id);
3504
+ }
3505
+ function renderCascadeResult(result, shownServerIds) {
3506
+ const unanswered = result.unansweredPrimaries;
3507
+ if (!unanswered)
3508
+ return { text: result.formatted, lines: [], serverIds: [] };
3509
+ const kept = unanswered.notes.filter((note) => note.serverIds.some((id) => !cascadeUnansweredPrimariesDelivered.has(id) && !shownServerIds.has(id)));
3510
+ const serverIds = kept.flatMap((note) => [...note.serverIds]);
3511
+ for (const id of serverIds)
3512
+ shownServerIds.add(id);
3513
+ return {
3514
+ text: unanswered.formattedWithoutNotes,
3515
+ lines: kept.map((note) => note.line),
3516
+ serverIds
3517
+ };
3518
+ }
3519
+ var MAX_PENDING_NEIGHBORS = 256;
3520
+ var pendingNeighbors = new BoundedFifoMap(MAX_PENDING_NEIGHBORS);
3521
+ function cascadeErrorKey(diagnostic) {
3522
+ return [
3523
+ diagnostic.id,
3524
+ diagnostic.rule ?? "",
3525
+ diagnostic.line ?? 0,
3526
+ diagnostic.column ?? 0,
3527
+ diagnostic.message
3528
+ ].join(":");
3529
+ }
3530
+ function cascadeStillPresentSince(filePath, diagnostic, turn) {
3531
+ const first = pendingNeighbors.get(normalizeMapKey(filePath))?.errors.get(cascadeErrorKey(diagnostic));
3532
+ return first !== void 0 && first < turn ? first : void 0;
3533
+ }
3534
+ function recordDeliveredCascadeNeighbor(filePath, diagnostics, turn, checkedAt) {
3535
+ const key2 = normalizeMapKey(filePath);
3536
+ const previous = pendingNeighbors.get(key2);
3537
+ const errors = /* @__PURE__ */ new Map();
3538
+ for (const d of diagnostics) {
3539
+ const k = cascadeErrorKey(d);
3540
+ errors.set(k, previous?.errors.get(k) ?? turn);
3541
+ }
3542
+ pendingNeighbors.delete(key2);
3543
+ if (errors.size === 0)
3544
+ return;
3545
+ if (pendingNeighbors.set(key2, { filePath, errors, checkedAt }).length > 0) {
3546
+ recordDegradationOnce({
3547
+ kind: "cascade-pending-neighbors-evicted",
3548
+ subject: `cap=${MAX_PENDING_NEIGHBORS}`,
3549
+ reason: "the pending cascade neighbour-error record hit its cap; the oldest neighbour's errors are no longer re-served"
3550
+ });
3551
+ }
3552
+ }
3553
+ function forgetCascadePendingNeighbor(filePath, observedAt) {
3554
+ const key2 = normalizeMapKey(filePath);
3555
+ const entry = pendingNeighbors.get(key2);
3556
+ if (!entry)
3557
+ return;
3558
+ if (observedAt !== void 0 && observedAt <= entry.checkedAt)
3559
+ return;
3560
+ pendingNeighbors.delete(key2);
3561
+ }
3562
+ function listCascadePendingNeighbors() {
3563
+ return [...pendingNeighbors.values()].map((entry) => ({
3564
+ filePath: entry.filePath,
3565
+ errorCount: entry.errors.size,
3566
+ checkedAt: entry.checkedAt
3567
+ }));
3568
+ }
3569
+ function resetCascadePendingNeighbors() {
3570
+ pendingNeighbors.clear();
3571
+ }
2988
3572
 
2989
3573
  // dist/clients/cascade-logger.js
2990
3574
  import * as path5 from "node:path";
@@ -3097,33 +3681,152 @@ function createDiagnosticTracker() {
3097
3681
  }
3098
3682
 
3099
3683
  // dist/clients/lsp/cascade-tier.js
3684
+ var MAX_OUTSTANDING_TOUCHES = 256;
3100
3685
  var OUTSTANDING_TOUCH_MAX_AGE_MS = 15 * 6e4;
3101
- var _outstandingTouches = /* @__PURE__ */ new Map();
3686
+ var _outstandingTouches = new BoundedFifoMap(MAX_OUTSTANDING_TOUCHES);
3102
3687
  var _expiredSinceLastSweep = 0;
3103
3688
  var _evictedSinceLastSweep = 0;
3689
+ var MAX_LOGGED_PATHS = 20;
3690
+ function pruneExpiredOutstandingTouches(now, guard, reads = /* @__PURE__ */ new Map()) {
3691
+ const expired = [];
3692
+ for (const [key2, touch] of _outstandingTouches) {
3693
+ if (now - touch.touchedAt <= OUTSTANDING_TOUCH_MAX_AGE_MS)
3694
+ break;
3695
+ _outstandingTouches.delete(key2);
3696
+ _expiredSinceLastSweep++;
3697
+ expired.push(touch);
3698
+ }
3699
+ if (expired.length === 0)
3700
+ return;
3701
+ let found = 0;
3702
+ let clean = 0;
3703
+ for (const touch of expired) {
3704
+ const outcome = readTouchedClient(touch, now, reads);
3705
+ if (!outcome)
3706
+ continue;
3707
+ if (outcome.outcome === "resolved-found")
3708
+ found++;
3709
+ else
3710
+ clean++;
3711
+ deliverResolvedOutcome(outcome, guard);
3712
+ }
3713
+ logCascade({
3714
+ phase: "cascade_tier3_expired",
3715
+ filePath: "<cascade-registry>",
3716
+ metadata: {
3717
+ count: expired.length,
3718
+ found,
3719
+ clean,
3720
+ unanswered: expired.length - found - clean,
3721
+ maxAgeCapMs: OUTSTANDING_TOUCH_MAX_AGE_MS,
3722
+ paths: expired.slice(0, MAX_LOGGED_PATHS).map((t) => t.filePath),
3723
+ ...expired.length > MAX_LOGGED_PATHS && {
3724
+ pathsTruncated: expired.length - MAX_LOGGED_PATHS
3725
+ }
3726
+ }
3727
+ });
3728
+ }
3729
+ function readTouchedClient(touch, now, reads) {
3730
+ const client = touch.client?.deref();
3731
+ if (!client || client.serverId !== touch.serverId)
3732
+ return void 0;
3733
+ if (client.isAlive?.() === false)
3734
+ return void 0;
3735
+ let all = reads.get(client);
3736
+ if (all === void 0) {
3737
+ all = client.getAllDiagnostics();
3738
+ reads.set(client, all);
3739
+ }
3740
+ const entry = all.get(normalizeMapKey(touch.filePath));
3741
+ if (!entry || entry.ts <= touch.touchedAt)
3742
+ return void 0;
3743
+ const found = entry.diags.length > 0;
3744
+ return {
3745
+ filePath: touch.filePath,
3746
+ serverId: touch.serverId,
3747
+ outcome: found ? "resolved-found" : "resolved-clean",
3748
+ ageMs: now - touch.touchedAt,
3749
+ diagnosticCount: entry.diags.length,
3750
+ publishedAt: entry.ts,
3751
+ touchedAt: touch.touchedAt,
3752
+ ...found && { diagnostics: entry.diags }
3753
+ };
3754
+ }
3755
+ function deliverResolvedOutcome(o, guard) {
3756
+ try {
3757
+ if (o.outcome === "resolved-found" && o.diagnostics?.length) {
3758
+ const neighbor = {
3759
+ filePath: o.filePath,
3760
+ serverId: o.serverId,
3761
+ diagnostics: o.diagnostics,
3762
+ ...o.publishedAt !== void 0 ? { publishedAt: o.publishedAt } : {},
3763
+ ...o.touchedAt !== void 0 ? { touchedAt: o.touchedAt } : {}
3764
+ };
3765
+ const deliver = () => _reconcileOptions.onResolvedFound?.(neighbor);
3766
+ if (guard)
3767
+ guard.guardedWrite(o.filePath, deliver);
3768
+ else
3769
+ deliver();
3770
+ } else if (o.outcome === "resolved-clean" && o.publishedAt != null) {
3771
+ _reconcileOptions.onResolvedClean?.({
3772
+ filePath: o.filePath,
3773
+ serverId: o.serverId,
3774
+ publishedAt: o.publishedAt
3775
+ });
3776
+ }
3777
+ } catch {
3778
+ }
3779
+ }
3104
3780
  function resetCascadeTierSessionState() {
3781
+ const dropped = _outstandingTouches.size;
3105
3782
  _outstandingTouches.clear();
3106
3783
  _expiredSinceLastSweep = 0;
3107
3784
  _evictedSinceLastSweep = 0;
3785
+ if (dropped > 0) {
3786
+ logCascade({
3787
+ phase: "cascade_tier3_dropped",
3788
+ filePath: "<session-reset>",
3789
+ metadata: { count: dropped }
3790
+ });
3791
+ }
3108
3792
  }
3793
+ function peekOutstandingCascadeTouches(guard) {
3794
+ const now = Date.now();
3795
+ const reads = /* @__PURE__ */ new Map();
3796
+ pruneExpiredOutstandingTouches(now, guard, reads);
3797
+ const count = _outstandingTouches.size;
3798
+ if (count === 0)
3799
+ return;
3800
+ let found = 0;
3801
+ let clean = 0;
3802
+ for (const [key2, touch] of _outstandingTouches) {
3803
+ const outcome = readTouchedClient(touch, now, reads);
3804
+ if (!outcome)
3805
+ continue;
3806
+ _outstandingTouches.delete(key2);
3807
+ if (outcome.outcome === "resolved-found")
3808
+ found++;
3809
+ else
3810
+ clean++;
3811
+ deliverResolvedOutcome(outcome, guard);
3812
+ }
3813
+ logCascade({
3814
+ phase: "cascade_tier3_peek",
3815
+ filePath: "<turn_end>",
3816
+ metadata: {
3817
+ count,
3818
+ found,
3819
+ clean,
3820
+ outstanding: _outstandingTouches.size
3821
+ }
3822
+ });
3823
+ }
3824
+ var _reconcileOptions = {};
3109
3825
 
3110
3826
  // dist/clients/dispatch/runners/ast-grep-napi.js
3111
- import * as fs4 from "node:fs";
3827
+ import * as fs5 from "node:fs";
3112
3828
  import * as path6 from "node:path";
3113
3829
 
3114
- // dist/clients/deps/ast-grep-napi.js
3115
- import { createRequire } from "node:module";
3116
- import { pathToFileURL } from "node:url";
3117
- var _require = createRequire(import.meta.url);
3118
- function loadAstGrepNapi() {
3119
- try {
3120
- const entry = _require.resolve("@ast-grep/napi");
3121
- return import(pathToFileURL(entry).href);
3122
- } catch {
3123
- return import("@ast-grep/napi");
3124
- }
3125
- }
3126
-
3127
3830
  // dist/clients/dispatch/priorities.js
3128
3831
  var PRIORITY = {
3129
3832
  LSP_PRIMARY: 4,
@@ -3140,6 +3843,66 @@ var PRIORITY = {
3140
3843
  DEEP_LANGUAGE_ANALYSIS: 50
3141
3844
  };
3142
3845
 
3846
+ // dist/clients/dispatch/runners/ast-grep-literal-anchors.js
3847
+ var PATTERN_WORD = /\$?[A-Za-z_]\w*/g;
3848
+ var LITERAL_REGEX = /^\^?((?:[A-Za-z0-9_"'`:\- =!<>/@#%&,;~]|\\\.)+)\$?$/;
3849
+ var RELATIONAL_KEYS = ["has", "inside", "follows", "precedes"];
3850
+ function ownLiterals(node) {
3851
+ const literals = [];
3852
+ if (typeof node.pattern === "string") {
3853
+ for (const word of node.pattern.match(PATTERN_WORD) ?? []) {
3854
+ if (!word.startsWith("$"))
3855
+ literals.push(word);
3856
+ }
3857
+ }
3858
+ const regex = typeof node.regex === "string" ? node.regex : "";
3859
+ const literal = LITERAL_REGEX.exec(regex)?.[1];
3860
+ if (literal)
3861
+ literals.push(literal.replaceAll(String.raw`\.`, "."));
3862
+ return literals;
3863
+ }
3864
+ function anyOf(branches) {
3865
+ if (branches.length === 0 || branches.includes(true))
3866
+ return true;
3867
+ return { or: branches };
3868
+ }
3869
+ function requirementOf(node, utils, resolving) {
3870
+ if (!node || typeof node !== "object")
3871
+ return true;
3872
+ const of = (child) => requirementOf(child, utils, resolving);
3873
+ const parts = [
3874
+ ...ownLiterals(node),
3875
+ ...RELATIONAL_KEYS.map((key2) => of(node[key2])),
3876
+ ...Array.isArray(node.all) ? node.all.map(of) : [],
3877
+ Array.isArray(node.any) ? anyOf(node.any.map(of)) : true
3878
+ ];
3879
+ const name = node.matches;
3880
+ if (typeof name === "string" && utils != null && Object.hasOwn(utils, name) && !resolving.has(name)) {
3881
+ parts.push(requirementOf(utils[name], utils, /* @__PURE__ */ new Set([...resolving, name])));
3882
+ }
3883
+ const required = parts.filter((part) => part !== true);
3884
+ return required.length === 0 ? true : { and: required };
3885
+ }
3886
+ function satisfied(requirement, text) {
3887
+ if (requirement === true)
3888
+ return true;
3889
+ if (typeof requirement === "string")
3890
+ return text.includes(requirement);
3891
+ if ("and" in requirement) {
3892
+ return requirement.and.every((part) => satisfied(part, text));
3893
+ }
3894
+ return requirement.or.some((part) => satisfied(part, text));
3895
+ }
3896
+ var requirements = /* @__PURE__ */ new WeakMap();
3897
+ function ruleCanMatchText(rule, text) {
3898
+ let requirement = requirements.get(rule);
3899
+ if (requirement === void 0) {
3900
+ requirement = requirementOf(rule.rule, rule.utils, /* @__PURE__ */ new Set());
3901
+ requirements.set(rule, requirement);
3902
+ }
3903
+ return satisfied(requirement, text);
3904
+ }
3905
+
3143
3906
  // dist/clients/dispatch/runners/ast-grep-napi.js
3144
3907
  var defaultUnsupportedLanguageLog = /* @__PURE__ */ new Set();
3145
3908
  var UNSUPPORTED_RULE_ID_SAMPLE_SIZE = 5;
@@ -3657,12 +4420,19 @@ function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}) {
3657
4420
  }
3658
4421
  if (!rule.rule)
3659
4422
  continue;
4423
+ if (options.content !== void 0 && !ruleCanMatchText(rule, options.content)) {
4424
+ continue;
4425
+ }
3660
4426
  try {
3661
4427
  const scopes = runsInsideHtmlScript ? htmlInjections.map((injection) => ({
3662
4428
  root: injection.root,
3663
4429
  position(match) {
3664
4430
  const start = match.range().start;
3665
4431
  return filePositionForIndex(fileLineStarts, injection.startIndex + start.index);
4432
+ },
4433
+ endLine(match) {
4434
+ const end = match.range().end;
4435
+ return filePositionForIndex(fileLineStarts, injection.startIndex + end.index).line;
3666
4436
  }
3667
4437
  })) : [
3668
4438
  {
@@ -3670,6 +4440,9 @@ function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}) {
3670
4440
  position(match) {
3671
4441
  const start = match.range().start;
3672
4442
  return { line: start.line, column: start.column };
4443
+ },
4444
+ endLine(match) {
4445
+ return match.range().end.line;
3673
4446
  }
3674
4447
  }
3675
4448
  ];
@@ -3679,7 +4452,9 @@ function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}) {
3679
4452
  if (rule.utils)
3680
4453
  nativeConfig.utils = rule.utils;
3681
4454
  const collected = [];
4455
+ const withheld = [];
3682
4456
  let rejectLogged = false;
4457
+ const editScoped = options.modifiedRanges !== void 0 && normalizeRuleSeverity(rule.severity) === "error";
3683
4458
  for (const scope of scopes) {
3684
4459
  let found;
3685
4460
  try {
@@ -3694,19 +4469,23 @@ function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}) {
3694
4469
  for (const match of found) {
3695
4470
  if (collected.length >= maxMatchesPerRule)
3696
4471
  break;
3697
- collected.push({ match, position: scope.position(match) });
4472
+ const position = scope.position(match);
4473
+ if (editScoped && !spanOverlapsModifiedRanges(position.line + 1, scope.endLine(match) + 1, options.modifiedRanges)) {
4474
+ if (withheld.length < maxMatchesPerRule)
4475
+ withheld.push({ position });
4476
+ continue;
4477
+ }
4478
+ collected.push({ match, position });
3698
4479
  }
3699
4480
  if (collected.length >= maxMatchesPerRule)
3700
4481
  break;
3701
4482
  }
3702
- for (const { position } of collected) {
3703
- if (diagnostics.length >= maxTotalDiagnostics)
3704
- break;
4483
+ const toDiagnostic3 = (position) => {
3705
4484
  const severity = normalizeRuleSeverity(rule.severity);
3706
4485
  const semantic = severity === "error" ? "blocking" : "warning";
3707
4486
  const defectClass = classifyDefect(rule.id, "ast-grep-napi", rule.message || rule.id);
3708
4487
  const ruleFix = explicitRuleFixSuggestion(rule);
3709
- diagnostics.push({
4488
+ return {
3710
4489
  id: `ast-grep-napi-${position.line}-${rule.id}`,
3711
4490
  message: `[${rule.metadata?.category || "slop"}] ${rule.message || rule.id}`,
3712
4491
  filePath,
@@ -3721,7 +4500,15 @@ function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}) {
3721
4500
  autoFixAvailable: false,
3722
4501
  fixKind: ruleFix ? "suggestion" : void 0,
3723
4502
  fixSuggestion: semantic === "blocking" ? ruleFix ?? defaultFixSuggestion(defectClass, rule.id) : ruleFix
3724
- });
4503
+ };
4504
+ };
4505
+ for (const { position } of collected) {
4506
+ if (diagnostics.length >= maxTotalDiagnostics)
4507
+ break;
4508
+ diagnostics.push(toDiagnostic3(position));
4509
+ }
4510
+ for (const { position } of withheld) {
4511
+ options.outOfEditScope?.push(toDiagnostic3(position));
3725
4512
  }
3726
4513
  if (diagnostics.length >= maxTotalDiagnostics)
3727
4514
  break;
@@ -3743,8 +4530,10 @@ var astGrepNapiRunner = {
3743
4530
  }
3744
4531
  const astGrepLspEnabled = enabledAuxiliaryLspServerIds((f) => ctx.pi?.getFlag?.(f)).includes("ast-grep");
3745
4532
  const astGrepLspPublished = astGrepLspEnabled ? await hasAuxiliaryLspPublishedForRoot("ast-grep", ctx.filePath) : false;
3746
- if (astGrepLspEnabled && astGrepLspPublished) {
3747
- if (hasPendingAuxiliaryCoverage(ctx.filePath, "ast-grep")) {
4533
+ const coveredByServer = astGrepLspEnabled && astGrepLspPublished;
4534
+ if (coveredByServer && !ctx.recordedRunnerIds?.has("ast-grep-napi")) {
4535
+ const coveragePending = hasPendingAuxiliaryCoverage(ctx.filePath, "ast-grep");
4536
+ if (coveragePending) {
3748
4537
  incrementDegradationCount({
3749
4538
  kind: "aux-runner-findings-lost",
3750
4539
  subject: "ast-grep",
@@ -3753,13 +4542,23 @@ var astGrepNapiRunner = {
3753
4542
  reason: `Gate B skipped napi: pending late-auxiliary pair from an EARLIER touch remains undelivered while prior publication satisfies per-file gate (file: ${ctx.filePath})`
3754
4543
  });
3755
4544
  }
3756
- return { status: "skipped", diagnostics: [], semantic: "none" };
4545
+ return {
4546
+ status: "skipped",
4547
+ diagnostics: [],
4548
+ semantic: "none",
4549
+ // Round 7: the verdict holds only if the server's host answered.
4550
+ ...coveragePending ? {} : {
4551
+ skipReason: "covered-by-primary",
4552
+ coveredBy: { runnerId: "lsp", serverId: "ast-grep" }
4553
+ },
4554
+ ...ctx.blockingOnly && ctx.modifiedRanges?.length ? { editScopedVerdict: true } : {}
4555
+ };
3757
4556
  }
3758
4557
  const sgModule = await loadSg();
3759
4558
  if (!sgModule) {
3760
4559
  return { status: "skipped", diagnostics: [], semantic: "none" };
3761
4560
  }
3762
- if (!fs4.existsSync(ctx.filePath)) {
4561
+ if (!fs5.existsSync(ctx.filePath)) {
3763
4562
  return { status: "skipped", diagnostics: [], semantic: "none" };
3764
4563
  }
3765
4564
  const lang = getLang(ctx.filePath, sgModule);
@@ -3768,7 +4567,7 @@ var astGrepNapiRunner = {
3768
4567
  }
3769
4568
  let stats;
3770
4569
  try {
3771
- stats = fs4.statSync(ctx.filePath);
4570
+ stats = fs5.statSync(ctx.filePath);
3772
4571
  } catch {
3773
4572
  return { status: "skipped", diagnostics: [], semantic: "none" };
3774
4573
  }
@@ -3781,7 +4580,7 @@ var astGrepNapiRunner = {
3781
4580
  content = contentFromFacts;
3782
4581
  } else {
3783
4582
  try {
3784
- content = fs4.readFileSync(ctx.filePath, "utf-8");
4583
+ content = fs5.readFileSync(ctx.filePath, "utf-8");
3785
4584
  } catch {
3786
4585
  return { status: "skipped", diagnostics: [], semantic: "none" };
3787
4586
  }
@@ -3803,10 +4602,16 @@ var astGrepNapiRunner = {
3803
4602
  recordNapiFallbackCoverage(ctx.filePath);
3804
4603
  clearPendingAuxiliaryCoverage(ctx.filePath, "ast-grep");
3805
4604
  }
4605
+ const outOfEditScope = [];
3806
4606
  const diagnostics = evaluateAstGrepRules(ctx.filePath, rootNode, ctx.cwd, ctx.kind, {
3807
4607
  blockingOnly: runningAsLspSubstitute ? false : ctx.blockingOnly,
3808
4608
  projectRoot: ctx.projectRoot,
3809
4609
  rulePolicy: ctx.rulePolicy,
4610
+ // #4481: keyed off the per-edit pass, not the substitute floor
4611
+ // above, so a pre-existing blocker on an unchanged line never
4612
+ // STOPs the write in either role.
4613
+ modifiedRanges: ctx.blockingOnly ? ctx.modifiedRanges : void 0,
4614
+ outOfEditScope,
3810
4615
  log: (message) => ctx.log(message),
3811
4616
  content,
3812
4617
  sgModule
@@ -3836,10 +4641,16 @@ var astGrepNapiRunner = {
3836
4641
  } else if (diagnostics.length > 0) {
3837
4642
  semantic = "warning";
3838
4643
  }
3839
- return findingsResult(diagnostics, {
3840
- status: hasBlocking ? "failed" : "succeeded",
3841
- semantic
3842
- });
4644
+ return {
4645
+ ...findingsResult(diagnostics, {
4646
+ status: hasBlocking ? "failed" : "succeeded",
4647
+ semantic
4648
+ }),
4649
+ ...outOfEditScope.length > 0 && { outOfEditScope },
4650
+ ...coveredByServer && {
4651
+ coveredBy: { runnerId: "lsp", serverId: "ast-grep" }
4652
+ }
4653
+ };
3843
4654
  }
3844
4655
  };
3845
4656
  var ast_grep_napi_default = astGrepNapiRunner;
@@ -4380,7 +5191,7 @@ var biomeCheckJsonRunner = {
4380
5191
  var biome_check_default = biomeCheckJsonRunner;
4381
5192
 
4382
5193
  // dist/clients/dispatch/runners/cpp-check.js
4383
- import * as fs5 from "node:fs";
5194
+ import * as fs6 from "node:fs";
4384
5195
  import * as path9 from "node:path";
4385
5196
  var compilerCheckers = {
4386
5197
  clang: createAvailabilityChecker("clang", ".exe"),
@@ -4419,7 +5230,7 @@ var CPP_HEADER_EXTENSIONS = /* @__PURE__ */ new Set([
4419
5230
  ]);
4420
5231
  function headerLooksLikeCpp(absPath) {
4421
5232
  try {
4422
- const content = fs5.readFileSync(absPath, "utf-8");
5233
+ const content = fs6.readFileSync(absPath, "utf-8");
4423
5234
  return /\b(namespace|template|class|constexpr|concept|using)\b|std::|\b(public|private|protected)\s*:/.test(content);
4424
5235
  } catch {
4425
5236
  return false;
@@ -4570,7 +5381,7 @@ var cpp_check_default = cppCheckRunner;
4570
5381
 
4571
5382
  // dist/clients/dispatch/runners/credo.js
4572
5383
  import * as path10 from "node:path";
4573
- import * as fs6 from "node:fs";
5384
+ import * as fs7 from "node:fs";
4574
5385
  var CREDO_PROBE_BUDGET_MS = 1e4;
4575
5386
  var probeCredo = createCwdCachedProbe((cwd) => safeSpawnAsync("mix", ["credo", "--version"], {
4576
5387
  timeout: CREDO_PROBE_BUDGET_MS,
@@ -4596,7 +5407,7 @@ function parseCredoJson(raw, fallbackPath, cwd) {
4596
5407
  }
4597
5408
  }
4598
5409
  function hasMixExs(cwd) {
4599
- return fs6.existsSync(path10.join(cwd, "mix.exs"));
5410
+ return fs7.existsSync(path10.join(cwd, "mix.exs"));
4600
5411
  }
4601
5412
  var credoRunner = {
4602
5413
  id: "credo",
@@ -4633,7 +5444,7 @@ var credoRunner = {
4633
5444
  var credo_default = credoRunner;
4634
5445
 
4635
5446
  // dist/clients/dispatch/runners/cue-vet.js
4636
- import * as fs7 from "node:fs";
5447
+ import * as fs8 from "node:fs";
4637
5448
  import * as path11 from "node:path";
4638
5449
  var cue = createAvailabilityChecker("cue", ".exe", ["version"]);
4639
5450
  function parseCueVetOutput(raw) {
@@ -4738,7 +5549,7 @@ var cueVetRunner = {
4738
5549
  const singleFileArgs = ["vet", "-c=false", `./${fileName}`];
4739
5550
  let touchedContent = null;
4740
5551
  try {
4741
- touchedContent = fs7.readFileSync(absPath, "utf8");
5552
+ touchedContent = fs8.readFileSync(absPath, "utf8");
4742
5553
  } catch {
4743
5554
  touchedContent = null;
4744
5555
  }
@@ -4943,8 +5754,11 @@ var dartAnalyzeRunner = {
4943
5754
  const parsed = parseToolRun("dart-analyze", {
4944
5755
  result,
4945
5756
  output: raw,
4946
- // EXIT TABLE (Dart 3.5 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
4947
- exitCodes: { ran: [1, 2] }
5757
+ // EXIT TABLE (Dart 3.13 measured fixture, tests/fixtures/runner-output/
5758
+ // dart-analyze): 0 clean; 1 infos; 2 warnings; 3 errors; other nonzero
5759
+ // rejected. The 3.5 table stopped at 2, so a file with a compile error
5760
+ // read as a rejected invocation and its findings were dropped.
5761
+ exitCodes: { ran: [1, 2, 3] }
4948
5762
  }, (out) => parseDartMachineOutput(out, ctx.filePath, cwd));
4949
5763
  if (parsed.skipped)
4950
5764
  return parsed.skipped;
@@ -4959,7 +5773,7 @@ var dartAnalyzeRunner = {
4959
5773
  var dart_analyze_default = dartAnalyzeRunner;
4960
5774
 
4961
5775
  // dist/clients/dispatch/runners/detekt.js
4962
- import * as fs8 from "node:fs";
5776
+ import * as fs9 from "node:fs";
4963
5777
  import * as path13 from "node:path";
4964
5778
  var detekt = createAvailabilityChecker("detekt", ".bat");
4965
5779
  var DETEKT_CONFIG_CANDIDATES = [
@@ -5033,7 +5847,7 @@ var DETEKT_FIXABLE_RULES = /* @__PURE__ */ new Set([
5033
5847
  function findDetektConfig(cwd) {
5034
5848
  for (const candidate of DETEKT_CONFIG_CANDIDATES) {
5035
5849
  const full = path13.join(cwd, candidate);
5036
- if (fs8.existsSync(full))
5850
+ if (fs9.existsSync(full))
5037
5851
  return full;
5038
5852
  }
5039
5853
  return void 0;
@@ -5112,11 +5926,11 @@ var detektRunner = {
5112
5926
  var detekt_default = detektRunner;
5113
5927
 
5114
5928
  // dist/clients/dispatch/runners/dotnet-build.js
5115
- import * as fs9 from "node:fs";
5929
+ import * as fs10 from "node:fs";
5116
5930
  import * as path14 from "node:path";
5117
5931
  var dotnet = createAvailabilityChecker("dotnet", ".exe");
5118
5932
  function findProjectTarget(cwd) {
5119
- const entries = fs9.readdirSync(cwd);
5933
+ const entries = fs10.readdirSync(cwd);
5120
5934
  const solution = entries.find((entry) => /\.(sln|slnx)$/i.test(entry));
5121
5935
  if (solution)
5122
5936
  return solution;
@@ -5266,12 +6080,12 @@ ${result.stderr ?? ""}`.trim();
5266
6080
  var dotnet_build_default = dotnetBuildRunner;
5267
6081
 
5268
6082
  // dist/clients/dispatch/runners/elixir-check.js
5269
- import * as fs10 from "node:fs";
6083
+ import * as fs11 from "node:fs";
5270
6084
  import * as path15 from "node:path";
5271
6085
  var mix = createAvailabilityChecker("mix", ".bat");
5272
6086
  var elixirc = createAvailabilityChecker("elixirc", ".bat");
5273
6087
  function hasMixExs2(cwd) {
5274
- return fs10.existsSync(path15.join(cwd, "mix.exs"));
6088
+ return fs11.existsSync(path15.join(cwd, "mix.exs"));
5275
6089
  }
5276
6090
  var ELIXIR_SNIPPET_LOCATION = /└─\s+(.+?):(\d+)(?::(\d+))?(?::|$)/;
5277
6091
  function parseElixirOutput(raw, filePath, cwd = process.cwd()) {
@@ -5371,7 +6185,7 @@ var elixirCheckRunner = {
5371
6185
  args = ["compile", "--warnings-as-errors"];
5372
6186
  } else if (await elixirc.isAvailableAsync(cwd)) {
5373
6187
  const outDir = path15.join(getProjectDataDir(cwd), "elixir-check");
5374
- fs10.mkdirSync(outDir, { recursive: true });
6188
+ fs11.mkdirSync(outDir, { recursive: true });
5375
6189
  command = "elixirc";
5376
6190
  args = ["-o", outDir, absPath];
5377
6191
  }
@@ -5706,9 +6520,10 @@ var parseRuffOutput = createLineParser({
5706
6520
  autoFixAvailable: ruffAutofix?.safePipelineAutofix ?? false,
5707
6521
  fixKind: ruffAutofix?.fixKind === "none" ? void 0 : ruffAutofix?.fixKind
5708
6522
  });
6523
+ var GO_VET_LINE = /^(?:vet:\s+)?(.+?):(\d+):(\d+):\s*(.+)/;
5709
6524
  var parseGoVetOutput = createLineParser({
5710
6525
  tool: "go-vet",
5711
- regex: /^(.+?):(\d+):(\d+):\s*(.+)/,
6526
+ regex: GO_VET_LINE,
5712
6527
  extractMessage: (m) => m[4],
5713
6528
  generateId: (m) => `go-vet-${m[2]}`,
5714
6529
  defectClass: "correctness"
@@ -5750,7 +6565,7 @@ var goVetRunner = {
5750
6565
  }
5751
6566
  const absTarget = resolve14(ctx.filePath);
5752
6567
  const relevant = raw.split("\n").filter((line) => {
5753
- const m = line.match(/^(.+?):(\d+):(\d+):\s*(.+)/);
6568
+ const m = line.match(GO_VET_LINE);
5754
6569
  return m != null && pathsEqual(resolve14(cwd, m[1].trim()), absTarget);
5755
6570
  }).join("\n");
5756
6571
  const diagnostics = parseGoVetOutput(relevant, ctx.filePath, cwd);
@@ -5762,6 +6577,16 @@ var go_vet_default = goVetRunner;
5762
6577
  // dist/clients/dispatch/runners/golangci-lint.js
5763
6578
  import * as path19 from "node:path";
5764
6579
  var golangci = createAvailabilityChecker("golangci-lint", ".exe");
6580
+ var OTHER_OUTPUT_FORMATS = [
6581
+ "text",
6582
+ "tab",
6583
+ "html",
6584
+ "checkstyle",
6585
+ "code-climate",
6586
+ "junit-xml",
6587
+ "teamcity",
6588
+ "sarif"
6589
+ ];
5765
6590
  function describeReplacement(replacement) {
5766
6591
  if (!replacement)
5767
6592
  return void 0;
@@ -5822,12 +6647,18 @@ var golangciRunner = {
5822
6647
  const cmd = await resolveAvailableOrInstall(golangci, "golangci-lint", cwd);
5823
6648
  if (!cmd)
5824
6649
  return { status: "skipped", diagnostics: [], semantic: "none" };
5825
- const result = await safeSpawnAsync(cmd, ["run", "--out-format=json", ctx.filePath], { timeout: 6e4, cwd });
6650
+ const result = await safeSpawnAsync(cmd, [
6651
+ "run",
6652
+ "--output.json.path=stdout",
6653
+ ...OTHER_OUTPUT_FORMATS.map((format) => `--output.${format}.path=`),
6654
+ "--show-stats=false",
6655
+ ctx.filePath
6656
+ ], { timeout: 6e4, cwd });
5826
6657
  const raw = (result.stdout ?? "").length > 0 ? result.stdout ?? "" : result.status !== 0 ? result.stderr || "" : "";
5827
6658
  const parsed = parseToolRun("golangci-lint", {
5828
6659
  result,
5829
6660
  output: raw,
5830
- // EXIT TABLE (golangci-lint 1.60 docs https://golangci-lint.run/docs/welcome/quick-start/): 0 clean; 1 findings; 2 error; 3 error; 4 error; 5 error; other nonzero rejected.
6661
+ // EXIT TABLE (golangci-lint v2 docs https://golangci-lint.run/docs/welcome/quick-start/; 2.14.0 exits 1 on findings and 3 on an unknown flag): 0 clean; 1 findings; 2 error; 3 error; 4 error; 5 error; other nonzero rejected.
5831
6662
  exitCodes: { ran: [1, 2, 3, 4, 5] }
5832
6663
  }, (raw2) => parseGolangciJson(raw2, ctx.filePath, cwd));
5833
6664
  if (parsed.skipped)
@@ -5922,7 +6753,7 @@ var hadolint_default = hadolintRunner;
5922
6753
 
5923
6754
  // dist/clients/dispatch/runners/helm-lint.js
5924
6755
  import * as path21 from "node:path";
5925
- var helm = createAvailabilityChecker("helm", ".exe");
6756
+ var helm = createAvailabilityChecker("helm", ".exe", ["version", "--short"]);
5926
6757
  var inFlightByChartRoot = new PathKeyedMap(normalizeMapKey);
5927
6758
  var HELM_LINT_TIMEOUT_MS = 3e4;
5928
6759
  var MAX_HELM_LINT_OUTPUT_BYTES = 8 * 1024 * 1024;
@@ -5936,14 +6767,17 @@ function isWithin(root, candidate) {
5936
6767
  return relative5 === "" || !relative5.startsWith(`..${path21.sep}`) && relative5 !== ".." && !path21.isAbsolute(relative5);
5937
6768
  }
5938
6769
  function diagnosticLocation(message, chartRoot) {
5939
- const location = message.match(/(?:^|\s)([^:\r\n]+\.(?:ya?ml|tpl|json))(?::(\d+))?(?::\d+)?(?::|\s)/i);
5940
- if (!location) {
6770
+ const location = message.match(/(?:^|[\s(])((?:[^\s:()\r\n]*\/[^:()\r\n"]*?|[^\s:()\r\n]+)\.(?:ya?ml|tpl|json))(?::(\d+))?(?=[:\s)]|$)/i);
6771
+ const [, found, rawLine] = location ?? [];
6772
+ if (!found) {
5941
6773
  return { filePath: path21.join(chartRoot, "Chart.yaml"), message };
5942
6774
  }
5943
- const candidate = path21.resolve(chartRoot, location[1]);
6775
+ const embedded = !message.startsWith(found);
6776
+ const relative5 = embedded ? found.slice(found.indexOf("/") + 1) : found;
6777
+ const candidate = path21.resolve(chartRoot, relative5);
5944
6778
  return {
5945
6779
  filePath: isWithin(chartRoot, candidate) ? candidate : path21.join(chartRoot, "Chart.yaml"),
5946
- line: location[2] ? Number(location[2]) : void 0,
6780
+ line: rawLine ? Number(rawLine) : void 0,
5947
6781
  message
5948
6782
  };
5949
6783
  }
@@ -6058,10 +6892,10 @@ var helmLintRunner = {
6058
6892
  var helm_lint_default = helmLintRunner;
6059
6893
 
6060
6894
  // dist/clients/dispatch/runners/helm-render.js
6061
- import * as fs11 from "node:fs";
6895
+ import * as fs12 from "node:fs";
6062
6896
  import * as os from "node:os";
6063
6897
  import * as path22 from "node:path";
6064
- var helm2 = createAvailabilityChecker("helm", ".exe");
6898
+ var helm2 = createAvailabilityChecker("helm", ".exe", ["version", "--short"]);
6065
6899
  var trivy = createAvailabilityChecker("trivy", ".exe");
6066
6900
  var inFlightByChartRoot2 = new PathKeyedMap(normalizeMapKey);
6067
6901
  var RENDER_TIMEOUT_MS = 3e4;
@@ -6095,10 +6929,10 @@ function resolveTemplateSource(sourceRef, chartRoot, realChartRoot) {
6095
6929
  if (!isWithin2(chartRoot, candidate))
6096
6930
  return null;
6097
6931
  try {
6098
- if (!fs11.lstatSync(candidate).isFile())
6932
+ if (!fs12.lstatSync(candidate).isFile())
6099
6933
  return null;
6100
- const root = realChartRoot ?? fs11.realpathSync(chartRoot);
6101
- if (!isWithin2(root, fs11.realpathSync(candidate)))
6934
+ const root = realChartRoot ?? fs12.realpathSync(chartRoot);
6935
+ if (!isWithin2(root, fs12.realpathSync(candidate)))
6102
6936
  return null;
6103
6937
  } catch {
6104
6938
  return null;
@@ -6107,7 +6941,7 @@ function resolveTemplateSource(sourceRef, chartRoot, realChartRoot) {
6107
6941
  }
6108
6942
  function canonicalChartRoot(chartRoot) {
6109
6943
  try {
6110
- return fs11.realpathSync(chartRoot);
6944
+ return fs12.realpathSync(chartRoot);
6111
6945
  } catch {
6112
6946
  return chartRoot;
6113
6947
  }
@@ -6342,7 +7176,7 @@ function collectRenderedFiles(root) {
6342
7176
  const dir = stack.pop();
6343
7177
  let entries;
6344
7178
  try {
6345
- entries = fs11.readdirSync(dir, { withFileTypes: true });
7179
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
6346
7180
  } catch {
6347
7181
  continue;
6348
7182
  }
@@ -6381,7 +7215,7 @@ function readRenderedTree(outputDir, chartRoot) {
6381
7215
  for (const renderedPath of walk2.files) {
6382
7216
  let content;
6383
7217
  try {
6384
- content = fs11.readFileSync(renderedPath, "utf-8");
7218
+ content = fs12.readFileSync(renderedPath, "utf-8");
6385
7219
  } catch {
6386
7220
  unreadable += 1;
6387
7221
  continue;
@@ -6500,7 +7334,7 @@ function logRenderPass(filePath, chartRoot, startedAt, metadata) {
6500
7334
  metadata: { chartRoot, ...metadata }
6501
7335
  });
6502
7336
  }
6503
- function discardScratchDir(outputDir, filePath, remove = (target) => fs11.rmSync(target, { recursive: true, force: true })) {
7337
+ function discardScratchDir(outputDir, filePath, remove = (target) => fs12.rmSync(target, { recursive: true, force: true })) {
6504
7338
  try {
6505
7339
  remove(outputDir);
6506
7340
  } catch (error) {
@@ -6521,7 +7355,7 @@ async function renderAndValidate(chartRoot, cwd, filePath) {
6521
7355
  const helmCmd = await resolveAvailableOrInstall(helm2, "helm", cwd);
6522
7356
  if (!helmCmd)
6523
7357
  return helmUnavailableResult(cwd);
6524
- const outputDir = fs11.mkdtempSync(path22.join(os.tmpdir(), "pi-lens-helm-render-"));
7358
+ const outputDir = fs12.mkdtempSync(path22.join(os.tmpdir(), "pi-lens-helm-render-"));
6525
7359
  try {
6526
7360
  const render = await safeSpawnAsync(helmCmd, ["template", "pi-lens-render", chartRoot, "--output-dir", outputDir], {
6527
7361
  cwd,
@@ -6842,9 +7676,30 @@ function normalizeKtlintResults(parsed) {
6842
7676
  }
6843
7677
  return null;
6844
7678
  }
7679
+ function parseResults(text) {
7680
+ try {
7681
+ return normalizeKtlintResults(JSON.parse(text));
7682
+ } catch {
7683
+ return null;
7684
+ }
7685
+ }
7686
+ function parseReporterDocument(raw) {
7687
+ const lines = raw.split(/\r?\n/);
7688
+ const start = lines.findIndex((line) => /^\s*[[{]/.test(line));
7689
+ if (start < 0)
7690
+ return parseResults(raw);
7691
+ for (let end = lines.length - 1; end >= start; end--) {
7692
+ if (!/[\]}]\s*$/.test(lines[end] ?? ""))
7693
+ continue;
7694
+ const results = parseResults(lines.slice(start, end + 1).join("\n"));
7695
+ if (results)
7696
+ return results;
7697
+ }
7698
+ return null;
7699
+ }
6845
7700
  function parseKtlintOutput(raw, filePath, cwd) {
6846
7701
  try {
6847
- const parsed = normalizeKtlintResults(JSON.parse(raw));
7702
+ const parsed = parseReporterDocument(raw);
6848
7703
  if (!parsed)
6849
7704
  return null;
6850
7705
  const autofix = getAutofixCapability("ktlint");
@@ -6855,15 +7710,15 @@ function parseKtlintOutput(raw, filePath, cwd) {
6855
7710
  continue;
6856
7711
  for (const err of result.errors ?? []) {
6857
7712
  diagnostics.push({
6858
- id: `ktlint-${err.ruleId}-${err.line}-${err.col}`,
6859
- message: `[${err.ruleId}] ${err.detail}`,
7713
+ id: `ktlint-${err.rule}-${err.line}-${err.column}`,
7714
+ message: `[${err.rule}] ${err.message}`,
6860
7715
  filePath,
6861
7716
  line: err.line,
6862
- column: err.col,
7717
+ column: err.column,
6863
7718
  severity: "warning",
6864
7719
  semantic: "warning",
6865
7720
  tool: "ktlint",
6866
- rule: err.ruleId,
7721
+ rule: err.rule,
6867
7722
  fixable: true,
6868
7723
  autoFixAvailable: autofix?.safePipelineAutofix ?? false,
6869
7724
  fixKind: autofix?.fixKind === "none" ? void 0 : autofix?.fixKind
@@ -7037,16 +7892,6 @@ async function demoteInferredProjectSweepResults(results, cwd, service, signal)
7037
7892
  return out;
7038
7893
  }
7039
7894
 
7040
- // dist/clients/dispatch/runners/utils.js
7041
- import * as fs12 from "node:fs";
7042
- function readFileContent(filePath) {
7043
- try {
7044
- return fs12.readFileSync(filePath, "utf-8");
7045
- } catch {
7046
- return void 0;
7047
- }
7048
- }
7049
-
7050
7895
  // dist/clients/dispatch/runners/lsp.js
7051
7896
  var LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
7052
7897
  var LSP_DIAGNOSTICS_WAIT_MS = 2500;
@@ -7083,13 +7928,23 @@ var lspRunner = {
7083
7928
  }
7084
7929
  const lspService = getLSPService();
7085
7930
  if (!lspService.supportsLSP(ctx.filePath)) {
7086
- return { status: "skipped", diagnostics: [], semantic: "none" };
7931
+ const { disabledServerIds: disabledPrimaryServerIds } = disabledPrimaries(ctx.filePath);
7932
+ return {
7933
+ status: "skipped",
7934
+ diagnostics: [],
7935
+ semantic: "none",
7936
+ ...disabledPrimaryServerIds.length > 0 && {
7937
+ primaryUnavailableCause: "disabled",
7938
+ disabledPrimaryServerIds
7939
+ }
7940
+ };
7087
7941
  }
7088
7942
  let lspDiags = [];
7089
7943
  let serverFailed = false;
7090
7944
  let lspClientReady = true;
7091
7945
  let diagnosticsInconclusive = false;
7092
7946
  let inconclusiveServerIds = [];
7947
+ let primaryUnavailable;
7093
7948
  let unconfirmedServerIds = [];
7094
7949
  let deferredServerIds = [];
7095
7950
  let usedWarmAttach = false;
@@ -7127,6 +7982,15 @@ var lspRunner = {
7127
7982
  lspDiags = touched.diags;
7128
7983
  diagnosticsInconclusive = touched.inconclusive === true;
7129
7984
  inconclusiveServerIds = touched.inconclusiveServerIds ?? [];
7985
+ if (touched.inconclusiveReason === "primary-unavailable") {
7986
+ const disabledPrimaryServerIds = touched.disabledPrimaryServerIds ?? [];
7987
+ primaryUnavailable = {
7988
+ primaryUnavailableCause: touched.primaryUnavailableCause ?? primaryUnavailableCause(inconclusiveServerIds, disabledPrimaryServerIds),
7989
+ ...disabledPrimaryServerIds.length > 0 && {
7990
+ disabledPrimaryServerIds
7991
+ }
7992
+ };
7993
+ }
7130
7994
  unconfirmedServerIds = [
7131
7995
  .../* @__PURE__ */ new Set([
7132
7996
  ...touchCoverageGap(touched),
@@ -7178,10 +8042,20 @@ var lspRunner = {
7178
8042
  };
7179
8043
  }
7180
8044
  if (!lspClientReady) {
7181
- return { status: "skipped", diagnostics: [], semantic: "none" };
8045
+ return {
8046
+ status: "skipped",
8047
+ diagnostics: [],
8048
+ semantic: "none",
8049
+ primaryUnavailableCause: "unstarted"
8050
+ };
7182
8051
  }
7183
8052
  if (diagnosticsInconclusive && lspDiags.length === 0) {
7184
- return { status: "skipped", diagnostics: [], semantic: "none" };
8053
+ return {
8054
+ status: "skipped",
8055
+ diagnostics: [],
8056
+ semantic: "none",
8057
+ ...primaryUnavailable
8058
+ };
7185
8059
  }
7186
8060
  if (lspDiags.length === 0) {
7187
8061
  if (deferredServerIds.length > 0) {
@@ -7246,7 +8120,15 @@ var lspRunner = {
7246
8120
  }));
7247
8121
  }
7248
8122
  const diagnostics = convertLspDiagnostics(validLspDiags, diagnosticPath, { fixSuggestionByIndex });
7249
- const keptDiagnostics = retagAuxiliaryDiagnostics(diagnostics, validLspDiags, content, { cwd: ctx.cwd, fileRole: ctx.fileRole });
8123
+ const rawRangeByDiagnostic = new Map(diagnostics.map((d, i) => [d, validLspDiags[i]?.range]));
8124
+ const retaggedDiagnostics = retagAuxiliaryDiagnostics(diagnostics, validLspDiags, content, { cwd: ctx.cwd, fileRole: ctx.fileRole });
8125
+ const keptDiagnostics = [];
8126
+ const outOfEditScope = [];
8127
+ for (const d of retaggedDiagnostics) {
8128
+ const range = ctx.blockingOnly && d.tool === "ast-grep" && d.semantic === "blocking" ? rawRangeByDiagnostic.get(d) : void 0;
8129
+ const inScope = range === void 0 || spanOverlapsModifiedRanges(range.start.line + 1, (range.end?.line ?? range.start.line) + 1, ctx.modifiedRanges);
8130
+ (inScope ? keptDiagnostics : outOfEditScope).push(d);
8131
+ }
7250
8132
  const hasErrors = keptDiagnostics.some((d) => d.semantic === "blocking");
7251
8133
  const resultSemantic = hasErrors ? "blocking" : keptDiagnostics.length > 0 ? "warning" : "none";
7252
8134
  return {
@@ -7261,7 +8143,8 @@ var lspRunner = {
7261
8143
  },
7262
8144
  ...deferredServerIds.length > 0 && {
7263
8145
  deferredServerIds: [...deferredServerIds]
7264
- }
8146
+ },
8147
+ ...outOfEditScope.length > 0 && { outOfEditScope }
7265
8148
  };
7266
8149
  }
7267
8150
  };
@@ -7791,13 +8674,9 @@ var phpLintRunner = {
7791
8674
  skipTestFiles: false,
7792
8675
  async run(ctx) {
7793
8676
  const cwd = resolveRunnerCwd(ctx, "php-lint");
7794
- if (!await php.isAvailableAsync(cwd)) {
7795
- return { status: "skipped", diagnostics: [], semantic: "none" };
7796
- }
7797
- const cmd = php.getCommand(cwd);
7798
- if (!cmd) {
8677
+ const cmd = ctx.analysisRootMode === "adopted" ? resolveAdoptedRootCommand("php", ctx, cwd) : await php.isAvailableAsync(cwd) ? php.getCommand(cwd) : null;
8678
+ if (!cmd)
7799
8679
  return { status: "skipped", diagnostics: [], semantic: "none" };
7800
- }
7801
8680
  const absPath = path30.resolve(cwd, ctx.filePath);
7802
8681
  const result = await safeSpawnAsync(cmd, ["-l", absPath], {
7803
8682
  timeout: 15e3,
@@ -8301,7 +9180,7 @@ var pyrightRunner = {
8301
9180
  cmd = installedPath;
8302
9181
  }
8303
9182
  if (!cmd) {
8304
- const { findCommandAsync } = await import("./chunk-67WMNWEE.js");
9183
+ const { findCommandAsync } = await import("./chunk-LLJNQKQW.js");
8305
9184
  const foundCmd = await findCommandAsync("pyright");
8306
9185
  if (foundCmd)
8307
9186
  cmd = foundCmd;
@@ -8904,6 +9783,7 @@ var spotbugs_default = spotbugsRunner;
8904
9783
  import * as fs15 from "node:fs";
8905
9784
  import * as path36 from "node:path";
8906
9785
  var shellcheck = createAvailabilityChecker("shellcheck", ".exe");
9786
+ var SHELLCHECK_EXIT_CODES = { ran: [1] };
8907
9787
  var SHELLCHECK_DIALECTS = [
8908
9788
  "sh",
8909
9789
  "bash",
@@ -9027,7 +9907,10 @@ var shellcheckRunner = {
9027
9907
  skipReason: "covered-by-primary",
9028
9908
  // WHO claimed (#3968 F2): declared (the row's own covers, gated
9029
9909
  // on the custom row's own command) or builtin-fact.
9030
- claimSource: cover.claimSource
9910
+ claimSource: cover.claimSource,
9911
+ // #4484 round 8: the skip is a verdict only when that server's
9912
+ // lane answers; the dispatcher voids it otherwise.
9913
+ coveredBy: { runnerId: "lsp", serverId: cover.serverId }
9031
9914
  };
9032
9915
  }
9033
9916
  const dialectInfo = resolveShellFileDialect(ctx.filePath);
@@ -9062,20 +9945,52 @@ var shellcheckRunner = {
9062
9945
  if (result.status === 0 && !result.stdout?.trim()) {
9063
9946
  return { status: "succeeded", diagnostics: [], semantic: "none" };
9064
9947
  }
9065
- const raw = result.stdout + result.stderr;
9066
- const diagnostics = parseShellcheckOutput(raw, ctx.filePath, cwd);
9948
+ const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
9949
+ const parsed = parseToolRun("shellcheck", { result, output: raw, exitCodes: SHELLCHECK_EXIT_CODES }, (output) => parseShellcheckOutput(output, ctx.filePath, cwd));
9950
+ if (parsed.skipped)
9951
+ return parsed.skipped;
9067
9952
  return finishParsedRun({
9068
9953
  tool: "shellcheck",
9069
9954
  ctx,
9070
9955
  result,
9071
- diagnostics
9956
+ diagnostics: parsed.diagnostics
9072
9957
  });
9073
9958
  }
9074
9959
  };
9075
9960
  var shellcheck_default = shellcheckRunner;
9076
9961
 
9077
9962
  // dist/clients/dispatch/runners/fish-indent.js
9963
+ import { resolve as resolve32 } from "node:path";
9078
9964
  var fishIndent = createAvailabilityChecker("fish_indent");
9965
+ var fish = createAvailabilityChecker("fish");
9966
+ var SYNTAX_STEP = "fish-syntax-check";
9967
+ function parseSyntaxErrors(output, filePath, cwd) {
9968
+ const diagnostics = [];
9969
+ for (const line of output.split("\n")) {
9970
+ const [, reported, lineText, message] = line.match(/^(.+) \(line (\d+)\): (.+)$/) ?? [];
9971
+ if (!reported || !lineText || !message)
9972
+ continue;
9973
+ const wanted = resolve32(cwd, filePath);
9974
+ if (!pathsEqual(resolve32(cwd, reported), wanted) && !(reported.startsWith("~/") && wanted.endsWith(reported.slice(1))))
9975
+ continue;
9976
+ const lineNumber = Number(lineText);
9977
+ diagnostics.push({
9978
+ id: `fish-syntax-${filePath}:${lineNumber}`,
9979
+ message: `fish: ${message.trim()}`,
9980
+ filePath,
9981
+ line: lineNumber,
9982
+ column: 1,
9983
+ severity: "error",
9984
+ semantic: "blocking",
9985
+ tool: "fish-indent",
9986
+ rule: "fish-syntax-error"
9987
+ });
9988
+ break;
9989
+ }
9990
+ return diagnostics;
9991
+ }
9992
+ var FISH_SYNTAX_EXIT_CODES = { ran: [127] };
9993
+ var BLOCKING_VERDICT = { status: "failed", semantic: "blocking" };
9079
9994
  var fishIndentRunner = {
9080
9995
  id: "fish-indent",
9081
9996
  appliesTo: ["fish"],
@@ -9083,12 +9998,27 @@ var fishIndentRunner = {
9083
9998
  skipTestFiles: false,
9084
9999
  async run(ctx) {
9085
10000
  const cwd = resolveRunnerCwd(ctx, "fish-indent");
9086
- const available = await fishIndent.isAvailableAsync(cwd);
9087
- if (!available)
9088
- return { status: "skipped", diagnostics: [], semantic: "none" };
9089
- const cmd = fishIndent.getCommand(cwd);
10001
+ const cmd = ctx.analysisRootMode === "adopted" ? resolveAdoptedRootCommand("fish_indent", ctx, cwd) : await fishIndent.isAvailableAsync(cwd) ? fishIndent.getCommand(cwd) : null;
9090
10002
  if (!cmd)
9091
10003
  return { status: "skipped", diagnostics: [], semantic: "none" };
10004
+ const fishCmd = ctx.analysisRootMode === "adopted" ? resolveAdoptedRootCommand("fish", ctx, cwd) : await fish.isAvailableAsync(cwd) ? fish.getCommand(cwd) : null;
10005
+ if (fishCmd) {
10006
+ const syntax = await safeSpawnAsync(fishCmd, ["--no-config", "--no-execute", ctx.filePath], { timeout: 1e4, cwd });
10007
+ const parsed = parseToolRun(SYNTAX_STEP, {
10008
+ result: syntax,
10009
+ output: syntax.stderr ?? "",
10010
+ exitCodes: FISH_SYNTAX_EXIT_CODES
10011
+ }, (output) => parseSyntaxErrors(output, ctx.filePath, cwd));
10012
+ if (parsed.diagnostics.length > 0) {
10013
+ return findingsResult(parsed.diagnostics, BLOCKING_VERDICT);
10014
+ }
10015
+ } else {
10016
+ recordDegradationOnce({
10017
+ kind: "fish-syntax-check-unavailable",
10018
+ subject: "fish",
10019
+ reason: "fish not found next to fish_indent: syntax errors are not checked"
10020
+ });
10021
+ }
9092
10022
  const result = await safeSpawnAsync(cmd, ["--check", ctx.filePath], {
9093
10023
  timeout: 1e4,
9094
10024
  cwd
@@ -9096,7 +10026,12 @@ var fishIndentRunner = {
9096
10026
  if (result.status === 0) {
9097
10027
  return { status: "succeeded", diagnostics: [], semantic: "none" };
9098
10028
  }
9099
- const stderrLine = (result.stderr ?? "").split("\n").find((l) => l.trim());
10029
+ if (result.signal || result.status === null) {
10030
+ const didNotRun = skipUnlessToolRan("fish_indent", { result });
10031
+ if (didNotRun)
10032
+ return didNotRun;
10033
+ }
10034
+ const stderrLine = (result.stderr ?? "").split("\n").find((l) => l.trim() && !pathsEqual(resolve32(cwd, l.trim()), resolve32(cwd, ctx.filePath)));
9100
10035
  if (stderrLine) {
9101
10036
  const lineMatch = stderrLine.match(/\(line\s+(\d+)\)/);
9102
10037
  const line = lineMatch ? Number(lineMatch[1]) : 1;
@@ -9113,10 +10048,7 @@ var fishIndentRunner = {
9113
10048
  rule: "fish-indent-parse-error"
9114
10049
  }
9115
10050
  ];
9116
- return findingsResult(diagnostics2, {
9117
- status: "failed",
9118
- semantic: "blocking"
9119
- });
10051
+ return findingsResult(diagnostics2, BLOCKING_VERDICT);
9120
10052
  }
9121
10053
  const diagnostics = [
9122
10054
  {
@@ -9167,6 +10099,12 @@ var shfmtRunner = {
9167
10099
  timeout: 1e4,
9168
10100
  cwd
9169
10101
  });
10102
+ const didNotRun = skipUnlessToolRan("shfmt", {
10103
+ result,
10104
+ output: `${result.stdout ?? ""}${result.stderr ?? ""}`
10105
+ });
10106
+ if (didNotRun)
10107
+ return didNotRun;
9170
10108
  if (result.status === 0) {
9171
10109
  return { status: "succeeded", diagnostics: [], semantic: "none" };
9172
10110
  }
@@ -9748,7 +10686,10 @@ var taploRunner = {
9748
10686
  // an unexplained empty output on the delivery surfaces
9749
10687
  // (defect shape 10). F2 adds the claim's provenance beside it.
9750
10688
  skipReason: "covered-by-primary",
9751
- claimSource: cover.claimSource
10689
+ claimSource: cover.claimSource,
10690
+ // #4484 round 8: the skip is a verdict only when that server's
10691
+ // lane answers; the dispatcher voids it otherwise.
10692
+ coveredBy: { runnerId: "lsp", serverId: cover.serverId }
9752
10693
  };
9753
10694
  }
9754
10695
  let cmd = localBinPath(findLocalBinUpwards("taplo", cwd)) ?? null;
@@ -10596,11 +11537,6 @@ function defaultFixSuggestion2(defectClass, ruleId) {
10596
11537
  }
10597
11538
  return "Refactor this pattern to a safer, explicit form matching project conventions.";
10598
11539
  }
10599
- function isLineInModifiedRanges(line, ranges) {
10600
- if (!ranges || ranges.length === 0)
10601
- return true;
10602
- return ranges.some((r) => line >= r.start && line <= r.end);
10603
- }
10604
11540
  function getTotalLinesChanged(ranges) {
10605
11541
  if (!ranges || ranges.length === 0)
10606
11542
  return 0;
@@ -10725,19 +11661,20 @@ var treeSitterRunner = {
10725
11661
  const contentFromFacts = ctx.facts.getFileFact(filePath, "file.content");
10726
11662
  const contentOverride = contentFromFacts !== void 0 && contentFromFacts !== null ? contentFromFacts : void 0;
10727
11663
  const diagnostics = [];
11664
+ const withheld = [];
11665
+ let queryRunFailed = false;
10728
11666
  try {
10729
11667
  const found = await client.runQueriesOnFile(effectiveQueries, filePath, languageId, { maxResults: 10 }, contentOverride);
10730
11668
  for (const { queryDef: query, match } of found) {
10731
11669
  const { line, column } = match;
10732
11670
  const isSeverityBlocking = query.severity === "error" || query.inline_tier === "blocking" || SILENT_ERROR_QUERY_IDS.has(query.id);
10733
- if (ctx.blockingOnly && isSeverityBlocking && !isLineInModifiedRanges(line, ctx.modifiedRanges)) {
10734
- continue;
10735
- }
11671
+ const endLine = line + (match.matchedText.match(/\n/g)?.length ?? 0);
11672
+ const outOfScope = ctx.blockingOnly && isSeverityBlocking && !spanOverlapsModifiedRanges(line, endLine, ctx.modifiedRanges);
10736
11673
  const semantic = query.severity === "error" ? "blocking" : query.severity === "warning" ? "warning" : "none";
10737
11674
  const defectClass = query.defect_class ?? classifyDefect(query.id, "tree-sitter", query.message);
10738
11675
  const suggestion = query.has_fix && query.fix_action ? `${query.fix_action} this statement` : semantic === "blocking" ? defaultFixSuggestion2(defectClass, query.id) : void 0;
10739
11676
  const hasSuggestedFix = !!query.has_fix;
10740
- diagnostics.push({
11677
+ (outOfScope ? withheld : diagnostics).push({
10741
11678
  id: `tree-sitter:${query.id}:${line}`,
10742
11679
  message: query.message.replace(/\{\{(\w+)\}\}/g, (_, name) => match.captures[name]?.trim() ?? `{{${name}}}`),
10743
11680
  filePath,
@@ -10759,6 +11696,7 @@ var treeSitterRunner = {
10759
11696
  });
10760
11697
  }
10761
11698
  } catch (err) {
11699
+ queryRunFailed = true;
10762
11700
  const msg = err instanceof Error ? err.message : String(err);
10763
11701
  if (classifyTreeSitterWasmError(err) === "abort") {
10764
11702
  markTreeSitterWasmAborted();
@@ -10818,6 +11756,10 @@ var treeSitterRunner = {
10818
11756
  } catch {
10819
11757
  }
10820
11758
  }
11759
+ const observationExtras = {
11760
+ ...withheld.length > 0 && { outOfEditScope: withheld },
11761
+ ...queryRunFailed && { inconclusive: true }
11762
+ };
10821
11763
  if (diagnostics.length === 0) {
10822
11764
  if (!isTreeSitterWasmAborted() && !client.getLanguage(languageId)) {
10823
11765
  logTreeSitter({
@@ -10827,7 +11769,11 @@ var treeSitterRunner = {
10827
11769
  reason: "grammar_unavailable",
10828
11770
  status: "skipped"
10829
11771
  });
10830
- return { status: "skipped", diagnostics: [], semantic: "none" };
11772
+ return {
11773
+ status: "skipped",
11774
+ diagnostics: [],
11775
+ semantic: "none"
11776
+ };
10831
11777
  }
10832
11778
  logTreeSitter({
10833
11779
  phase: "runner_complete",
@@ -10839,7 +11785,12 @@ var treeSitterRunner = {
10839
11785
  queryCount: languageQueries.length,
10840
11786
  effectiveQueryCount: effectiveQueries.length
10841
11787
  });
10842
- return { status: "succeeded", diagnostics: [], semantic: "none" };
11788
+ return {
11789
+ status: "succeeded",
11790
+ diagnostics: [],
11791
+ semantic: "none",
11792
+ ...observationExtras
11793
+ };
10843
11794
  }
10844
11795
  const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
10845
11796
  const blockingCount = diagnostics.filter((d) => d.semantic === "blocking").length;
@@ -10853,10 +11804,13 @@ var treeSitterRunner = {
10853
11804
  queryCount: languageQueries.length,
10854
11805
  effectiveQueryCount: effectiveQueries.length
10855
11806
  });
10856
- return findingsResult(diagnostics, {
10857
- status: hasBlocking ? "failed" : "succeeded",
10858
- semantic: hasBlocking ? "blocking" : "warning"
10859
- });
11807
+ return {
11808
+ ...findingsResult(diagnostics, {
11809
+ status: hasBlocking ? "failed" : "succeeded",
11810
+ semantic: hasBlocking ? "blocking" : "warning"
11811
+ }),
11812
+ ...observationExtras
11813
+ };
10860
11814
  }
10861
11815
  };
10862
11816
  var tree_sitter_default = treeSitterRunner;
@@ -11267,8 +12221,6 @@ function isTestSuiteOrganizer(calls) {
11267
12221
  // dist/clients/dispatch/rules/high-complexity.js
11268
12222
  var DEFAULT_HIGH_COMPLEXITY_THRESHOLD = 15;
11269
12223
  var DEFAULT_HIGH_COMPLEXITY_DEPTH_THRESHOLD = 6;
11270
- var ccThreshold = DEFAULT_HIGH_COMPLEXITY_THRESHOLD;
11271
- var depthThreshold = DEFAULT_HIGH_COMPLEXITY_DEPTH_THRESHOLD;
11272
12224
  var highComplexityRule = {
11273
12225
  id: "high-complexity",
11274
12226
  requires: ["file.functionSummaries"],
@@ -11279,12 +12231,12 @@ var highComplexityRule = {
11279
12231
  const fns = store.getFileFact(ctx.filePath, "file.functionSummaries") ?? [];
11280
12232
  const diagnostics = [];
11281
12233
  const configuredCcThreshold = ctx.projectConfig?.rules["high-complexity"]?.threshold;
11282
- const activeCcThreshold = configuredCcThreshold ?? ccThreshold;
12234
+ const activeCcThreshold = configuredCcThreshold ?? DEFAULT_HIGH_COMPLEXITY_THRESHOLD;
11283
12235
  for (const f of fns) {
11284
12236
  if (isTestSuiteOrganizer(f.outgoingCalls))
11285
12237
  continue;
11286
12238
  const ccBreached = f.cyclomaticComplexity >= activeCcThreshold;
11287
- const depthBreached = f.maxNestingDepth >= depthThreshold;
12239
+ const depthBreached = f.maxNestingDepth >= DEFAULT_HIGH_COMPLEXITY_DEPTH_THRESHOLD;
11288
12240
  if (!ccBreached && !depthBreached)
11289
12241
  continue;
11290
12242
  const parts = [];
@@ -11310,7 +12262,6 @@ var highComplexityRule = {
11310
12262
 
11311
12263
  // dist/clients/dispatch/rules/high-fan-out.js
11312
12264
  var DEFAULT_HIGH_FAN_OUT_THRESHOLD = 20;
11313
- var fanOutThreshold = DEFAULT_HIGH_FAN_OUT_THRESHOLD;
11314
12265
  var highFanOutRule = {
11315
12266
  id: "high-fan-out",
11316
12267
  requires: ["file.functionSummaries"],
@@ -11321,7 +12272,7 @@ var highFanOutRule = {
11321
12272
  const fns = store.getFileFact(ctx.filePath, "file.functionSummaries") ?? [];
11322
12273
  const diagnostics = [];
11323
12274
  const configuredFanOutThreshold = ctx.projectConfig?.rules["high-fan-out"]?.threshold;
11324
- const activeFanOutThreshold = configuredFanOutThreshold ?? fanOutThreshold;
12275
+ const activeFanOutThreshold = configuredFanOutThreshold ?? DEFAULT_HIGH_FAN_OUT_THRESHOLD;
11325
12276
  for (const f of fns) {
11326
12277
  if (isTestSuiteOrganizer(f.outgoingCalls))
11327
12278
  continue;
@@ -11680,6 +12631,8 @@ function applyProjectLensConfig(cwd) {
11680
12631
  return loadPiLensProjectConfig(cwd);
11681
12632
  }
11682
12633
  var sessionFacts = new FactStore("dispatch");
12634
+ var preEditCaptureFacts = new FactStore("pre-edit-capture");
12635
+ var preEditCapturesInFlight = /* @__PURE__ */ new Map();
11683
12636
  setFactStoreEvictionReporter((subject, axis, reason) => {
11684
12637
  if (axis === "pinned-over-budget") {
11685
12638
  recordDegradationOnce({
@@ -11861,12 +12814,16 @@ function resetDispatchBaselines(cwd) {
11861
12814
  resetAstGrepNapiLoadState();
11862
12815
  resetTreeSitterClientLoadState();
11863
12816
  sessionFacts.clearAll();
12817
+ for (const controller of preEditCapturesInFlight.values())
12818
+ controller.abort();
12819
+ preEditCaptureFacts.clearAll();
11864
12820
  resetSessionSlopScore();
11865
12821
  clearCoverageNoticeState();
11866
12822
  clearReviewGraphWorkspaceCache();
11867
12823
  clearReverseDepsIndexCache();
11868
12824
  clearModuleGraphCache();
11869
- recentlyCleanNeighborCache.clear();
12825
+ resetCascadeUnansweredPrimariesDelivered();
12826
+ resetCascadePendingNeighbors();
11870
12827
  primaryFilesThisTurn.clear();
11871
12828
  cascadeSessionStats = {
11872
12829
  runs: 0,
@@ -11882,7 +12839,6 @@ var cascadeSessionStats = {
11882
12839
  diagnosticsSurfaced: 0,
11883
12840
  coldSnapshotTouches: 0
11884
12841
  };
11885
- var recentlyCleanNeighborCache = /* @__PURE__ */ new Map();
11886
12842
  var primaryFilesThisTurn = /* @__PURE__ */ new Set();
11887
12843
  var MAX_PER_FILE = RUNTIME_CONFIG.pipeline.cascadeMaxDiagnosticsPerFile;
11888
12844
  var MAX_FILES = RUNTIME_CONFIG.pipeline.cascadeMaxFiles;
@@ -11910,8 +12866,16 @@ var CASCADE_GRAPH_KINDS = /* @__PURE__ */ new Set([
11910
12866
  "ruby",
11911
12867
  "cxx"
11912
12868
  ]);
12869
+ var IMPLICIT_SCOPE_NEIGHBOR_CAP = MAX_FILES * 2;
11913
12870
  async function dispatchLintWithResult(filePath, cwd, pi, modifiedRanges, logContext, options) {
11914
- const ctx = createDispatchContext(filePath, cwd, pi, sessionFacts, options?.blockingOnly ?? true, modifiedRanges, options?.projectRoot, options?.writeIndex, options?.telemetryModel, options?.telemetryProvider, options?.sessionGeneration, options?.sessionId, options?.analysisRootMode);
12871
+ const created = createDispatchContext(filePath, cwd, pi, sessionFacts, options?.blockingOnly ?? true, modifiedRanges, options?.projectRoot, options?.writeIndex, options?.telemetryModel, options?.telemetryProvider, options?.sessionGeneration, options?.sessionId, options?.analysisRootMode);
12872
+ const ctx = options?.owedRanges?.size || options?.recordedRunnerIds?.size ? {
12873
+ ...created,
12874
+ ...options.owedRanges?.size && { owedRanges: options.owedRanges },
12875
+ ...options.recordedRunnerIds?.size && {
12876
+ recordedRunnerIds: options.recordedRunnerIds
12877
+ }
12878
+ } : created;
11915
12879
  sessionFacts.clearFileFactsFor(ctx.filePath);
11916
12880
  try {
11917
12881
  const kind = ctx.kind;
@@ -12042,14 +13006,17 @@ function enforceWarmWordIndexLruCap() {
12042
13006
  return;
12043
13007
  }
12044
13008
  }
13009
+ function usableWarmWordIndex(index) {
13010
+ return index?.forward ? index : void 0;
13011
+ }
12045
13012
  function acquireWarmWordIndex(cwd) {
12046
13013
  const key2 = path50.resolve(cwd);
12047
13014
  let entry = warmWordIndexes.get(key2);
12048
13015
  if (!entry) {
12049
13016
  const snapshot = loadProjectSnapshot(key2);
12050
- const index = deserializeWordIndex(snapshot?.wordIndex) ?? void 0;
13017
+ const index = usableWarmWordIndex(deserializeWordIndex(snapshot?.wordIndex) ?? void 0);
12051
13018
  entry = {
12052
- index: index?.forward ? index : void 0,
13019
+ index,
12053
13020
  timer: void 0,
12054
13021
  leases: 0,
12055
13022
  lastUsedAt: Date.now(),
@@ -12077,6 +13044,26 @@ function acquireWarmWordIndex(cwd) {
12077
13044
  }
12078
13045
  };
12079
13046
  }
13047
+ function adoptPublishedWarmWordIndex(cwd, published, trigger) {
13048
+ const index = usableWarmWordIndex(published);
13049
+ if (!index)
13050
+ return;
13051
+ const key2 = path50.resolve(cwd);
13052
+ const entry = warmWordIndexes.get(key2);
13053
+ if (!entry || entry.index)
13054
+ return;
13055
+ entry.index = index;
13056
+ entry.lastUsedAt = Date.now();
13057
+ scheduleWarmWordIndexIdleEviction(key2, entry);
13058
+ logWordIndex({
13059
+ phase: "warm_cache_adopted",
13060
+ cwd: key2,
13061
+ trigger,
13062
+ indexedFileCount: index.docCount,
13063
+ tokens: index.postings.size
13064
+ });
13065
+ }
13066
+ setWordIndexPublishedListener(adoptPublishedWarmWordIndex);
12080
13067
  function _resetWarmWordIndexCacheForTests() {
12081
13068
  for (const entry of warmWordIndexes.values())
12082
13069
  clearWarmWordIndexTimer(entry);
@@ -12139,7 +13126,13 @@ async function warmLspForFile(absPath, host) {
12139
13126
  clientScope: "primary",
12140
13127
  maxClientWaitMs: WARMUP_CLIENT_WAIT_MS,
12141
13128
  maxDiagnosticsWaitMs: WARMUP_DIAGNOSTICS_WAIT_MS,
12142
- source: "mcp-warmup"
13129
+ source: "mcp-warmup",
13130
+ // #3405: every caller of `analyzeFile` (the MCP hosts' post-write hooks,
13131
+ // warm and cold, and `pilens_analyze`) asks about the bytes on disk now,
13132
+ // and this is the analysis's first push of them, so it is the save. A
13133
+ // save-triggered server (Expert compiles on didSave and on nothing else)
13134
+ // answers an MCP host's edit only because of it.
13135
+ saved: true
12143
13136
  });
12144
13137
  } catch {
12145
13138
  }
@@ -12154,6 +13147,7 @@ async function analyzeFile(filePath, cwd, options = {}) {
12154
13147
  const start = Date.now();
12155
13148
  const result = await dispatchLintWithResult(absPath, cwd, host, void 0, void 0, {
12156
13149
  blockingOnly: options.blockingOnly ?? false,
13150
+ analysisRootMode: resolveAnalysisRoot(absPath, cwd),
12157
13151
  // #3791: this is a pull surface. Each call is an independent question,
12158
13152
  // so the synthetic coverage notice must come back on every call rather
12159
13153
  // than being latched once per session by the dispatcher — otherwise a
@@ -12259,6 +13253,18 @@ export {
12259
13253
  CacheManager,
12260
13254
  getDiagnosticTracker,
12261
13255
  normalizeRuleId,
13256
+ readFileContent,
13257
+ applyRulePolicy,
13258
+ convertLspDiagnostics,
13259
+ applyFindingPolicy,
13260
+ loadProjectRulePolicyMap,
13261
+ inlineBlockerIdentities,
13262
+ applyPushedFindingPolicy,
13263
+ applyPushedFindingPolicyByFile,
13264
+ applyLspFindingPolicy,
13265
+ filterFindingsByDisposition,
13266
+ stampLineHashes,
13267
+ owedByContentOnly,
12262
13268
  getToolPlan,
12263
13269
  resetObservedRunnerLatency,
12264
13270
  requeueRunnerFindings,
@@ -12266,25 +13272,28 @@ export {
12266
13272
  dropStaleRunnerFindings,
12267
13273
  resetPendingRunnerFindings,
12268
13274
  pendingRunnerFindingsSize,
12269
- applyRulePolicy,
13275
+ laneCopyStandsFor,
12270
13276
  DELTA_UNUSED_PROMOTION_NOTE,
12271
13277
  formatDiagnostics,
12272
13278
  createDispatchContext,
12273
13279
  getLatencyReports,
12274
13280
  cascadeSettleWaitMs,
12275
- convertLspDiagnostics,
12276
- applyFindingPolicy,
12277
- loadProjectRulePolicyMap,
12278
- inlineBlockerIdentities,
12279
- applyPushedFindingPolicy,
12280
- applyLspFindingPolicy,
12281
- filterFindingsByDisposition,
12282
13281
  markUnreconciledFindings,
12283
13282
  formatCacheAgeLabel,
13283
+ dispositionSuppressedLine,
12284
13284
  admitsProjectWideFindings,
13285
+ formatCascadeNeighborDiagnostics,
13286
+ composeCascadeSection,
12285
13287
  cascadeCarrySuffix,
13288
+ markCascadeUnansweredPrimariesDelivered,
13289
+ renderCascadeResult,
13290
+ cascadeStillPresentSince,
13291
+ recordDeliveredCascadeNeighbor,
13292
+ forgetCascadePendingNeighbor,
13293
+ listCascadePendingNeighbors,
12286
13294
  logCascade,
12287
13295
  resetCascadeTierSessionState,
13296
+ peekOutstandingCascadeTouches,
12288
13297
  loadSg,
12289
13298
  canHandle,
12290
13299
  getLang,