pi-lens 3.8.70 → 3.8.71
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/CHANGELOG.md +24 -0
- package/README.md +16 -0
- package/dist/clients/bus-events-logger.js +10 -8
- package/dist/clients/cache/rule-cache.js +9 -5
- package/dist/clients/cache-manager.js +19 -13
- package/dist/clients/diagnostic-dispositions.js +298 -0
- package/dist/clients/dispatch/auxiliary-lsp.js +85 -3
- package/dist/clients/dispatch/dispatcher.js +6 -1
- package/dist/clients/dispatch/integration.js +19 -4
- package/dist/clients/dispatch/runners/lsp.js +5 -32
- package/dist/clients/dispatch/runners/oxlint.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +68 -0
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +4 -1
- package/dist/clients/disposition-logger.js +39 -0
- package/dist/clients/disposition-publish.js +109 -0
- package/dist/clients/file-utils.js +94 -33
- package/dist/clients/format-events-publish.js +299 -0
- package/dist/clients/git-tracked-ignore.js +208 -0
- package/dist/clients/instance-registry.js +74 -4
- package/dist/clients/json-cache-read.js +65 -0
- package/dist/clients/language-profile.js +4 -0
- package/dist/clients/lens-map.js +1079 -0
- package/dist/clients/lsp/aggregation.js +69 -2
- package/dist/clients/lsp/client.js +72 -7
- package/dist/clients/lsp/index.js +486 -14
- package/dist/clients/lsp/server.js +17 -1
- package/dist/clients/lsp/tsserver-sync.js +145 -0
- package/dist/clients/lsp/workspace-diagnostics-cache.js +172 -0
- package/dist/clients/mcp/session.js +8 -0
- package/dist/clients/opengrep-config.js +2 -10
- package/dist/clients/path-utils.js +26 -0
- package/dist/clients/pipeline.js +4 -0
- package/dist/clients/project-diagnostics/cache.js +5 -12
- package/dist/clients/project-snapshot.js +3 -6
- package/dist/clients/read-guard.js +1 -1
- package/dist/clients/review-graph/builder.js +78 -52
- package/dist/clients/review-graph/import-resolvers.js +53 -19
- package/dist/clients/runtime-agent-end.js +26 -0
- package/dist/clients/runtime-coordinator.js +9 -1
- package/dist/clients/runtime-session.js +53 -11
- package/dist/clients/runtime-tool-call.js +789 -0
- package/dist/clients/runtime-tool-result.js +15 -2
- package/dist/clients/runtime-turn.js +9 -2
- package/dist/clients/session-state-store.js +6 -9
- package/dist/clients/sgconfig.js +2 -9
- package/dist/clients/source-filter.js +5 -0
- package/dist/clients/startup-scan.js +68 -1
- package/dist/clients/typos-config.js +2 -11
- package/dist/clients/widget-state.js +22 -0
- package/dist/clients/zizmor-config.js +2 -10
- package/dist/index.js +21089 -18685
- package/dist/tools/lens-diagnostic-mark.js +158 -0
- package/dist/tools/lens-diagnostics.js +41 -7
- package/dist/tools/lsp-diagnostics.js +99 -119
- package/docs/agent-tools.md +11 -4
- package/docs/dispositions.md +85 -0
- package/docs/environment-variables.md +16 -0
- package/docs/fable.md +10 -9
- package/docs/servercapabilities.md +1 -1
- package/docs/tools.md +3 -0
- package/package.json +6 -1
- package/rules/ast-grep-rules/rule-tests/find-import-file-without-extension-test.yml +4 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +5 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +9 -0
- package/rules/ast-grep-rules/rules/find-import-file-without-extension.yml +3 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +12 -0
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +12 -0
- package/docs/agent-tooling-improvement-plan.md +0 -660
- package/docs/inspiration_gograph.md +0 -601
- package/docs/inspiration_neovim.md +0 -1795
- package/docs/inspiration_neovim2.md +0 -402
- package/docs/inspiration_tree_sitter_rules.md +0 -369
- package/docs/inspiration_tree_sitter_rules_investigation.md +0 -328
- package/docs/inspiration_tree_sitter_rules_round2.md +0 -470
- package/docs/lsp-bridge-analysis.md +0 -444
- package/docs/newinspiration.md +0 -338
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-lua.wasm +0 -0
- package/grammars/tree-sitter-ocaml.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-toml.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/grammars/tree-sitter-zig.wasm +0 -0
|
@@ -5,8 +5,9 @@ import * as fs from "node:fs";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { minimatch } from "./deps/minimatch.js";
|
|
8
|
+
import { collectTrackedFiles, getTrackedFilesSnapshot } from "./git-tracked-ignore.js";
|
|
8
9
|
import { getGlobalIgnorePatterns, getPiLensGlobalConfigPath, } from "./lens-config.js";
|
|
9
|
-
import { normalizeFilePath } from "./path-utils.js";
|
|
10
|
+
import { normalizeEphemeralMapKey, normalizeFilePath } from "./path-utils.js";
|
|
10
11
|
import { findPiLensProjectConfig, loadPiLensProjectConfig, } from "./project-lens-config.js";
|
|
11
12
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
12
13
|
/**
|
|
@@ -167,7 +168,7 @@ function stripTrailingSpaces(value) {
|
|
|
167
168
|
end -= 1;
|
|
168
169
|
return value.slice(0, end).replace(/\\ /g, " ");
|
|
169
170
|
}
|
|
170
|
-
function parseGitignoreContent(content) {
|
|
171
|
+
function parseGitignoreContent(content, layer) {
|
|
171
172
|
const patterns = [];
|
|
172
173
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
173
174
|
let line = stripTrailingSpaces(rawLine.trimStart());
|
|
@@ -195,6 +196,7 @@ function parseGitignoreContent(content) {
|
|
|
195
196
|
directoryOnly,
|
|
196
197
|
rooted,
|
|
197
198
|
hasSlash: line.includes("/"),
|
|
199
|
+
layer,
|
|
198
200
|
});
|
|
199
201
|
}
|
|
200
202
|
return patterns;
|
|
@@ -225,10 +227,10 @@ function matchesGitignorePattern(pattern, relativePath, isDirectory) {
|
|
|
225
227
|
return candidates.some((value) => minimatch(value, expanded, options));
|
|
226
228
|
});
|
|
227
229
|
}
|
|
228
|
-
export function readGitignorePatterns(rootDir) {
|
|
230
|
+
export function readGitignorePatterns(rootDir, layer = "gitignore") {
|
|
229
231
|
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
230
232
|
try {
|
|
231
|
-
return parseGitignoreContent(fs.readFileSync(gitignorePath, "utf-8"));
|
|
233
|
+
return parseGitignoreContent(fs.readFileSync(gitignorePath, "utf-8"), layer);
|
|
232
234
|
}
|
|
233
235
|
catch {
|
|
234
236
|
return [];
|
|
@@ -264,7 +266,7 @@ function buildProjectIgnoreMatcher(resolvedRoot, patterns) {
|
|
|
264
266
|
});
|
|
265
267
|
return nextPatterns;
|
|
266
268
|
};
|
|
267
|
-
// Per-matcher path →
|
|
269
|
+
// Per-matcher path → pattern-verdict memo. The matcher itself is cached by
|
|
268
270
|
// `getProjectIgnoreMatcher` keyed on `.gitignore` mtime, so this Map's
|
|
269
271
|
// lifetime is bounded to a single set of ignore rules — when any
|
|
270
272
|
// `.gitignore` changes, the matcher is rebuilt and the memo is dropped
|
|
@@ -272,41 +274,97 @@ function buildProjectIgnoreMatcher(resolvedRoot, patterns) {
|
|
|
272
274
|
// jscpd, call-graph, source-filter, pipeline) recomputes O(ancestorDirs ×
|
|
273
275
|
// patterns) per file, multiplying into 2-3s of pure CPU on a 2k-file
|
|
274
276
|
// project. With it, the second visitor of the same path is O(1).
|
|
275
|
-
|
|
277
|
+
//
|
|
278
|
+
// #703: this memoizes only the PATTERN verdict (which is deterministic —
|
|
279
|
+
// it never changes for a given matcher instance), NOT the tracked-aware
|
|
280
|
+
// final verdict. The tracked-files set can transition from "not yet
|
|
281
|
+
// primed" to "primed" mid-process (a walker calls `ensureTrackedIndex()`
|
|
282
|
+
// partway through the matcher's lifetime), so baking the tracked check
|
|
283
|
+
// into this memo would let an early, pre-priming call poison every later
|
|
284
|
+
// lookup of the same path for this matcher's entire cache lifetime. The
|
|
285
|
+
// tracked-set lookup itself is a cheap Set#has over syntactically-folded
|
|
286
|
+
// keys (see `isTrackedAndRescued` — no `realpathSync` anywhere in this
|
|
287
|
+
// function), and — critically — it's only ever paid for paths a pattern
|
|
288
|
+
// already flagged as ignored, so it doesn't reintroduce the per-file cost
|
|
289
|
+
// this memo exists to avoid for the common (not-ignored) case.
|
|
290
|
+
const patternMemo = new Map();
|
|
291
|
+
// #703 perf follow-up: `normalizeEphemeralMapKey` (cheap slash-fold +
|
|
292
|
+
// Windows-lowercase, zero fs I/O), NOT `normalizeMapKey` (realpath-backed).
|
|
293
|
+
// This runs on every `isIgnored` call that reaches this branch — walks
|
|
294
|
+
// over this repo alone visit thousands of pattern-ignored compiled
|
|
295
|
+
// `*.js`/`*.d.ts` files, and `dispatch/integration.ts`'s per-edit cascade
|
|
296
|
+
// check hits it too — so a `realpathSync` here would violate the
|
|
297
|
+
// event-loop/slow-FS discipline `isIgnored` is required to keep (#462: 75x
|
|
298
|
+
// slower on 9p/drvfs). Both `resolved` (this matcher's own `path.resolve`,
|
|
299
|
+
// never realpath'd) and the tracked-set's keys (`git-tracked-ignore.ts`,
|
|
300
|
+
// realpath'd ONCE per fetch on the shared root, not per file) are
|
|
301
|
+
// self-consistent within one process/session, which is exactly
|
|
302
|
+
// `normalizeEphemeralMapKey`'s contract — see that function's doc and
|
|
303
|
+
// `git-tracked-ignore.ts`'s module doc for the full reasoning. Accepted
|
|
304
|
+
// edge case: a symlinked or 8.3-short-name project root can make the cheap
|
|
305
|
+
// fold miss even after the fetch side's one realpath — the file then stays
|
|
306
|
+
// pattern-ignored, i.e. degrades to today's (pre-#703) behavior, which is
|
|
307
|
+
// consistent with this whole feature's fail-open contract.
|
|
308
|
+
function isTrackedAndRescued(resolved) {
|
|
309
|
+
const snapshot = getTrackedFilesSnapshot(resolvedRoot);
|
|
310
|
+
if (!snapshot)
|
|
311
|
+
return false; // never primed / git unavailable: fail-open to pattern-only
|
|
312
|
+
return snapshot.has(normalizeEphemeralMapKey(resolved));
|
|
313
|
+
}
|
|
276
314
|
return {
|
|
277
315
|
rootDir: resolvedRoot,
|
|
278
316
|
patterns,
|
|
317
|
+
ensureTrackedIndex() {
|
|
318
|
+
return collectTrackedFiles(resolvedRoot).then(() => undefined);
|
|
319
|
+
},
|
|
279
320
|
isIgnored(filePath, isDirectory = false) {
|
|
280
321
|
const resolved = path.resolve(filePath);
|
|
281
322
|
// Two namespaces (D: for directory queries, F: for file queries)
|
|
282
323
|
// because gitignore semantics differ for trailing-slash patterns.
|
|
283
324
|
const memoKey = (isDirectory ? "D:" : "F:") + resolved;
|
|
284
|
-
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
325
|
+
let verdict = patternMemo.get(memoKey);
|
|
326
|
+
if (verdict === undefined) {
|
|
327
|
+
const rootRelative = path.relative(resolvedRoot, resolved);
|
|
328
|
+
if (!rootRelative ||
|
|
329
|
+
rootRelative.startsWith("..") ||
|
|
330
|
+
path.isAbsolute(rootRelative)) {
|
|
331
|
+
verdict = { ignored: false, layer: undefined };
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
let ignored = false;
|
|
335
|
+
let layer;
|
|
336
|
+
const patternDirs = ancestorDirsBetween(resolvedRoot, path.dirname(resolved));
|
|
337
|
+
for (const dir of patternDirs) {
|
|
338
|
+
const dirPatterns = patternsForDir(dir);
|
|
339
|
+
if (dirPatterns.length === 0)
|
|
340
|
+
continue;
|
|
341
|
+
const relative = path.relative(dir, resolved);
|
|
342
|
+
const normalized = normalizeIgnorePath(relative);
|
|
343
|
+
for (const pattern of dirPatterns) {
|
|
344
|
+
if (!matchesGitignorePattern(pattern, normalized, isDirectory))
|
|
345
|
+
continue;
|
|
346
|
+
ignored = !pattern.negated;
|
|
347
|
+
layer = pattern.layer;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
verdict = { ignored, layer };
|
|
306
351
|
}
|
|
352
|
+
patternMemo.set(memoKey, verdict);
|
|
307
353
|
}
|
|
308
|
-
|
|
309
|
-
|
|
354
|
+
if (!verdict.ignored)
|
|
355
|
+
return false;
|
|
356
|
+
// #703 layer semantics: a winning positive match from `global` or
|
|
357
|
+
// `gitignore` emulates git, so it inherits git's "a tracked file is
|
|
358
|
+
// never ignored" rule. A winning match from `pilens` is pi-lens-native
|
|
359
|
+
// intent and stays excluded regardless of tracked status. Directory
|
|
360
|
+
// queries are never tracked-rescued — the tracked set is a file-id
|
|
361
|
+
// set, not a directory set.
|
|
362
|
+
if (!isDirectory &&
|
|
363
|
+
verdict.layer !== "pilens" &&
|
|
364
|
+
isTrackedAndRescued(resolved)) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
310
368
|
},
|
|
311
369
|
};
|
|
312
370
|
}
|
|
@@ -315,10 +373,13 @@ export function createProjectIgnoreMatcher(rootDir, extraPatterns = [], globalPa
|
|
|
315
373
|
// Precedence is gitignore order: LATER patterns override earlier ones. So
|
|
316
374
|
// global (lowest) → project .gitignore → project .pi-lens.json (highest),
|
|
317
375
|
// which lets a project `!negation` re-include a globally-ignored path (#252).
|
|
376
|
+
// Each layer is tagged (#703) so `isIgnored` can tell a git-emulating match
|
|
377
|
+
// (`global`/`gitignore` — subject to "a tracked file is never ignored")
|
|
378
|
+
// apart from pi-lens-native intent (`pilens` — excludes regardless).
|
|
318
379
|
const patterns = [
|
|
319
|
-
...parseGitignoreContent(globalPatterns.join("\n")),
|
|
320
|
-
...readGitignorePatterns(resolvedRoot),
|
|
321
|
-
...parseGitignoreContent(extraPatterns.join("\n")),
|
|
380
|
+
...parseGitignoreContent(globalPatterns.join("\n"), "global"),
|
|
381
|
+
...readGitignorePatterns(resolvedRoot, "gitignore"),
|
|
382
|
+
...parseGitignoreContent(extraPatterns.join("\n"), "pilens"),
|
|
322
383
|
];
|
|
323
384
|
return buildProjectIgnoreMatcher(resolvedRoot, patterns);
|
|
324
385
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishes `pilens:format:queued`, `pilens:format:start`, and
|
|
3
|
+
* `pilens:autofix:start` on pi's shared `pi.events` bus (#673, #684).
|
|
4
|
+
*
|
|
5
|
+
* Sibling to `clients/bus-publish.ts` (#482 `pilens:files:touched`) and
|
|
6
|
+
* `clients/diagnostics-publish.ts` (#502 `pilens:diagnostics`) rather than a
|
|
7
|
+
* new export crammed into either file: same emit plumbing shape and the same
|
|
8
|
+
* `PI_LENS_BUS_PUBLISH` kill switch (`isBusPublishEnabled`, imported from
|
|
9
|
+
* bus-publish.ts), but this producer owns its own emit-function singleton
|
|
10
|
+
* (wired separately from index.ts, same as diagnostics-publish.ts does) since
|
|
11
|
+
* it has nothing else in common with either sibling's module state.
|
|
12
|
+
*
|
|
13
|
+
* ## Motivation (#673)
|
|
14
|
+
*
|
|
15
|
+
* `pilens:files:touched` only fires AFTER deferred formatting has completed —
|
|
16
|
+
* it tells a same-process listener what pi-lens just mutated, in the past
|
|
17
|
+
* tense. #673 reported a concrete failure mode this leaves uncovered: another
|
|
18
|
+
* in-process extension (a review/snapshot controller) derives an immutable
|
|
19
|
+
* candidate tree from the live worktree mid-turn, unaware that pi-lens has a
|
|
20
|
+
* file queued for deferred formatting that will still land — silently
|
|
21
|
+
* invalidating the snapshot it just took. There was no bus signal for either
|
|
22
|
+
* of the two moments that matter to that use case:
|
|
23
|
+
*
|
|
24
|
+
* 1. A file NEWLY entering the deferred-format pending queue
|
|
25
|
+
* (`pilens:format:queued` — one event per file, only on first queue
|
|
26
|
+
* entry; a file re-touched by a second edit before `agent_end` does NOT
|
|
27
|
+
* re-emit, to avoid event spam for repeated edits to the same file).
|
|
28
|
+
* 2. The deferred-format phase actually starting at `agent_end`
|
|
29
|
+
* (`pilens:format:start` — one event per batch, only when there is at
|
|
30
|
+
* least one queued file to format).
|
|
31
|
+
*
|
|
32
|
+
* This is deliberately visibility-only: it lets a listener know pi-lens MIGHT
|
|
33
|
+
* or IS ABOUT TO mutate specific files via deferred formatting, so it can
|
|
34
|
+
* choose to wait, re-derive, or flag its own snapshot as provisional. It is
|
|
35
|
+
* NOT a synchronous flush/barrier API (a caller cannot block deferred
|
|
36
|
+
* formatting via these events) — that remains a separate, explicitly
|
|
37
|
+
* out-of-scope future feature. Completion is already covered by the existing
|
|
38
|
+
* `pilens:files:touched` (`reason: "format"`) event; these two do not
|
|
39
|
+
* duplicate it.
|
|
40
|
+
*
|
|
41
|
+
* ## `pilens:autofix:start` (#684)
|
|
42
|
+
*
|
|
43
|
+
* Follow-up to #673/#674, applying the identical fix to a sibling race: the
|
|
44
|
+
* `agent_end` actionable-warnings autofix batch (`clients/runtime-agent-end.ts`,
|
|
45
|
+
* gated by `getFlag("lens-actionable-warning-autofix")`) also reads a cached
|
|
46
|
+
* report and applies fixes as a batch well after the edit that produced the
|
|
47
|
+
* warnings — same "runs later, unpredictably" shape as deferred formatting.
|
|
48
|
+
* It only emitted `pilens:files:touched` (`reason: "autofix"`) AFTER the fact.
|
|
49
|
+
* `pilens:autofix:start` fires once per batch, only when the cached
|
|
50
|
+
* actionable-warnings report is fresh (`checkActionableWarningsReportFresh`)
|
|
51
|
+
* AND has at least one autofix-eligible warning to act on — mirroring
|
|
52
|
+
* `pilens:format:start`'s "only when there's genuine work" gate. Note: this is
|
|
53
|
+
* distinct from `pipeline.ts`'s per-edit `runAutofix` (biome/ruff/eslint fixes
|
|
54
|
+
* applied synchronously inside the `tool_result` hook, awaited before the
|
|
55
|
+
* tool result returns) — that path isn't deferred and doesn't have this race,
|
|
56
|
+
* so it doesn't get a `start` event.
|
|
57
|
+
*
|
|
58
|
+
* Versioning policy: frozen-additive per event, same discipline as #482/#502.
|
|
59
|
+
* New optional fields may be added under the same `v: 1` for any of the three
|
|
60
|
+
* events; a breaking change to an existing field's meaning must bump that
|
|
61
|
+
* event's `v` independently (each event versions separately since they're
|
|
62
|
+
* unrelated payloads).
|
|
63
|
+
*
|
|
64
|
+
* Fire-and-forget: publishing must never affect the write path's or
|
|
65
|
+
* `agent_end`'s success or latency. Any failure (bus unavailable, emit
|
|
66
|
+
* throws) is swallowed; an optional `dbg` callback is invoked at most once
|
|
67
|
+
* per event type on first failure so a wired caller can log it without
|
|
68
|
+
* spamming.
|
|
69
|
+
*/
|
|
70
|
+
import { logBusEvent } from "./bus-events-logger.js";
|
|
71
|
+
import { isBusPublishEnabled } from "./bus-publish.js";
|
|
72
|
+
import { normalizeFilePath } from "./path-utils.js";
|
|
73
|
+
export const BUS_FORMAT_QUEUED_EVENT = "pilens:format:queued";
|
|
74
|
+
export const BUS_FORMAT_QUEUED_VERSION = 1;
|
|
75
|
+
export const BUS_FORMAT_START_EVENT = "pilens:format:start";
|
|
76
|
+
export const BUS_FORMAT_START_VERSION = 1;
|
|
77
|
+
export const BUS_AUTOFIX_START_EVENT = "pilens:autofix:start";
|
|
78
|
+
export const BUS_AUTOFIX_START_VERSION = 1;
|
|
79
|
+
let busEmit;
|
|
80
|
+
let hasLoggedQueuedFailure = false;
|
|
81
|
+
let hasLoggedQueuedUnwired = false;
|
|
82
|
+
let hasLoggedQueuedDisabled = false;
|
|
83
|
+
let hasLoggedStartFailure = false;
|
|
84
|
+
let hasLoggedStartUnwired = false;
|
|
85
|
+
let hasLoggedStartDisabled = false;
|
|
86
|
+
let hasLoggedAutofixStartFailure = false;
|
|
87
|
+
let hasLoggedAutofixStartUnwired = false;
|
|
88
|
+
let hasLoggedAutofixStartDisabled = false;
|
|
89
|
+
/**
|
|
90
|
+
* Wire the emit function from pi's `pi.events` bus. Called once at extension
|
|
91
|
+
* factory time from index.ts, alongside `wireBusEmitter` (#482) and
|
|
92
|
+
* `wireDiagnosticsBusEmitter` (#502) — all three producers share the
|
|
93
|
+
* identical `pi.events.emit` binding, wired separately per producer.
|
|
94
|
+
*/
|
|
95
|
+
export function wireFormatEventsBusEmitter(emitFn) {
|
|
96
|
+
busEmit = emitFn;
|
|
97
|
+
}
|
|
98
|
+
/** Test-only: reset module state between test files. */
|
|
99
|
+
export function _resetFormatEventsPublishForTests() {
|
|
100
|
+
busEmit = undefined;
|
|
101
|
+
hasLoggedQueuedFailure = false;
|
|
102
|
+
hasLoggedQueuedUnwired = false;
|
|
103
|
+
hasLoggedQueuedDisabled = false;
|
|
104
|
+
hasLoggedStartFailure = false;
|
|
105
|
+
hasLoggedStartUnwired = false;
|
|
106
|
+
hasLoggedStartDisabled = false;
|
|
107
|
+
hasLoggedAutofixStartFailure = false;
|
|
108
|
+
hasLoggedAutofixStartUnwired = false;
|
|
109
|
+
hasLoggedAutofixStartDisabled = false;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Publish one `pilens:format:queued` event for a file newly entering the
|
|
113
|
+
* deferred-format pending queue. Callers MUST only invoke this on the
|
|
114
|
+
* NEW-entry branch of `RuntimeCoordinator.deferFormat` (not on a re-touch of
|
|
115
|
+
* an already-queued file) — see `clients/runtime-tool-result.ts`'s call site.
|
|
116
|
+
* Fire-and-forget: never throws, never awaited by the write path.
|
|
117
|
+
*/
|
|
118
|
+
export function publishFormatQueued(args) {
|
|
119
|
+
if (!isBusPublishEnabled()) {
|
|
120
|
+
if (!hasLoggedQueuedDisabled) {
|
|
121
|
+
hasLoggedQueuedDisabled = true;
|
|
122
|
+
logBusEvent({
|
|
123
|
+
event: BUS_FORMAT_QUEUED_EVENT,
|
|
124
|
+
outcome: "skipped_disabled",
|
|
125
|
+
cwd: normalizeFilePath(args.cwd),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!busEmit) {
|
|
131
|
+
if (!hasLoggedQueuedUnwired) {
|
|
132
|
+
hasLoggedQueuedUnwired = true;
|
|
133
|
+
logBusEvent({
|
|
134
|
+
event: BUS_FORMAT_QUEUED_EVENT,
|
|
135
|
+
outcome: "skipped_unwired",
|
|
136
|
+
cwd: normalizeFilePath(args.cwd),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const payload = {
|
|
143
|
+
v: BUS_FORMAT_QUEUED_VERSION,
|
|
144
|
+
source: "pi-lens",
|
|
145
|
+
filePath: normalizeFilePath(args.filePath),
|
|
146
|
+
cwd: normalizeFilePath(args.cwd),
|
|
147
|
+
tool: args.tool,
|
|
148
|
+
};
|
|
149
|
+
busEmit(BUS_FORMAT_QUEUED_EVENT, payload);
|
|
150
|
+
logBusEvent({
|
|
151
|
+
event: BUS_FORMAT_QUEUED_EVENT,
|
|
152
|
+
outcome: "emitted",
|
|
153
|
+
cwd: payload.cwd,
|
|
154
|
+
fileCount: 1,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
logBusEvent({
|
|
159
|
+
event: BUS_FORMAT_QUEUED_EVENT,
|
|
160
|
+
outcome: "emit_failed",
|
|
161
|
+
cwd: normalizeFilePath(args.cwd),
|
|
162
|
+
error: String(err),
|
|
163
|
+
});
|
|
164
|
+
if (!hasLoggedQueuedFailure) {
|
|
165
|
+
hasLoggedQueuedFailure = true;
|
|
166
|
+
args.dbg?.(`format-events-publish: pilens:format:queued emit failed (further failures suppressed): ${err}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Publish one `pilens:format:start` event when the `agent_end` deferred-
|
|
172
|
+
* format phase begins. Callers MUST only invoke this when `paths.length > 0`
|
|
173
|
+
* — see `clients/runtime-agent-end.ts`'s call site, placed at the same point
|
|
174
|
+
* as the existing `agent_end_deferred_format_start` latency-log phase so both
|
|
175
|
+
* signals represent the identical moment. Fire-and-forget: never throws,
|
|
176
|
+
* never awaited by `agent_end`.
|
|
177
|
+
*/
|
|
178
|
+
export function publishFormatStart(args) {
|
|
179
|
+
if (args.paths.length === 0)
|
|
180
|
+
return;
|
|
181
|
+
if (!isBusPublishEnabled()) {
|
|
182
|
+
if (!hasLoggedStartDisabled) {
|
|
183
|
+
hasLoggedStartDisabled = true;
|
|
184
|
+
logBusEvent({
|
|
185
|
+
event: BUS_FORMAT_START_EVENT,
|
|
186
|
+
outcome: "skipped_disabled",
|
|
187
|
+
cwd: normalizeFilePath(args.cwd),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!busEmit) {
|
|
193
|
+
if (!hasLoggedStartUnwired) {
|
|
194
|
+
hasLoggedStartUnwired = true;
|
|
195
|
+
logBusEvent({
|
|
196
|
+
event: BUS_FORMAT_START_EVENT,
|
|
197
|
+
outcome: "skipped_unwired",
|
|
198
|
+
cwd: normalizeFilePath(args.cwd),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const paths = args.paths.map((p) => normalizeFilePath(p));
|
|
205
|
+
const payload = {
|
|
206
|
+
v: BUS_FORMAT_START_VERSION,
|
|
207
|
+
source: "pi-lens",
|
|
208
|
+
cwd: normalizeFilePath(args.cwd),
|
|
209
|
+
paths,
|
|
210
|
+
fileCount: paths.length,
|
|
211
|
+
};
|
|
212
|
+
busEmit(BUS_FORMAT_START_EVENT, payload);
|
|
213
|
+
logBusEvent({
|
|
214
|
+
event: BUS_FORMAT_START_EVENT,
|
|
215
|
+
outcome: "emitted",
|
|
216
|
+
cwd: payload.cwd,
|
|
217
|
+
fileCount: payload.fileCount,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
logBusEvent({
|
|
222
|
+
event: BUS_FORMAT_START_EVENT,
|
|
223
|
+
outcome: "emit_failed",
|
|
224
|
+
cwd: normalizeFilePath(args.cwd),
|
|
225
|
+
error: String(err),
|
|
226
|
+
});
|
|
227
|
+
if (!hasLoggedStartFailure) {
|
|
228
|
+
hasLoggedStartFailure = true;
|
|
229
|
+
args.dbg?.(`format-events-publish: pilens:format:start emit failed (further failures suppressed): ${err}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Publish one `pilens:autofix:start` event when the `agent_end`
|
|
235
|
+
* actionable-warnings autofix batch begins (#684). Callers MUST only invoke
|
|
236
|
+
* this when `paths.length > 0` — i.e. after the cached report has been
|
|
237
|
+
* confirmed fresh (`checkActionableWarningsReportFresh`) AND has at least one
|
|
238
|
+
* autofix-eligible warning to act on, the same "genuine work" gate
|
|
239
|
+
* `publishFormatStart` applies for deferred formatting — see
|
|
240
|
+
* `clients/runtime-agent-end.ts`'s call site, placed immediately before
|
|
241
|
+
* `applyConservativeActionableWarningFixes` is invoked. Fire-and-forget:
|
|
242
|
+
* never throws, never awaited by `agent_end`.
|
|
243
|
+
*/
|
|
244
|
+
export function publishAutofixStart(args) {
|
|
245
|
+
if (args.paths.length === 0)
|
|
246
|
+
return;
|
|
247
|
+
if (!isBusPublishEnabled()) {
|
|
248
|
+
if (!hasLoggedAutofixStartDisabled) {
|
|
249
|
+
hasLoggedAutofixStartDisabled = true;
|
|
250
|
+
logBusEvent({
|
|
251
|
+
event: BUS_AUTOFIX_START_EVENT,
|
|
252
|
+
outcome: "skipped_disabled",
|
|
253
|
+
cwd: normalizeFilePath(args.cwd),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!busEmit) {
|
|
259
|
+
if (!hasLoggedAutofixStartUnwired) {
|
|
260
|
+
hasLoggedAutofixStartUnwired = true;
|
|
261
|
+
logBusEvent({
|
|
262
|
+
event: BUS_AUTOFIX_START_EVENT,
|
|
263
|
+
outcome: "skipped_unwired",
|
|
264
|
+
cwd: normalizeFilePath(args.cwd),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const paths = args.paths.map((p) => normalizeFilePath(p));
|
|
271
|
+
const payload = {
|
|
272
|
+
v: BUS_AUTOFIX_START_VERSION,
|
|
273
|
+
source: "pi-lens",
|
|
274
|
+
cwd: normalizeFilePath(args.cwd),
|
|
275
|
+
paths,
|
|
276
|
+
fileCount: paths.length,
|
|
277
|
+
eligibleCount: args.eligibleCount,
|
|
278
|
+
};
|
|
279
|
+
busEmit(BUS_AUTOFIX_START_EVENT, payload);
|
|
280
|
+
logBusEvent({
|
|
281
|
+
event: BUS_AUTOFIX_START_EVENT,
|
|
282
|
+
outcome: "emitted",
|
|
283
|
+
cwd: payload.cwd,
|
|
284
|
+
fileCount: payload.fileCount,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
logBusEvent({
|
|
289
|
+
event: BUS_AUTOFIX_START_EVENT,
|
|
290
|
+
outcome: "emit_failed",
|
|
291
|
+
cwd: normalizeFilePath(args.cwd),
|
|
292
|
+
error: String(err),
|
|
293
|
+
});
|
|
294
|
+
if (!hasLoggedAutofixStartFailure) {
|
|
295
|
+
hasLoggedAutofixStartFailure = true;
|
|
296
|
+
args.dbg?.(`format-events-publish: pilens:autofix:start emit failed (further failures suppressed): ${err}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|