@herbertgao/pi-extensions 2026.8.5 → 2026.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +5 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +24 -17
- package/node_modules/@narumitw/pi-btw/package.json +5 -5
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +33 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +95 -0
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- package/node_modules/pi-lens/dist/clients/biome-client.js +121 -13
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
- package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +135 -30
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +19 -7
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +6 -4
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +114 -10
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +101 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +230 -50
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +296 -3
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
- package/node_modules/pi-lens/dist/clients/git-guard.js +18 -21
- package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +116 -7
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +4 -9
- package/node_modules/pi-lens/dist/clients/knip-client.js +51 -14
- package/node_modules/pi-lens/dist/clients/latency-logger.js +50 -1
- package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +58 -13
- package/node_modules/pi-lens/dist/clients/lsp/client.js +169 -8
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +29 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +264 -66
- package/node_modules/pi-lens/dist/clients/lsp/server.js +247 -60
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
- package/node_modules/pi-lens/dist/clients/mcp/session.js +30 -17
- package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +29 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
- package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +153 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +104 -11
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +170 -22
- package/node_modules/pi-lens/dist/clients/runtime-session.js +28 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +124 -10
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +388 -35
- package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +88 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +141 -23
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
- package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +431 -24
- package/node_modules/pi-lens/dist/clients/tool-policy.js +2 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
- package/node_modules/pi-lens/dist/index.js +4827 -1633
- package/node_modules/pi-lens/dist/mcp/server.js +8 -2
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +3 -2
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +62 -7
- package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +38 -13
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +11 -3
- package/node_modules/pi-lens/docs/features.md +14 -1
- package/node_modules/pi-lens/docs/globalconfig.md +11 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
- package/node_modules/pi-lens/docs/settings.md +6 -0
- package/node_modules/pi-lens/docs/usage.md +23 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -0
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +79 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
- package/node_modules/pi-mcp-adapter/README.md +4 -1
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +18 -15
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +18 -8
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +5 -0
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
- package/node_modules/pi-mcp-adapter/types.ts +93 -10
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +9 -9
|
@@ -15,10 +15,30 @@ import { getGlobalPiLensDir } from "./file-utils.js";
|
|
|
15
15
|
import { findGlobalBinary } from "./package-manager.js";
|
|
16
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
17
17
|
import { biomeConfigArgs } from "./tool-policy.js";
|
|
18
|
-
import { resolveManagedToolClient } from "./dispatch/runners/utils/runner-helpers.js";
|
|
18
|
+
import { resolveManagedToolClient, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
19
|
+
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
19
20
|
// --- Client ---
|
|
21
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
20
22
|
export class BiomeClient {
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Availability memo, backed by the shared transient-aware latch (#1476).
|
|
25
|
+
*
|
|
26
|
+
* Biome is the hot-path formatter: before this, ANY probe failure — a
|
|
27
|
+
* timeout included — collapsed to `{ outcome: "missing" }`, which both
|
|
28
|
+
* triggered an install and latched `false` for the life of the process. One
|
|
29
|
+
* slow first format then disabled formatting until restart.
|
|
30
|
+
*/
|
|
31
|
+
availabilityLatch = createAvailabilityLatch();
|
|
32
|
+
/** Cause of the last probe, read when the resolution comes back. */
|
|
33
|
+
lastProbeCause = null;
|
|
34
|
+
/**
|
|
35
|
+
* Measurements of the last failed probe, held until the latch decides how
|
|
36
|
+
* long the verdict lasts. The decision record is emitted after that, so it
|
|
37
|
+
* can carry `retryAfterMs` — a latch you can see is worth little if the
|
|
38
|
+
* retry schedule is not greppable beside it (#1474).
|
|
39
|
+
*/
|
|
40
|
+
lastProbeElapsedMs = 0;
|
|
41
|
+
lastProbeHostStallMs = 0;
|
|
22
42
|
// Per-cwd cache of the resolved biome binary. Keying by cwd matters in
|
|
23
43
|
// monorepos where different sub-packages each ship their own biome
|
|
24
44
|
// installation; sharing one slot across the whole client would cause
|
|
@@ -100,10 +120,15 @@ export class BiomeClient {
|
|
|
100
120
|
* `ensureInFlight` promise so probing/auto-install isn't duplicated.
|
|
101
121
|
* Mirrors the dedupe pattern in `SgRunner` / `KnipClient` /
|
|
102
122
|
* `DependencyChecker`.
|
|
123
|
+
*
|
|
124
|
+
* The memo returns `null` when the last verdict was transient and its
|
|
125
|
+
* cooldown expired, which re-enters the probe: "biome is not installed" is a
|
|
126
|
+
* fact worth caching, "the probe timed out" is a moment worth retrying.
|
|
103
127
|
*/
|
|
104
128
|
async ensureAvailable() {
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
const memo = this.availabilityLatch.read();
|
|
130
|
+
if (memo !== null)
|
|
131
|
+
return memo;
|
|
107
132
|
if (this.ensureInFlight)
|
|
108
133
|
return this.ensureInFlight;
|
|
109
134
|
this.ensureInFlight = this.doEnsureAvailable();
|
|
@@ -114,23 +139,106 @@ export class BiomeClient {
|
|
|
114
139
|
this.ensureInFlight = null;
|
|
115
140
|
}
|
|
116
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Probe `biome --version` and classify the failure through the shared
|
|
144
|
+
* policy. Only a durable verdict (`missing` / `non-installable`) may reach
|
|
145
|
+
* the install branch of `resolveManagedToolClient`; a transient one returns
|
|
146
|
+
* as `transient`, which that seam passes straight back without installing.
|
|
147
|
+
*/
|
|
148
|
+
async probeBiome() {
|
|
149
|
+
// The probe budget is enforced by a HOST-side timer, so a stalled event
|
|
150
|
+
// loop expires it while the child is still healthy. Measure the stall
|
|
151
|
+
// that overlapped the window and let the classifier see it (#1467).
|
|
152
|
+
const sampler = startHostStallSampler();
|
|
153
|
+
const startedAt = Date.now();
|
|
154
|
+
let result;
|
|
155
|
+
let hostStallMs;
|
|
156
|
+
try {
|
|
157
|
+
result = await this.spawnBiomeAsync(["--version"], PROBE_TIMEOUT_MS);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
hostStallMs = sampler.stop();
|
|
161
|
+
}
|
|
162
|
+
const elapsedMs = Date.now() - startedAt;
|
|
163
|
+
if (!result.error && result.status === 0) {
|
|
164
|
+
this.lastProbeCause = "ok";
|
|
165
|
+
logAvailabilityDecision({
|
|
166
|
+
tool: "biome",
|
|
167
|
+
verdict: "available",
|
|
168
|
+
outcome: "success",
|
|
169
|
+
cause: "ok",
|
|
170
|
+
elapsedMs,
|
|
171
|
+
latched: true,
|
|
172
|
+
hostStallMs,
|
|
173
|
+
budgetMs: PROBE_TIMEOUT_MS,
|
|
174
|
+
});
|
|
175
|
+
return { outcome: "success", value: true };
|
|
176
|
+
}
|
|
177
|
+
// `unclassifiedFailureOutcome: "missing"` preserves the pre-#1476
|
|
178
|
+
// meaning of a plain non-zero exit (npx reporting no biome package):
|
|
179
|
+
// still "missing", still installable. Only the timeout/abort arm changes.
|
|
180
|
+
const { outcome, cause } = classifyProbeFailure(result, {
|
|
181
|
+
hostStallMs,
|
|
182
|
+
unclassifiedFailureOutcome: "missing",
|
|
183
|
+
});
|
|
184
|
+
// `classifyProbeFailure` is typed over the full outcome union but never
|
|
185
|
+
// returns "success" for a failed probe; narrow it for the seam's type.
|
|
186
|
+
const failureOutcome = outcome === "success" ? "non-installable" : outcome;
|
|
187
|
+
this.lastProbeCause = cause;
|
|
188
|
+
this.lastProbeElapsedMs = elapsedMs;
|
|
189
|
+
this.lastProbeHostStallMs = hostStallMs;
|
|
190
|
+
// The record is emitted by `doEnsureAvailable`, once the latch has said how
|
|
191
|
+
// long this verdict holds.
|
|
192
|
+
return { outcome: failureOutcome };
|
|
193
|
+
}
|
|
117
194
|
async doEnsureAvailable() {
|
|
118
|
-
const
|
|
195
|
+
const resolved = await resolveManagedToolClient({
|
|
119
196
|
toolId: "biome",
|
|
120
197
|
cwd: process.cwd(),
|
|
121
|
-
probe:
|
|
122
|
-
const result = await this.spawnBiomeAsync(["--version"], 10000);
|
|
123
|
-
return !result.error && result.status === 0
|
|
124
|
-
? { outcome: "success", value: true }
|
|
125
|
-
: { outcome: "missing" };
|
|
126
|
-
},
|
|
198
|
+
probe: () => this.probeBiome(),
|
|
127
199
|
acceptInstalled: (installedPath) => {
|
|
128
200
|
this.autoInstalledBinaryPath = installedPath;
|
|
129
201
|
return true;
|
|
130
202
|
},
|
|
131
203
|
});
|
|
132
|
-
|
|
133
|
-
|
|
204
|
+
if (resolved.outcome === "success") {
|
|
205
|
+
this.availabilityLatch.noteAvailable();
|
|
206
|
+
// The probe itself logs a clean hit. This arm is the other way to
|
|
207
|
+
// succeed — the install repaired a durable miss — and it needs its own
|
|
208
|
+
// record, or "biome went missing and came back" reads as silence.
|
|
209
|
+
if (this.lastProbeCause !== null && this.lastProbeCause !== "ok") {
|
|
210
|
+
logAvailabilityDecision({
|
|
211
|
+
tool: "biome",
|
|
212
|
+
verdict: "available",
|
|
213
|
+
outcome: "success",
|
|
214
|
+
cause: "ok",
|
|
215
|
+
elapsedMs: this.lastProbeElapsedMs,
|
|
216
|
+
latched: true,
|
|
217
|
+
hostStallMs: this.lastProbeHostStallMs,
|
|
218
|
+
budgetMs: PROBE_TIMEOUT_MS,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
// A transient probe expires; a durable verdict is remembered for the
|
|
224
|
+
// session exactly as before.
|
|
225
|
+
const cause = this.lastProbeCause ?? "not-found";
|
|
226
|
+
const retryAfterMs = this.availabilityLatch.noteUnavailable(resolved.outcome, cause);
|
|
227
|
+
logAvailabilityDecision({
|
|
228
|
+
tool: "biome",
|
|
229
|
+
verdict: "unavailable",
|
|
230
|
+
outcome: resolved.outcome,
|
|
231
|
+
cause,
|
|
232
|
+
elapsedMs: this.lastProbeElapsedMs,
|
|
233
|
+
latched: resolved.outcome !== "transient",
|
|
234
|
+
hostStallMs: this.lastProbeHostStallMs,
|
|
235
|
+
...(retryAfterMs > 0 && { retryAfterMs }),
|
|
236
|
+
budgetMs: PROBE_TIMEOUT_MS,
|
|
237
|
+
});
|
|
238
|
+
if (resolved.outcome === "transient") {
|
|
239
|
+
this.log("biome availability probe timed out; will retry (not installing)");
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
134
242
|
}
|
|
135
243
|
/**
|
|
136
244
|
* Check if a file is supported by Biome
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent NDJSON trace of `pi.events` bus publish attempts
|
|
3
3
|
* (`pilens:files:touched` #482 / `pilens:diagnostics` #502 /
|
|
4
|
-
* `pilens:
|
|
4
|
+
* `pilens:diagnostic:disposition` / `pilens:format:queued` +
|
|
5
|
+
* `pilens:format:start` + `pilens:autofix:start` #673/#684 /
|
|
6
|
+
* `pi-lens/analysis-complete` + `pi-lens/findings` + `pi-lens/turn-findings`
|
|
7
|
+
* #1415) — nine event names across five producers.
|
|
5
8
|
*
|
|
6
|
-
* All
|
|
7
|
-
* clients/format-events-publish.ts
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* All five producers (clients/bus-publish.ts, clients/diagnostics-publish.ts,
|
|
10
|
+
* clients/disposition-publish.ts, clients/format-events-publish.ts,
|
|
11
|
+
* clients/lens-events.ts) are fire-and-forget: on failure or on a structural
|
|
12
|
+
* no-op (never wired, kill switch off) the four #482/#502/#673/#684
|
|
13
|
+
* producers only invoke an optional `dbg` callback, which varies by host and
|
|
14
|
+
* is a documented no-op in the MCP host (clients/mcp/session.ts's
|
|
15
|
+
* `dbg: noop`); `lens-events.ts` (#1415) has no `dbg` param at all — its
|
|
16
|
+
* events are purely observational inter-extension telemetry, so this NDJSON
|
|
17
|
+
* trace is its ONLY failure-visible surface. Either way, that leaves
|
|
11
18
|
* bus-publish outcomes invisible in exactly the context where they matter
|
|
12
19
|
* most — same failure shape as the #544 MCP session_start incident this repo
|
|
13
20
|
* already fixed once.
|
|
@@ -26,6 +33,9 @@
|
|
|
26
33
|
* spam one identical line per write batch for an entire session with zero
|
|
27
34
|
* new information after the first. Both are gated log-once-per-process, the
|
|
28
35
|
* same `hasLoggedFailure` shape the producers already use for emit_failed.
|
|
36
|
+
* `skipped_stale_session` is an info-level per-occurrence outcome: the emit
|
|
37
|
+
* seam intentionally declined a target whose associated ctx was confirmed
|
|
38
|
+
* stale, so it is neither a bus failure nor input for the smells rollup.
|
|
29
39
|
* The empty-batch branch (`paths.length === 0` / `files.length === 0`) is
|
|
30
40
|
* NOT logged at all: every call site already guards against invoking these
|
|
31
41
|
* functions with nothing to report (see clients/pipeline.ts,
|
|
@@ -37,17 +47,63 @@ import { isTestMode } from "./env-utils.js";
|
|
|
37
47
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
38
48
|
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
39
49
|
import { getMaxLogSizeMB } from "./log-cleanup.js";
|
|
50
|
+
import { logLatency } from "./latency-logger.js";
|
|
40
51
|
const BUS_EVENTS_LOG_FILE = path.join(getGlobalPiLensDir(), "bus-events.log");
|
|
41
52
|
const writer = createNdjsonLogger({
|
|
42
53
|
filePath: BUS_EVENTS_LOG_FILE,
|
|
43
54
|
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
|
|
44
55
|
});
|
|
56
|
+
const eventRollupCounts = new Map();
|
|
57
|
+
function bumpRollupCount(event, outcome) {
|
|
58
|
+
const existing = eventRollupCounts.get(event) ?? {
|
|
59
|
+
emitted: 0,
|
|
60
|
+
skipped_stale_session: 0,
|
|
61
|
+
emit_failed: 0,
|
|
62
|
+
};
|
|
63
|
+
existing[outcome] += 1;
|
|
64
|
+
eventRollupCounts.set(event, existing);
|
|
65
|
+
}
|
|
45
66
|
export function logBusEvent(entry) {
|
|
67
|
+
if (entry.outcome === "emitted" ||
|
|
68
|
+
entry.outcome === "skipped_stale_session" ||
|
|
69
|
+
entry.outcome === "emit_failed") {
|
|
70
|
+
bumpRollupCount(entry.event, entry.outcome);
|
|
71
|
+
}
|
|
46
72
|
if (isTestMode()) {
|
|
47
73
|
return;
|
|
48
74
|
}
|
|
49
75
|
writer.log({ ts: new Date().toISOString(), ...entry });
|
|
50
76
|
}
|
|
77
|
+
/** Snapshot the current session's rollup, keyed by event name. Non-mutating —
|
|
78
|
+
* pair with {@link resetBusEventRollupCounts} at session end. */
|
|
79
|
+
export function getBusEventRollupCounts() {
|
|
80
|
+
return Object.fromEntries(eventRollupCounts);
|
|
81
|
+
}
|
|
82
|
+
/** Clear the rollup — call once the session-end snapshot has been logged
|
|
83
|
+
* (or from tests) so a new session starts from zero. */
|
|
84
|
+
export function resetBusEventRollupCounts() {
|
|
85
|
+
eventRollupCounts.clear();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Log one `session_end_bus_rollup` latency row per event NAME that had any
|
|
89
|
+
* activity this session, then clear the rollup. Called from index.ts's
|
|
90
|
+
* `session_shutdown` handler (the session-end hook this repo's other
|
|
91
|
+
* teardown work — LSP fleet shutdown, cache-prefix eviction — already runs
|
|
92
|
+
* from). A no-op (logs nothing) when nothing was ever published, matching
|
|
93
|
+
* `formatSmellsSessionStartLine`'s "no noise on an ordinary session" shape.
|
|
94
|
+
*/
|
|
95
|
+
export function emitBusEventRollupAtSessionEnd(cwd) {
|
|
96
|
+
for (const [event, counts] of eventRollupCounts) {
|
|
97
|
+
logLatency({
|
|
98
|
+
type: "phase",
|
|
99
|
+
phase: "session_end_bus_rollup",
|
|
100
|
+
filePath: cwd,
|
|
101
|
+
durationMs: 0,
|
|
102
|
+
metadata: { event, ...counts },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
resetBusEventRollupCounts();
|
|
106
|
+
}
|
|
51
107
|
export function getBusEventsLogPath() {
|
|
52
108
|
return BUS_EVENTS_LOG_FILE;
|
|
53
109
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { logBusEvent } from "./bus-events-logger.js";
|
|
21
21
|
import { normalizeFilePath } from "./path-utils.js";
|
|
22
22
|
import { appendRecentTouches } from "./recent-touches.js";
|
|
23
|
-
import { createLiveBusEmitter, recordStaleBusFailure, } from "./live-bus-emitter.js";
|
|
23
|
+
import { createLiveBusEmitter, recordStaleBusFailure, resolveLiveBusEmitter, } from "./live-bus-emitter.js";
|
|
24
24
|
export const BUS_FILES_TOUCHED_EVENT = "pilens:files:touched";
|
|
25
25
|
export const BUS_FILES_TOUCHED_VERSION = 1;
|
|
26
26
|
const liveEmitter = createLiveBusEmitter();
|
|
@@ -106,8 +106,14 @@ export function publishFilesTouched(args) {
|
|
|
106
106
|
}).catch((err) => {
|
|
107
107
|
args.dbg?.(`bus-publish: recent-touches append failed: ${err}`);
|
|
108
108
|
});
|
|
109
|
-
const
|
|
110
|
-
|
|
109
|
+
const resolution = resolveLiveBusEmitter(liveEmitter, () => ({
|
|
110
|
+
event: BUS_FILES_TOUCHED_EVENT,
|
|
111
|
+
cwd: normalizeFilePath(args.cwd),
|
|
112
|
+
reason: args.reason,
|
|
113
|
+
}));
|
|
114
|
+
if (resolution.outcome === "stale-session")
|
|
115
|
+
return;
|
|
116
|
+
if (resolution.outcome === "unwired") {
|
|
111
117
|
if (!hasLoggedUnwired) {
|
|
112
118
|
hasLoggedUnwired = true;
|
|
113
119
|
logBusEvent({
|
|
@@ -119,6 +125,7 @@ export function publishFilesTouched(args) {
|
|
|
119
125
|
}
|
|
120
126
|
return;
|
|
121
127
|
}
|
|
128
|
+
const busEmit = resolution.emit;
|
|
122
129
|
try {
|
|
123
130
|
const payload = {
|
|
124
131
|
v: BUS_FILES_TOUCHED_VERSION,
|
|
@@ -150,6 +157,7 @@ export function publishFilesTouched(args) {
|
|
|
150
157
|
cwd: normalizeFilePath(args.cwd),
|
|
151
158
|
reason: args.reason,
|
|
152
159
|
error: String(err),
|
|
160
|
+
ctxSource: resolution.ctxSource,
|
|
153
161
|
});
|
|
154
162
|
if (!hasLoggedFailure) {
|
|
155
163
|
hasLoggedFailure = true;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
|
|
1
2
|
import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
|
|
2
3
|
export function formatCascadeNeighborDiagnostics(cwd, neighbors, options = {}) {
|
|
3
4
|
const withErrors = neighbors.filter((n) => n.diagnostics.length > 0);
|
|
4
|
-
|
|
5
|
+
const inconclusive = neighbors.filter((n) => n.inconclusive === true && n.diagnostics.length === 0);
|
|
6
|
+
if (withErrors.length === 0 && inconclusive.length === 0)
|
|
5
7
|
return "";
|
|
6
8
|
const noun = options.noun ?? "neighbor";
|
|
7
|
-
let out =
|
|
9
|
+
let out = withErrors.length > 0
|
|
10
|
+
? `📐 Cascade errors in ${withErrors.length} ${noun} file(s) — fix before finishing turn:`
|
|
11
|
+
: "";
|
|
8
12
|
for (const neighbor of withErrors) {
|
|
9
13
|
const display = toRunnerDisplayPath(cwd, neighbor.filePath);
|
|
10
14
|
const reason = options.includeReason ? ` reason="${neighbor.reason}"` : "";
|
|
@@ -17,5 +21,56 @@ export function formatCascadeNeighborDiagnostics(cwd, neighbors, options = {}) {
|
|
|
17
21
|
}
|
|
18
22
|
out += "\n</diagnostics>";
|
|
19
23
|
}
|
|
24
|
+
if (inconclusive.length > 0) {
|
|
25
|
+
if (out)
|
|
26
|
+
out += "\n";
|
|
27
|
+
out += `⚠️ Cascade diagnostics inconclusive for ${inconclusive.length} ${noun} file(s) — no clean result was confirmed:`;
|
|
28
|
+
for (const neighbor of inconclusive) {
|
|
29
|
+
out += `\n ${toRunnerDisplayPath(cwd, neighbor.filePath)}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
20
32
|
return out;
|
|
21
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the turn-end `CascadeRun` for a neighbour whose diagnostics landed only
|
|
36
|
+
* AFTER its cascade touch skipped the in-lane wait (#1023's `resolved-found`
|
|
37
|
+
* quiet-window outcome; #1444 made native TS7 take that same path). Returns
|
|
38
|
+
* `undefined` when there is nothing agent-facing to say — no ERROR-severity
|
|
39
|
+
* diagnostics, or nothing the formatter renders.
|
|
40
|
+
*
|
|
41
|
+
* Lives here rather than inline in the quiet-window callback so the delivery
|
|
42
|
+
* path (reconcile → run → turn_end) is testable end to end; index.ts only wires
|
|
43
|
+
* it to `runtime.appendCascadeRun`.
|
|
44
|
+
*/
|
|
45
|
+
export function buildResolvedFoundCascadeRun(cwd, neighbor) {
|
|
46
|
+
const { filePath } = neighbor;
|
|
47
|
+
const diagnostics = convertLspDiagnostics(neighbor.diagnostics.filter((d) => d.severity === 1), filePath, { tool: "lsp" });
|
|
48
|
+
if (diagnostics.length === 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
const neighbors = [
|
|
51
|
+
{ filePath, reason: "references", diagnostics, lspTouched: true },
|
|
52
|
+
];
|
|
53
|
+
const formatted = formatCascadeNeighborDiagnostics(cwd, neighbors, {
|
|
54
|
+
noun: "cold neighbor",
|
|
55
|
+
});
|
|
56
|
+
if (!formatted)
|
|
57
|
+
return undefined;
|
|
58
|
+
return {
|
|
59
|
+
filePath,
|
|
60
|
+
result: {
|
|
61
|
+
filePath,
|
|
62
|
+
impact: {
|
|
63
|
+
filePath,
|
|
64
|
+
changedSymbols: [],
|
|
65
|
+
directImporters: [],
|
|
66
|
+
directCallers: [],
|
|
67
|
+
neighborFiles: [filePath],
|
|
68
|
+
riskFlags: [],
|
|
69
|
+
},
|
|
70
|
+
neighbors,
|
|
71
|
+
formatted,
|
|
72
|
+
},
|
|
73
|
+
neighborCount: 1,
|
|
74
|
+
diagnosticCount: diagnostics.length,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { performance } from "node:perf_hooks";
|
|
2
1
|
// Capture this before the guard's other module initialization so host boot is
|
|
3
|
-
// not charged to extension evaluation (#1374).
|
|
4
|
-
|
|
2
|
+
// not charged to extension evaluation (#1374). Lives in its own
|
|
3
|
+
// side-effect-free module (#1434 S3d) — see eval-timestamp.ts's docstring for
|
|
4
|
+
// why: `startup-timing.ts` needs this constant but must never trigger a
|
|
5
|
+
// console-guard install merely by importing it.
|
|
6
|
+
import { PI_LENS_EVAL_STARTED_MS } from "./eval-timestamp.js";
|
|
5
7
|
// #1333: install the console guard as an import side-effect so it runs before
|
|
6
8
|
// any other module's initialization code. Installing inside the extension
|
|
7
9
|
// factory is too late — by the time the factory runs, every static/transitive
|
|
@@ -10,5 +12,15 @@ export const PI_LENS_EVAL_STARTED_MS = performance.now();
|
|
|
10
12
|
// MUST remain the first import of index.ts; the enforcement test pins that.
|
|
11
13
|
// installConsoleGuard() itself is idempotent and no-ops under test mode and
|
|
12
14
|
// PI_LENS_CONSOLE_GUARD=0.
|
|
13
|
-
|
|
15
|
+
// #1434: the guard captures only while pi-lens owns execution, so the module
|
|
16
|
+
// window must be open for the rest of the import graph. index.ts closes it on
|
|
17
|
+
// its last line; an unref'd backstop closes it if that never runs.
|
|
18
|
+
import { installConsoleGuard, openModuleLoadConsoleWindow, } from "./extension-log.js";
|
|
14
19
|
installConsoleGuard();
|
|
20
|
+
openModuleLoadConsoleWindow();
|
|
21
|
+
// Not re-exported: nothing should import the timestamp from this module.
|
|
22
|
+
// `clients/startup-timing.ts` imports it straight from `eval-timestamp.ts`,
|
|
23
|
+
// which is what keeps that import side-effect-free (S3d). The `void` keeps
|
|
24
|
+
// the import from looking unused to lint while documenting that this module
|
|
25
|
+
// only needs the ordering (evaluate eval-timestamp.ts first), not the value.
|
|
26
|
+
void PI_LENS_EVAL_STARTED_MS;
|
|
@@ -15,6 +15,7 @@ import { createSubsystemLogger } from "./extension-log.js";
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { findNearestMarkerRoot } from "./path-utils.js";
|
|
17
17
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
18
|
+
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
18
19
|
function emptyResult(language) {
|
|
19
20
|
return {
|
|
20
21
|
success: false,
|
|
@@ -39,6 +40,21 @@ const VULTURE_EXCLUDES = [
|
|
|
39
40
|
"*/__pycache__/*",
|
|
40
41
|
"*/.eggs/*",
|
|
41
42
|
];
|
|
43
|
+
// Decorators whose target is called by a framework, never by name in the tree.
|
|
44
|
+
// vulture cannot see those call sites, so every such symbol is a permanent
|
|
45
|
+
// false positive that reappears in every scan — the fastest way to teach a
|
|
46
|
+
// reader to stop reading the advisory. Glob-matched by vulture itself.
|
|
47
|
+
const VULTURE_IGNORE_DECORATORS = [
|
|
48
|
+
"@pytest.fixture",
|
|
49
|
+
"@pytest.fixture(*",
|
|
50
|
+
"@fixture",
|
|
51
|
+
"@fixture(*",
|
|
52
|
+
"@app.*",
|
|
53
|
+
"@router.*",
|
|
54
|
+
"@celery.task*",
|
|
55
|
+
"@task",
|
|
56
|
+
"@shared_task*",
|
|
57
|
+
];
|
|
42
58
|
// vulture line: `path/to/file.py:12: unused function 'foo' (60% confidence)`
|
|
43
59
|
const VULTURE_LINE = /^(.*?):(\d+): unused (\w[\w ]*?) '([^']+)' \((\d+)% confidence\)\s*$/;
|
|
44
60
|
/**
|
|
@@ -86,7 +102,12 @@ export function parseVultureOutput(output, root) {
|
|
|
86
102
|
export class PythonDeadCodeClient {
|
|
87
103
|
id = "python";
|
|
88
104
|
language = "Python";
|
|
89
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Transient-aware memo (#1467): only a durable "vulture is not installed"
|
|
107
|
+
* verdict is remembered for the session. A probe that timed out expires and
|
|
108
|
+
* is re-probed, so vulture cannot be disabled for the process by one stall.
|
|
109
|
+
*/
|
|
110
|
+
availabilityLatch = createAvailabilityLatch();
|
|
90
111
|
resolved = null;
|
|
91
112
|
ensureInFlight = null;
|
|
92
113
|
inFlight = new Map();
|
|
@@ -103,6 +124,10 @@ export class PythonDeadCodeClient {
|
|
|
103
124
|
detect(cwd) {
|
|
104
125
|
return this.resolveProjectRoot(cwd) !== null;
|
|
105
126
|
}
|
|
127
|
+
owns(filePath) {
|
|
128
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
129
|
+
return ext === ".py" || ext === ".pyi";
|
|
130
|
+
}
|
|
106
131
|
/**
|
|
107
132
|
* Nearest dir with a Python project marker, never at/above $HOME and never
|
|
108
133
|
* escaping a VCS boundary — same containment rules as KnipClient so a scan
|
|
@@ -121,8 +146,9 @@ export class PythonDeadCodeClient {
|
|
|
121
146
|
], { boundaries: [".git", ".hg", ".svn"], homeDir: homeDirOverride });
|
|
122
147
|
}
|
|
123
148
|
async ensureAvailable() {
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
const memo = this.availabilityLatch.read();
|
|
150
|
+
if (memo !== null)
|
|
151
|
+
return memo;
|
|
126
152
|
if (this.ensureInFlight)
|
|
127
153
|
return this.ensureInFlight;
|
|
128
154
|
this.ensureInFlight = this.doEnsureAvailable();
|
|
@@ -146,19 +172,81 @@ export class PythonDeadCodeClient {
|
|
|
146
172
|
{ cmd: "python", prefix: ["-m", "vulture"] },
|
|
147
173
|
{ cmd: "python3", prefix: ["-m", "vulture"] },
|
|
148
174
|
];
|
|
175
|
+
// A timeout on ANY candidate means the machine, not the tool, answered —
|
|
176
|
+
// the run gets a bounded retry instead of a permanent skip (#1467).
|
|
177
|
+
let sawTransient = false;
|
|
178
|
+
let transientCause = "probe-timeout";
|
|
179
|
+
// Accumulated across ALL candidates, because the failure verdicts below
|
|
180
|
+
// are about the whole sweep rather than any one probe. Reporting zero
|
|
181
|
+
// here would erase the evidence that cracked #1467: four 5s probes and
|
|
182
|
+
// a stalled host look identical to an absent tool without these two
|
|
183
|
+
// numbers.
|
|
184
|
+
const sweepStartedAt = Date.now();
|
|
185
|
+
let sweepHostStallMs = 0;
|
|
149
186
|
for (const c of candidates) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
187
|
+
const sampler = startHostStallSampler();
|
|
188
|
+
const startedAt = Date.now();
|
|
189
|
+
let probe;
|
|
190
|
+
let hostStallMs;
|
|
191
|
+
try {
|
|
192
|
+
probe = await safeSpawnAsync(c.cmd, [...c.prefix, "--version"], {
|
|
193
|
+
timeout: 5000,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
hostStallMs = sampler.stop();
|
|
198
|
+
sweepHostStallMs += hostStallMs;
|
|
199
|
+
}
|
|
153
200
|
if (!probe.error && probe.status === 0) {
|
|
154
201
|
this.resolved = c;
|
|
155
|
-
this.
|
|
202
|
+
this.availabilityLatch.noteAvailable();
|
|
156
203
|
this.log(`vulture found: ${[c.cmd, ...c.prefix].join(" ")}`);
|
|
204
|
+
logAvailabilityDecision({
|
|
205
|
+
tool: "vulture",
|
|
206
|
+
verdict: "available",
|
|
207
|
+
outcome: "success",
|
|
208
|
+
cause: "ok",
|
|
209
|
+
elapsedMs: Date.now() - startedAt,
|
|
210
|
+
latched: true,
|
|
211
|
+
hostStallMs,
|
|
212
|
+
budgetMs: 5000,
|
|
213
|
+
});
|
|
157
214
|
return true;
|
|
158
215
|
}
|
|
216
|
+
const classified = classifyProbeFailure(probe, { hostStallMs });
|
|
217
|
+
if (classified.outcome === "transient") {
|
|
218
|
+
sawTransient = true;
|
|
219
|
+
transientCause = classified.cause;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (sawTransient) {
|
|
223
|
+
const retryAfterMs = this.availabilityLatch.noteUnavailable("transient", transientCause);
|
|
224
|
+
this.log("vulture probe timed out; will retry (not treated as missing)");
|
|
225
|
+
logAvailabilityDecision({
|
|
226
|
+
tool: "vulture",
|
|
227
|
+
verdict: "unavailable",
|
|
228
|
+
outcome: "transient",
|
|
229
|
+
cause: transientCause,
|
|
230
|
+
elapsedMs: Date.now() - sweepStartedAt,
|
|
231
|
+
hostStallMs: sweepHostStallMs,
|
|
232
|
+
latched: false,
|
|
233
|
+
retryAfterMs,
|
|
234
|
+
budgetMs: 5000,
|
|
235
|
+
});
|
|
236
|
+
return false;
|
|
159
237
|
}
|
|
160
|
-
this.
|
|
238
|
+
this.availabilityLatch.noteUnavailable("missing", "not-found");
|
|
161
239
|
this.log("vulture not installed; skipping (no auto-install)");
|
|
240
|
+
logAvailabilityDecision({
|
|
241
|
+
tool: "vulture",
|
|
242
|
+
verdict: "unavailable",
|
|
243
|
+
outcome: "missing",
|
|
244
|
+
cause: "not-found",
|
|
245
|
+
elapsedMs: Date.now() - sweepStartedAt,
|
|
246
|
+
hostStallMs: sweepHostStallMs,
|
|
247
|
+
latched: true,
|
|
248
|
+
budgetMs: 5000,
|
|
249
|
+
});
|
|
162
250
|
return false;
|
|
163
251
|
}
|
|
164
252
|
async analyze(cwd) {
|
|
@@ -193,6 +281,7 @@ export class PythonDeadCodeClient {
|
|
|
193
281
|
".",
|
|
194
282
|
`--min-confidence=${this.minConfidence}`,
|
|
195
283
|
`--exclude=${VULTURE_EXCLUDES.join(",")}`,
|
|
284
|
+
`--ignore-decorators=${VULTURE_IGNORE_DECORATORS.join(",")}`,
|
|
196
285
|
];
|
|
197
286
|
const result = await safeSpawnAsync(invocation.cmd, args, {
|
|
198
287
|
timeout: ANALYSIS_TIMEOUT_MS,
|
|
@@ -258,30 +347,46 @@ export function deadCodeIssueCount(result) {
|
|
|
258
347
|
result.unusedDeps.length +
|
|
259
348
|
result.unlistedDeps.length);
|
|
260
349
|
}
|
|
350
|
+
/** Every bucket flattened — delta diffing and diagnostic mapping want one list. */
|
|
351
|
+
export function deadCodeIssues(result) {
|
|
352
|
+
return [
|
|
353
|
+
...result.unusedExports,
|
|
354
|
+
...result.unusedFiles,
|
|
355
|
+
...result.unusedDeps,
|
|
356
|
+
...result.unlistedDeps,
|
|
357
|
+
];
|
|
358
|
+
}
|
|
261
359
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
360
|
+
* Stable identity for diffing one scan against the previous one.
|
|
361
|
+
*
|
|
362
|
+
* Deliberately excludes the line number. An edit shifts every line below it,
|
|
363
|
+
* and the delta is then filtered to exactly the files the edit touched — so a
|
|
364
|
+
* line in the key turns each shifted pre-existing finding into a "newly unused"
|
|
365
|
+
* report under a heading that blames the agent's own edit for orphaning it.
|
|
366
|
+
* Inserting four lines above one real finding produced four false ones.
|
|
266
367
|
*/
|
|
267
|
-
export function
|
|
268
|
-
|
|
269
|
-
|
|
368
|
+
export function deadCodeIssueKey(issue) {
|
|
369
|
+
return `${issue.category}:${issue.file ?? ""}:${issue.name}`;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Format this turn's ATTRIBUTABLE delta — symbols that became unused in files
|
|
373
|
+
* the agent just edited — or "" when there is nothing to report. Mirrors the
|
|
374
|
+
* Knip advisory's contract deliberately: the project-wide list is not injected
|
|
375
|
+
* per turn (hundreds of pre-existing findings would drown the blockers and burn
|
|
376
|
+
* context every turn), it stays available on demand via lens_diagnostics.
|
|
377
|
+
*/
|
|
378
|
+
export function formatDeadCodeDelta(issues, language, max = 5) {
|
|
379
|
+
if (issues.length === 0)
|
|
270
380
|
return "";
|
|
271
|
-
|
|
272
|
-
for (const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
if (r.unusedExports.length > maxPerLang) {
|
|
282
|
-
lines.push(` … and ${r.unusedExports.length - maxPerLang} more`);
|
|
283
|
-
}
|
|
381
|
+
let report = `💀 Newly unused ${language} symbols in files you edited — check if callers need updating (dead-code):\n`;
|
|
382
|
+
for (const issue of issues.slice(0, max)) {
|
|
383
|
+
const loc = issue.file
|
|
384
|
+
? `${issue.file}${issue.line ? `:${issue.line}` : ""}`
|
|
385
|
+
: "(unknown)";
|
|
386
|
+
report += ` ${loc} — unused ${issue.kind} ${issue.name}\n`;
|
|
387
|
+
}
|
|
388
|
+
if (issues.length > max) {
|
|
389
|
+
report += ` … and ${issues.length - max} more\n`;
|
|
284
390
|
}
|
|
285
|
-
return
|
|
286
|
-
lines.join("\n "));
|
|
391
|
+
return report;
|
|
287
392
|
}
|