pi-lens 3.8.69 → 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 +99 -0
- package/README.md +31 -13
- package/dist/clients/bootstrap.js +3 -1
- package/dist/clients/bus-events-logger.js +53 -0
- package/dist/clients/bus-publish.js +45 -2
- package/dist/clients/cache/rule-cache.js +9 -5
- package/dist/clients/cache-manager.js +19 -13
- package/dist/clients/code-quality-warnings.js +27 -0
- package/dist/clients/dead-code-client.js +5 -23
- package/dist/clients/diagnostic-dispositions.js +298 -0
- package/dist/clients/diagnostics-publish.js +38 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +144 -6
- package/dist/clients/dispatch/dispatcher.js +6 -1
- package/dist/clients/dispatch/facts/function-facts.js +83 -0
- package/dist/clients/dispatch/integration.js +19 -4
- package/dist/clients/dispatch/rules/async-noise.js +1 -1
- package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +1 -1
- package/dist/clients/dispatch/rules/cors-wildcard.js +1 -1
- package/dist/clients/dispatch/rules/error-obscuring.js +2 -1
- package/dist/clients/dispatch/rules/error-swallowing.js +2 -1
- package/dist/clients/dispatch/rules/framework-call-noise.js +67 -0
- package/dist/clients/dispatch/rules/high-complexity.js +12 -1
- package/dist/clients/dispatch/rules/high-fan-out.js +14 -3
- package/dist/clients/dispatch/rules/high-import-coupling.js +1 -1
- package/dist/clients/dispatch/rules/missing-error-propagation.js +1 -1
- package/dist/clients/dispatch/rules/no-commented-credentials.js +1 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +1 -1
- package/dist/clients/dispatch/rules/placeholder-comments.js +1 -1
- package/dist/clients/dispatch/rules/unsafe-boundary.js +1 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +75 -18
- package/dist/clients/dispatch/runners/lsp.js +23 -34
- 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/gitleaks-client.js +40 -5
- package/dist/clients/installer/index.js +123 -6
- package/dist/clients/instance-reaper.js +229 -2
- package/dist/clients/instance-registry.js +141 -2
- package/dist/clients/json-cache-read.js +65 -0
- package/dist/clients/knip-client.js +35 -38
- package/dist/clients/language-profile.js +11 -11
- package/dist/clients/lens-engine.js +12 -0
- package/dist/clients/lens-map.js +1079 -0
- package/dist/clients/log-cleanup.js +61 -19
- package/dist/clients/lsp/aggregation.js +69 -2
- package/dist/clients/lsp/cascade-tier.js +31 -19
- package/dist/clients/lsp/client.js +124 -14
- package/dist/clients/lsp/config.js +27 -1
- package/dist/clients/lsp/index.js +1028 -68
- package/dist/clients/lsp/server-strategies.js +17 -8
- package/dist/clients/lsp/server.js +80 -28
- package/dist/clients/lsp/tsserver-sync.js +145 -0
- package/dist/clients/lsp/workspace-diagnostics-cache.js +172 -0
- package/dist/clients/lsp-budget.js +148 -0
- package/dist/clients/mcp/session.js +9 -0
- package/dist/clients/middle-man-analysis.js +260 -0
- package/dist/clients/module-report.js +173 -11
- package/dist/clients/ndjson-logger.js +24 -0
- package/dist/clients/opengrep-client.js +217 -0
- package/dist/clients/opengrep-config.js +2 -10
- package/dist/clients/path-utils.js +66 -0
- package/dist/clients/pipeline.js +5 -1
- package/dist/clients/project-diagnostics/cache.js +5 -12
- package/dist/clients/project-diagnostics/extractors.js +21 -1
- package/dist/clients/project-diagnostics/fresh-fetch.js +275 -0
- package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +40 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +46 -0
- package/dist/clients/project-snapshot.js +3 -6
- package/dist/clients/quiet-window.js +57 -2
- package/dist/clients/read-expansion.js +2 -3
- package/dist/clients/read-guard-tool-lines.js +10 -0
- package/dist/clients/read-guard.js +1 -1
- package/dist/clients/resource-sampler.js +254 -0
- package/dist/clients/review-graph/builder.js +322 -42
- package/dist/clients/review-graph/import-resolvers.js +53 -19
- package/dist/clients/review-graph/symbol-id.js +57 -0
- package/dist/clients/runtime-agent-end.js +26 -0
- package/dist/clients/runtime-coordinator.js +9 -1
- package/dist/clients/runtime-session.js +93 -17
- package/dist/clients/runtime-tool-call.js +789 -0
- package/dist/clients/runtime-tool-result.js +15 -2
- package/dist/clients/runtime-turn.js +62 -13
- package/dist/clients/safe-spawn.js +48 -2
- package/dist/clients/session-state-store.js +6 -9
- package/dist/clients/sgconfig.js +2 -9
- package/dist/clients/source-filter.js +17 -25
- package/dist/clients/source-walker.js +66 -0
- package/dist/clients/startup-scan.js +105 -43
- package/dist/clients/symbol-containment.js +24 -0
- package/dist/clients/test-runner-client.js +544 -29
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/typos-config.js +2 -11
- package/dist/clients/widget-state.js +83 -1
- package/dist/clients/write-ordering-guard.js +58 -0
- package/dist/clients/zizmor-config.js +29 -10
- package/dist/index.js +25511 -19414
- package/dist/mcp/server.js +95 -19
- package/dist/tools/activate-tools.js +72 -0
- package/dist/tools/ast-dump.js +2 -8
- package/dist/tools/lens-diagnostic-mark.js +158 -0
- package/dist/tools/lens-diagnostics.js +327 -33
- package/dist/tools/lsp-diagnostics.js +525 -105
- package/dist/tools/module-report.js +18 -5
- package/docs/agent-tools.md +136 -0
- package/docs/ast-grep_rules_catalog.md +564 -0
- package/docs/astplayground.md +178 -0
- package/docs/audit1.md +177 -0
- package/docs/custom-rules.md +231 -0
- package/docs/dependencies.md +54 -0
- package/docs/dispositions.md +85 -0
- package/docs/env_variables.md +10 -0
- package/docs/environment-variables.md +83 -0
- package/docs/fable.md +140 -0
- package/docs/features.md +338 -0
- package/docs/globalconfig.md +79 -0
- package/docs/language-coverage.md +48 -0
- package/docs/lsp-capability-matrix.md +157 -0
- package/docs/lsp-latency-benchmark.md +169 -0
- package/docs/mcp.md +438 -0
- package/docs/module-report-read-symbol.md +258 -0
- package/docs/servercapabilities.md +118 -0
- package/docs/subagent-compat.md +281 -0
- package/docs/tools.md +16 -0
- package/docs/tools_improvement2.md +345 -0
- package/docs/tree-sitter_rules_catalog.md +296 -0
- package/docs/usage.md +129 -0
- package/docs/word-index.md +150 -0
- package/package.json +10 -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/array-callback-return-js.yml +1 -1
- package/rules/ast-grep-rules/rules/find-import-file-without-extension.yml +3 -1
- package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +1 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +13 -1
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +12 -0
- package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +1 -1
- package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +1 -1
- package/skills/pi-lens-ast-grep/SKILL.md +3 -1
- package/dist/clients/production-readiness.js +0 -493
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "untracked AND ignored" file-id computation (#679's `/lens-map`,
|
|
3
|
+
* reused by #694's review-graph ignore-gated node creation) — and, since
|
|
4
|
+
* #703, the sibling "tracked files" set that lets `getProjectIgnoreMatcher`
|
|
5
|
+
* (`file-utils.ts`) honor the OTHER half of the same git semantic.
|
|
6
|
+
*
|
|
7
|
+
* THE critical git semantic this exists to respect: a TRACKED file is never
|
|
8
|
+
* ignored, even when a `.gitignore` pattern matches it (pi-lens's own
|
|
9
|
+
* committed `clients/deps/*.js` vendored sources match the repo's `*.js`
|
|
10
|
+
* ignore pattern and MUST stay graph/map nodes) — which is why this asks git
|
|
11
|
+
* itself (`ls-files --others --ignored --exclude-standard` for the untracked
|
|
12
|
+
* side, `ls-files` alone for the tracked side) instead of running a pattern
|
|
13
|
+
* matcher over `.gitignore` (a matcher-only approach would wrongly drop
|
|
14
|
+
* tracked vendored files, e.g. #703's `clients/test-runner-client.ts`
|
|
15
|
+
* matching `.gitignore`'s `test-*.ts`).
|
|
16
|
+
*
|
|
17
|
+
* Degradation: when git is absent/fails/times out (not a git repo, bare
|
|
18
|
+
* checkout, etc.) both `collectUntrackedIgnoredIds` and `collectTrackedFiles`
|
|
19
|
+
* return `undefined` and every caller SKIPS/degrades — the graph/map shows
|
|
20
|
+
* what's known rather than guessing via a matcher that can't see tracked
|
|
21
|
+
* status, and `getProjectIgnoreMatcher` falls back to pattern-only behavior.
|
|
22
|
+
*
|
|
23
|
+
* Keying asymmetry (deliberate, not an oversight): `collectUntrackedIgnoredIds`
|
|
24
|
+
* keys its ids with `normalizeMapKey` (realpath-backed) because those ids are
|
|
25
|
+
* compared against review-graph/map NODE ids — externally-produced state that
|
|
26
|
+
* genuinely needs canonical on-disk casing/symlink resolution to match up.
|
|
27
|
+
* `collectTrackedFiles`'s ids, by contrast, are only ever compared against a
|
|
28
|
+
* `ProjectIgnoreMatcher`'s own `path.resolve`'d paths, produced by THIS
|
|
29
|
+
* process in the SAME walk — an ephemeral, self-consistent comparison per
|
|
30
|
+
* `normalizeEphemeralMapKey`'s contract (`path-utils.ts`), so it uses the
|
|
31
|
+
* cheap syntactic-only fold instead. Do not "fix" this asymmetry by unifying
|
|
32
|
+
* the two — they solve different problems (#703 review).
|
|
33
|
+
*/
|
|
34
|
+
import * as path from "node:path";
|
|
35
|
+
import { isExcludedDirName } from "./file-utils.js";
|
|
36
|
+
import { normalizeEphemeralMapKey, normalizeMapKey } from "./path-utils.js";
|
|
37
|
+
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
38
|
+
/**
|
|
39
|
+
* Parses `git ls-files --others --ignored --exclude-standard` output
|
|
40
|
+
* (repo-relative paths, one per line) into normalized map-key file ids.
|
|
41
|
+
* Exported as a pure function so the parse/normalize step is unit-testable
|
|
42
|
+
* without mocking the spawn.
|
|
43
|
+
*/
|
|
44
|
+
export function parseUntrackedIgnoredOutput(stdout, cwd) {
|
|
45
|
+
const ids = new Set();
|
|
46
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
47
|
+
const rel = line.trim();
|
|
48
|
+
if (!rel)
|
|
49
|
+
continue;
|
|
50
|
+
// Paths inside shared-excluded dirs (node_modules, dist, .git, …) can
|
|
51
|
+
// never be review-graph/map nodes — the graph walk itself routes
|
|
52
|
+
// exclusion through `isExcludedDirName` — so skip them BEFORE paying for
|
|
53
|
+
// `normalizeMapKey` (realpath-backed, per-call filesystem cost): on
|
|
54
|
+
// pi-lens itself this prunes a 66k-line ignored list (node_modules) down
|
|
55
|
+
// to ~1.6k paths that actually need normalizing.
|
|
56
|
+
const dirSegments = rel.split("/").slice(0, -1);
|
|
57
|
+
if (dirSegments.some((segment) => isExcludedDirName(segment)))
|
|
58
|
+
continue;
|
|
59
|
+
ids.add(normalizeMapKey(path.join(cwd, rel)));
|
|
60
|
+
}
|
|
61
|
+
return ids;
|
|
62
|
+
}
|
|
63
|
+
async function fetchUntrackedIgnoredIds(cwd) {
|
|
64
|
+
try {
|
|
65
|
+
const result = await safeSpawnAsync("git", ["ls-files", "--others", "--ignored", "--exclude-standard"], { cwd, timeout: 10_000, resourceLabel: "git-tracked-ignore" });
|
|
66
|
+
if (result.error || result.status !== 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
return parseUntrackedIgnoredOutput(result.stdout, cwd);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// #694: the review-graph build calls this on EVERY incremental/cascade
|
|
75
|
+
// rebuild (a hot, per-edit path), not just `/lens-map`'s one-shot generation —
|
|
76
|
+
// spawning `git ls-files` per edit would be a real per-keystroke cost. Memoize
|
|
77
|
+
// per cwd with a short time bound: cheap enough that a `.gitignore` edit or a
|
|
78
|
+
// newly-untracked file takes effect within a few seconds, but a burst of
|
|
79
|
+
// per-edit rebuilds within that window shares one spawn.
|
|
80
|
+
const CACHE_TTL_MS = 30_000;
|
|
81
|
+
const _cache = new Map();
|
|
82
|
+
/**
|
|
83
|
+
* The untracked-AND-ignored id set for `cwd`, memoized per-process with a
|
|
84
|
+
* {@link CACHE_TTL_MS} time bound so a hot rebuild loop (review-graph
|
|
85
|
+
* incremental/cascade builds) never spawns `git` per file/per edit — see the
|
|
86
|
+
* module doc for the tracked-file semantic and the degrade-to-undefined
|
|
87
|
+
* contract.
|
|
88
|
+
*/
|
|
89
|
+
export function collectUntrackedIgnoredIds(cwd) {
|
|
90
|
+
const key = normalizeMapKey(cwd);
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const cached = _cache.get(key);
|
|
93
|
+
if (cached && now - cached.fetchedAtMs < CACHE_TTL_MS)
|
|
94
|
+
return cached.promise;
|
|
95
|
+
const promise = fetchUntrackedIgnoredIds(cwd);
|
|
96
|
+
_cache.set(key, { promise, fetchedAtMs: now });
|
|
97
|
+
return promise;
|
|
98
|
+
}
|
|
99
|
+
/** Test hook: drop the memoized cache so a test's fake git state is re-read. */
|
|
100
|
+
export function _resetUntrackedIgnoredCacheForTests() {
|
|
101
|
+
_cache.clear();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parses `git ls-files` output (repo-relative paths, one per line, tracked
|
|
105
|
+
* files only — no `--others`) into normalized ids, keyed for comparison
|
|
106
|
+
* against a `ProjectIgnoreMatcher`'s own (non-realpath'd) `path.resolve`'d
|
|
107
|
+
* paths — NOT against {@link parseUntrackedIgnoredOutput}'s ids, which are
|
|
108
|
+
* realpath-keyed for a different consumer (review-graph/map node ids). The
|
|
109
|
+
* two sets are NOT directly comparable/mergeable despite the shared
|
|
110
|
+
* dir-exclusion-prune shape — see this file's module doc for the asymmetry.
|
|
111
|
+
*
|
|
112
|
+
* `cwd` is realpath'd exactly ONCE here (not per file) to reconcile the one
|
|
113
|
+
* divergence that matters in practice: on Windows, the matcher's
|
|
114
|
+
* `resolvedRoot` comes from `path.resolve` while this fetch's `cwd` may carry
|
|
115
|
+
* different on-disk casing (e.g. `c:\users` vs `C:\Users`) — a single
|
|
116
|
+
* realpath on the shared root, followed by a cheap syntactic fold
|
|
117
|
+
* (`normalizeEphemeralMapKey`) on every per-file join, reconciles that without
|
|
118
|
+
* paying `realpathSync` per tracked file (#703 perf follow-up — a ~2k-file
|
|
119
|
+
* repo was doing ~2k realpath calls in one synchronous burst per fetch).
|
|
120
|
+
* Accepted edge case: a symlinked or 8.3-short-name project root can still
|
|
121
|
+
* make the cheap per-file fold miss even after this one realpath — degrades
|
|
122
|
+
* to today's pattern-only behavior for that root, consistent with the
|
|
123
|
+
* fail-open contract elsewhere in this module.
|
|
124
|
+
*/
|
|
125
|
+
export function parseTrackedFilesOutput(stdout, cwd) {
|
|
126
|
+
const ids = new Set();
|
|
127
|
+
const base = normalizeMapKey(cwd);
|
|
128
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
129
|
+
const rel = line.trim();
|
|
130
|
+
if (!rel)
|
|
131
|
+
continue;
|
|
132
|
+
const dirSegments = rel.split("/").slice(0, -1);
|
|
133
|
+
if (dirSegments.some((segment) => isExcludedDirName(segment)))
|
|
134
|
+
continue;
|
|
135
|
+
ids.add(normalizeEphemeralMapKey(path.join(base, rel)));
|
|
136
|
+
}
|
|
137
|
+
return ids;
|
|
138
|
+
}
|
|
139
|
+
async function fetchTrackedFiles(cwd) {
|
|
140
|
+
try {
|
|
141
|
+
const result = await safeSpawnAsync("git", ["ls-files"], {
|
|
142
|
+
cwd,
|
|
143
|
+
timeout: 10_000,
|
|
144
|
+
resourceLabel: "git-tracked-ignore",
|
|
145
|
+
});
|
|
146
|
+
if (result.error || result.status !== 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
return parseTrackedFilesOutput(result.stdout, cwd);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const _trackedCache = new Map();
|
|
155
|
+
// Sync snapshot of the most recently RESOLVED tracked-files fetch per root,
|
|
156
|
+
// keyed the same way as `_trackedCache`. `getProjectIgnoreMatcher`'s
|
|
157
|
+
// `isIgnored` is sync-and-hot (#703 constraint) and can't await a promise, so
|
|
158
|
+
// this is the seam it reads from: `ensureTrackedIndex` (file-utils.ts) awaits
|
|
159
|
+
// `collectTrackedFiles` ONCE per walk, which populates this snapshot as a
|
|
160
|
+
// side effect; `isIgnored` then reads the snapshot synchronously for every
|
|
161
|
+
// file in that same walk. A caller that never primes sees an absent snapshot
|
|
162
|
+
// entry and degrades to pattern-only behavior (fail-open, by design).
|
|
163
|
+
const _trackedSnapshot = new Map();
|
|
164
|
+
/**
|
|
165
|
+
* The tracked-files id set for `cwd` (i.e. `git ls-files`, no `--others`),
|
|
166
|
+
* memoized per-process with the same {@link CACHE_TTL_MS} time bound as
|
|
167
|
+
* {@link collectUntrackedIgnoredIds} — see that function's doc for the hot
|
|
168
|
+
* per-edit-rebuild-loop rationale. Also updates the synchronous snapshot
|
|
169
|
+
* `getTrackedFilesSnapshot` reads, so sync hot-path callers can consult a
|
|
170
|
+
* cheap in-memory Set instead of awaiting this promise per file.
|
|
171
|
+
*/
|
|
172
|
+
export function collectTrackedFiles(cwd) {
|
|
173
|
+
// #703 perf follow-up: `normalizeEphemeralMapKey`, not `normalizeMapKey` —
|
|
174
|
+
// this map's only readers/writers are `ensureTrackedIndex`/`isIgnored`
|
|
175
|
+
// (file-utils.ts), both of which derive `cwd` from the SAME matcher's
|
|
176
|
+
// `resolvedRoot` (`path.resolve`, not realpath) within one process, so the
|
|
177
|
+
// cheap syntactic fold is sufficient and avoids a realpath per call — see
|
|
178
|
+
// this file's module doc for why this diverges from `collectUntrackedIgnoredIds`.
|
|
179
|
+
const key = normalizeEphemeralMapKey(cwd);
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
const cached = _trackedCache.get(key);
|
|
182
|
+
if (cached && now - cached.fetchedAtMs < CACHE_TTL_MS)
|
|
183
|
+
return cached.promise;
|
|
184
|
+
const promise = fetchTrackedFiles(cwd).then((result) => {
|
|
185
|
+
_trackedSnapshot.set(key, result);
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
_trackedCache.set(key, { promise, fetchedAtMs: now });
|
|
189
|
+
return promise;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Synchronous read of the most recent {@link collectTrackedFiles} result for
|
|
193
|
+
* `cwd`. Returns `undefined` when nothing has primed this root yet (never
|
|
194
|
+
* called `collectTrackedFiles`), when git degraded (no repo/spawn failure),
|
|
195
|
+
* OR when a fetch is still in flight — all three collapse to the same
|
|
196
|
+
* fail-open "tracked status unknown, use pattern-only behavior" contract.
|
|
197
|
+
* Called on every `isIgnored` check that reaches the tracked-rescue branch
|
|
198
|
+
* (file-utils.ts's `isTrackedAndRescued`), so this key MUST stay the cheap
|
|
199
|
+
* `normalizeEphemeralMapKey` fold, not a realpath.
|
|
200
|
+
*/
|
|
201
|
+
export function getTrackedFilesSnapshot(cwd) {
|
|
202
|
+
return _trackedSnapshot.get(normalizeEphemeralMapKey(cwd));
|
|
203
|
+
}
|
|
204
|
+
/** Test hook: drop the memoized tracked-files cache/snapshot. */
|
|
205
|
+
export function _resetTrackedFilesCacheForTests() {
|
|
206
|
+
_trackedCache.clear();
|
|
207
|
+
_trackedSnapshot.clear();
|
|
208
|
+
}
|
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* - `gitleaks` reference in `package.json` deps, OR
|
|
18
18
|
* - a git pre-commit hook (.husky/, .git/hooks/) referencing gitleaks
|
|
19
19
|
*
|
|
20
|
+
* `lens_diagnostics mode=full`'s fresh-fetch path (`clients/project-diagnostics/
|
|
21
|
+
* fresh-fetch.ts`) uses a looser "smart-default" gate instead — any tracked
|
|
22
|
+
* git repo, via `hasGitRepo` — since that's an explicitly-requested
|
|
23
|
+
* comprehensive review where gitleaks's low cost and advisory-only findings
|
|
24
|
+
* make the stricter default needlessly conservative. session_start and
|
|
25
|
+
* per-edit dispatch keep the strict gate above unchanged.
|
|
26
|
+
*
|
|
20
27
|
* If the gate trips, the runner auto-installs gitleaks from GitHub releases
|
|
21
28
|
* (installer entry registered in clients/installer/index.ts) and runs
|
|
22
29
|
* `gitleaks detect --no-git --report-format json` against the analysis root.
|
|
@@ -95,6 +102,26 @@ export function hasGitleaksSignal(cwd) {
|
|
|
95
102
|
}
|
|
96
103
|
return false;
|
|
97
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* "Smart-default" tier from #130's own considered-but-unshipped options:
|
|
107
|
+
* fire whenever the project is a tracked git repo, not only when an explicit
|
|
108
|
+
* gitleaks signal is present. gitleaks's own scan target is "a git repo's
|
|
109
|
+
* history/tree" — nearly every project qualifies, so this is meaningfully
|
|
110
|
+
* looser than {@link hasGitleaksSignal}. Used ONLY by `mode=full`'s
|
|
111
|
+
* fresh-fetch path (an explicitly-requested comprehensive review, where
|
|
112
|
+
* gitleaks's low cost — ~10MB binary, no external DB pull — and advisory-only
|
|
113
|
+
* findings make the stricter opt-in gate needlessly conservative); session_start
|
|
114
|
+
* and per-edit dispatch keep the strict {@link hasGitleaksSignal} gate
|
|
115
|
+
* unchanged, so day-to-day noise/cost stays exactly as conservative as before.
|
|
116
|
+
*/
|
|
117
|
+
export function hasGitRepo(cwd) {
|
|
118
|
+
try {
|
|
119
|
+
return fs.existsSync(path.join(cwd, ".git"));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
98
125
|
// --- Client ---
|
|
99
126
|
export class GitleaksClient extends SecurityScanClient {
|
|
100
127
|
constructor(verbose = false) {
|
|
@@ -107,6 +134,10 @@ export class GitleaksClient extends SecurityScanClient {
|
|
|
107
134
|
static hasGitleaksSignal(cwd) {
|
|
108
135
|
return hasGitleaksSignal(cwd);
|
|
109
136
|
}
|
|
137
|
+
/** Smart-default tier (#130) — see {@link hasGitRepo}'s doc comment. */
|
|
138
|
+
static hasGitRepo(cwd) {
|
|
139
|
+
return hasGitRepo(cwd);
|
|
140
|
+
}
|
|
110
141
|
/**
|
|
111
142
|
* Auto-install via the GitHub-release path (registered in
|
|
112
143
|
* `clients/installer/index.ts`) when gitleaks isn't already on PATH.
|
|
@@ -118,18 +149,22 @@ export class GitleaksClient extends SecurityScanClient {
|
|
|
118
149
|
/**
|
|
119
150
|
* Scan a directory tree for secrets.
|
|
120
151
|
*
|
|
121
|
-
* Skips early when the directory shows no gitleaks opt-in signal
|
|
122
|
-
*
|
|
123
|
-
*
|
|
152
|
+
* Skips early when the directory shows no gitleaks opt-in signal — unless
|
|
153
|
+
* `requireSignal: false` (the `mode=full` fresh-fetch path uses this to
|
|
154
|
+
* apply the looser #130 "smart-default" gate, {@link hasGitRepo}, instead;
|
|
155
|
+
* session_start and per-edit dispatch never pass this, so their behavior
|
|
156
|
+
* is unchanged). When gitleaks is unavailable, returns an empty result
|
|
157
|
+
* with an explanatory summary rather than failing the session_start task.
|
|
124
158
|
*
|
|
125
159
|
* Re-entrancy safe: concurrent calls against the same root share a
|
|
126
160
|
* single gitleaks process (mirrors `KnipClient` / `JscpdClient` /
|
|
127
161
|
* `GovulncheckClient`).
|
|
128
162
|
*/
|
|
129
|
-
async scan(cwd) {
|
|
163
|
+
async scan(cwd, options) {
|
|
130
164
|
const targetDir = path.resolve(cwd);
|
|
131
165
|
const scannedAt = new Date().toISOString();
|
|
132
|
-
|
|
166
|
+
const requireSignal = options?.requireSignal ?? true;
|
|
167
|
+
if (requireSignal && !GitleaksClient.hasGitleaksSignal(targetDir)) {
|
|
133
168
|
return {
|
|
134
169
|
...EMPTY_RESULT,
|
|
135
170
|
success: true,
|