pi-lens 3.8.66 → 3.8.68
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 +44 -0
- package/README.md +2 -1
- package/dist/clients/actionable-warnings-logger.js +11 -36
- package/dist/clients/ast-grep-tool-logger.js +11 -36
- package/dist/clients/cascade-logger.js +7 -12
- package/dist/clients/dead-code-logger.js +11 -22
- package/dist/clients/deps/ast-grep-napi.js +20 -1
- package/dist/clients/deps/pi-tui.js +4 -1
- package/dist/clients/deps/typebox.js +5 -2
- package/dist/clients/deps/web-tree-sitter.js +22 -1
- package/dist/clients/diagnostic-logger.js +7 -31
- package/dist/clients/dispatch/auxiliary-lsp.js +38 -0
- package/dist/clients/dispatch/dispatcher.js +1 -36
- package/dist/clients/dispatch/inline-suppressions.js +61 -0
- package/dist/clients/dispatch/integration.js +75 -23
- package/dist/clients/dispatch/runners/lsp.js +17 -5
- package/dist/clients/dispatch/runners/tree-sitter.js +7 -2
- package/dist/clients/instance-reaper.js +418 -0
- package/dist/clients/instance-registry.js +238 -0
- package/dist/clients/latency-logger.js +10 -15
- package/dist/clients/lsp/client.js +76 -2
- package/dist/clients/lsp/config.js +61 -3
- package/dist/clients/lsp/index.js +43 -3
- package/dist/clients/lsp/launch.js +2 -0
- package/dist/clients/lsp/server-strategies.js +61 -0
- package/dist/clients/lsp/server.js +8 -1
- package/dist/clients/ndjson-logger.js +150 -0
- package/dist/clients/pipeline.js +25 -41
- package/dist/clients/project-diagnostics/scanner.js +8 -2
- package/dist/clients/read-guard-logger.js +11 -36
- package/dist/clients/review-graph/builder.js +336 -17
- package/dist/clients/review-graph/git-identity.js +150 -0
- package/dist/clients/review-graph/service.js +3 -3
- package/dist/clients/runtime-coordinator.js +83 -0
- package/dist/clients/runtime-session.js +89 -8
- package/dist/clients/runtime-tool-result.js +10 -2
- package/dist/clients/runtime-turn.js +30 -0
- package/dist/clients/session-lifecycle.js +252 -0
- package/dist/clients/sgconfig.js +31 -1
- package/dist/clients/slow-fs.js +137 -0
- package/dist/clients/source-filter.js +18 -5
- package/dist/clients/subagent-mode.js +53 -0
- package/dist/clients/tree-sitter-logger.js +7 -12
- package/dist/clients/tree-sitter-query-loader.js +1 -0
- package/dist/index.js +60587 -1636
- package/dist/tools/lens-diagnostics.js +203 -12
- package/package.json +3 -2
- package/rules/ast-grep-rules/rule-tests/no-init-return-test.yml +15 -0
- package/rules/ast-grep-rules/rules/no-init-return.yml +11 -5
- package/rules/tree-sitter-queries/python/python-assert-production.yml +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,50 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
+
## [3.8.68] - 2026-07-10
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Subagent light mode** (#449) — the nicobailon/pi-subagents extension spawns each subagent as a child `pi` CLI process and sets `PI_SUBAGENT_CHILD=1` unconditionally in every child's environment, so a fan-out of N subagents in the same cwd previously paid N full LSP pre-warms plus N sets of heavyweight startup scans — mostly wasted on short-lived task agents. `clients/subagent-mode.ts` adds `isSubagentSession()`, detected once at session start; when engaged, both the LSP pre-warm (explicit `warmFiles` and the dominant-language auto-warm) and the knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy startup scans are skipped, extending the same `skipHeavyweightScans` gate #462 introduced for slow filesystems. Per-edit LSP dispatch and the in-process scans (todo/call-graph/codebase-model/ast-grep-exports/word-index) are untouched, so a subagent that actually edits code still gets diagnostics and symbol search. Escape hatch: `PI_LENS_SUBAGENT_FULL=1` forces full behavior even inside a detected subagent session. Logged to the latency log as a `subagent_light_mode` phase with the subagent's `runId`/`agentName` (from `PI_SUBAGENT_RUN_ID`/`PI_SUBAGENT_CHILD_AGENT`) when present.
|
|
18
|
+
- **Cross-process instance registry** (#449 slice 1) — a tiny machine-global registry (`~/.pi-lens/instances.json`, `clients/instance-registry.ts`) now records every live pi-lens process: pid, project root, live LSP child servers (pid/serverId/command/spawn marker), RSS, and a heartbeat. Registered at `session_start`, updated opportunistically at `turn_end` (piggybacked on the existing per-turn touchpoint, no new timer), and deregistered synchronously at `session_shutdown`. Pure observability substrate for now (zero dispatch/behavior change) — the groundwork later slices (cross-process LSP budget, same-root warm attach) will build on. Reads are corruption-safe (garbage/missing file ⇒ empty, never throws); writes are atomic tmp+rename. `PI_LENS_INSTANCE_REGISTRY=0` disables it entirely.
|
|
19
|
+
- **Slow-filesystem mode** (#462) — WSL 9p mounts (`/mnt/c/...`) measure ~1.3ms/`stat` vs ~17µs native (75x), so an unbounded synchronous tree walk (e.g. a 5,000-file project) could cost ~6.5s of stat time alone and freeze the TUI. `clients/slow-fs.ts` adds a cheap session-start probe (median of up to 15 `fs.statSync` calls under the project root) that classifies the workspace by measurement, not path shape, so it also catches drvfs/NFS/SMB rather than 9p-only. In slow-FS mode the sync `collectSourceFiles` walker clamps to a reduced 500-file cap (the async twin is unaffected), and the knip/jscpd/madge/dead-code/govulncheck/gitleaks/trivy background scans are skipped at session start with a visible notice instead of silently returning stale/empty results. Escape hatches: `PI_LENS_ALLOW_SLOW_FS_SCAN=1` disables slow-FS mode entirely; `PI_LENS_FORCE_SLOW_FS=1` forces it on for testing or when the probe under-fires; `PI_LENS_SLOW_FS_THRESHOLD_US` overrides the 500µs default. The verdict is logged to the latency log as a `slow_fs_probe` phase for dogfooding.
|
|
20
|
+
- **Subagent-extension compat smoke** (#476) — pi-lens's subagent-compatibility features (#473/#474/#475) were built on reverse-engineered facts about the nicobailon/pi-subagents and `@tintinweb/pi-subagents` extensions plus the pi SDK itself. A new nightly `.github/workflows/compat-smoke.yml` makes that compatibility empirical: Layer A (`scripts/compat-contracts.mjs`) npm-installs the real third-party packages and mechanically re-verifies six pinned contracts with resilient pattern matchers (`scripts/lib/compat-contracts.mjs`) against the installed source — no `pi` process, no LLM. Layer B (`scripts/compat-smoke-behavioral.mjs`) installs the packed pi-lens tarball into a real `pi` (the same mechanism `install-smoke.yml`'s `pi-load` job uses) and drives `pi --mode rpc` to assert, through pi-lens's own latency log, that subagent light mode engages under `PI_SUBAGENT_CHILD=1`, that `PI_LENS_SUBAGENT_FULL=1` overrides it off, and that zero LSP-server processes survive a graceful pi exit (the #472 orphan class). Both layers run `continue-on-error`; a failure opens/refreshes a single tracking issue rather than reddening the nightly. `docs/subagent-compat.md` records the exact pinned contracts (file + version last verified) and the three env levers (`PI_LENS_SUBAGENT_FULL`, `PI_LENS_CONCURRENT_SESSION_GUARD`, `PI_LENS_INSTANCE_REGISTRY`).
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Orphaned LSP server processes no longer survive abnormal session exit** (#472) — the #234 teardown constraint (no child spawn during `session_shutdown`, else libuv aborts) only covers CLEAN shutdown; a crashed/hard-killed/OOM'd session never runs teardown, and Windows does not kill children when a parent dies, so the whole LSP fleet could leak (7 orphaned ast-grep pairs found in the wild, up to 13 days old, ~700MB). `killProcessTree`'s `processExiting` branch only ever killed the DIRECT child (for shell/`.cmd`-wrapped servers that's the wrapper, not the real server) — its comment claiming Windows grandchildren "are reaped by the OS as the host exits" was false and is now corrected in place (behavior unchanged: still direct-child-only, per #234). The real fix is the #449 instance registry's orphan reaper: every LSP child (core and auxiliary, uniformly, at the shared `clients/lsp/launch.ts`/`client.ts` spawn/kill seam — no per-server special casing) is now recorded with its pid, resolved command, and — when the launch args carry a temp-config-style value (e.g. ast-grep's `--config <tmp sgconfig path>`) — a per-spawn-unique marker for command-line re-identification when the pid chain is broken (the synthesized baseline sgconfig now embeds the owning pid in its filename — `baseline-<pid>.sgconfig.yml`, with age-based cleanup of stale siblings — so the marker really is unique per instance; the previous shared `baseline.sgconfig.yml` would have made the marker fallback match every live ast-grep on the machine). `clients/instance-reaper.ts` sweeps at every `session_start`: a pure `decideOrphanReaping` function (conservative liveness — ESRCH-only counts as dead, EPERM/ambiguous never does; markers claimed by any live instance are never search-killed; pid kills are identity-verified against a batched command-line lookup so a recycled pid is never killed blind) decides what to kill, and an impure `sweepOrphans` executes it (`taskkill /F /T` on Windows, process-group kill on POSIX) plus a marker-based `Get-CimInstance` command-line search fallback for the broken-pid-chain case. Also resolves ast-grep's platform-native exe directly (`resolveAstGrepNativeExe`, `@ast-grep/cli-<platform>-<arch>` packages) ahead of the node-bin-wrapper candidate — one less orphanable process layer.
|
|
27
|
+
- **Concurrent in-process subagent binds no longer tear down the parent's LSP fleet/runtime state (#473)** — extensions that build a fresh `AgentSession` and call `session.bindExtensions()` *inside the same Node process* as the parent pi session (tintinweb/pi-subagents-style) reuse pi's process-global extension-loader cache, so the subagent's `session_start` re-invoked pi-lens's SAME module-scope singletons the parent was still using — `resetLSPService({fast:true})` killed every live LSP client and `runtime.resetForSession()` bumped the session generation, silently orphaning the parent's in-flight continuations (parked cascades, diagnostics waits) mid-turn, with no visible error. New `clients/session-lifecycle.ts` classifies each `session_start`/`session_shutdown` as `primary` / `sequential-replacement` / `concurrent-secondary` by probing whether the previously-registered ctx is still active (an SDK-wrapped accessor throws pi's own stale-ctx error only for real sequential replacement — `newSession`/`fork`/`switchSession`/`reload` — never for a concurrently-live sibling). A `concurrent-secondary` session_start now skips `handleSessionStart` and the runtime-identity update entirely and rides the already-initialized shared infra; its later shutdown skips the destructive teardown too. Classification is fail-safe: any inconclusive signal (probe failure, no prior session) falls back to today's full-reset behavior, and `PI_LENS_CONCURRENT_SESSION_GUARD=0` disables the guard outright. Zero behavior change for the common single-session process.
|
|
28
|
+
- **Review-graph snapshot persist is now atomic (tmp + rename)** — the debounced cache write went straight to `review-graph.json` with a plain `fs.writeFile`, so a concurrent reader (another process's blind load, or the tier-2 disk load under CI's parallel test runners) could observe a created-but-partially-written file, fail the JSON parse, and silently fall open to a full whole-repo rebuild. The write now lands in a `.tmp-<pid>` sibling and is renamed into place (atomic on POSIX and Windows), so a snapshot either doesn't exist yet or is complete; the process-exit flush uses the same pattern. This was the flaky `expected 'cached' to be 'full'` CI failure in the #300 git-stamp tests.
|
|
29
|
+
- **`servercapabilities.md` merge guard survives schema changes and merges bullet sections (#469)** — the #390 nightly merge guard for `scripts/server-capabilities.mjs` required the prior and freshly-generated table headers to be byte-identical before merging, so adding the `ws-pull` column silently disabled the guard and last night's ubuntu-only run dropped the rust and php rows (and their capability-key / executeCommand bullets) entirely. Fixed by reshaping prior rows onto the new header **by column name** (`reshapeRowsByName` in `scripts/lib/md-matrix.mjs` — columns the prior doc lacked are filled with the `·` placeholder, columns dropped from the new schema are simply not carried) and by merging the two bulleted sections ("Raw advertised capability keys", "Advertised executeCommand allowlists") for preserved servers, which the original guard never touched at all (`parseBulletSection`/`mergeBulletSection`). The whole merge is now a pure, unit-tested function (`mergeServerCapabilitiesDoc`) that never spawns an LSP server and fails open (writes the fresh doc, logs to stderr) if either doc's table is unparseable.
|
|
30
|
+
- **pi-lens loads under pi's Bun-compiled binary again** (#335) — pi ships as a `bun build --compile` single-file executable and loads extensions inside that embedded runtime, whose module resolver does not traverse the extension's on-disk `node_modules` for a bare specifier. So a static `import { minimatch } from "minimatch"` in `file-utils.js` (and every other third-party bare import) failed with `Cannot find package`, dropping the jscpd/todo/complexity analyzers into degraded mode. `dist/index.js` is now bundled into one self-contained file (`scripts/bundle-dist.mjs`, wired into `build:dist`) that inlines the pure-JS deps (minimatch, js-yaml, vscode-jsonrpc and transitives), so nothing is imported by bare specifier at load time. Host-provided packages (typebox, `@earendil-works/pi-coding-agent`, `@earendil-works/pi-tui`) and the native/wasm packages (`@ast-grep/napi`, web-tree-sitter) stay external; the two lazy native/wasm accessors resolve to an absolute path via `createRequire` before dynamic-importing (a bare specifier fails under the compiled host, an absolute path does not), with the bare specifier kept as a fallback for other runtimes. esbuild is run through `node <npm-cli> exec` (shell-free, not the `npx.cmd` shim), installing into npm's cache rather than the project tree, so the build adds no dependency and works on a from-source `--omit=dev` install; a `createRequire` banner is prepended so the bundled CJS deps load under pure-ESM Node. The reporter's Windows junction workaround does not port to the Linux compiled host, so bundling is the cross-platform fix.
|
|
31
|
+
|
|
32
|
+
## [3.8.67] - 2026-07-09
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- nightly clean-signal probe: probe-clean-signal.mjs generalized per-server (Tier 2 publishes-empty vs Tier 3 silent-on-clean among push LSPs) and wired into the tool-smoke nightly so the capability matrix's clean-behavior column self-populates (#460). The probe is phase-aware (dirty touch proves liveness; clean transitions are the discriminator) and classifies 4-way: `publishes-versioned` (tier 2 — affirmative + currency-proven, ast-grep), `publishes-unversioned` (tier 2\* — a version-less publish still early-returns the wait at runtime since the client accepts it as fresh, but currency is only temporally correlated: a staleness-risk note, not a latency cost — opengrep, yaml), `silent` (tier 3 — alive on dirty, silent on clean: the budget-wait case and #458's learned-deadline target set — typescript on a clean file), and `unknown` (no publish — conservatively unclassified). Clean fixtures (`typescript-clean`) are authoritative for a lang's row: typescript measurably re-publishes while dirty but goes silent once clean, so the dirty-fixture 2\* is overridden by the clean fixture's tier 3.
|
|
37
|
+
- nightly LSP-docs commit-back (#390): the tool-smoke nightly now also runs `server-capabilities.mjs` (regenerating `docs/servercapabilities.md` incl. the ws-pull column) and opens/updates a single auto-PR (`bot/lsp-docs-refresh`) with the regenerated `lsp-capability-matrix.md` + `servercapabilities.md` — previously these were generated in CI then discarded. All three generators now **merge** into their docs (keyed by lang/server), so a server the ubuntu host can't spawn keeps its prior dev-box row and the nightly can never regress a richer run. The phase-aware probe refined opengrep's hand-noted Tier 2 to 2\* (re-publishes on clean scans, so the wait early-returns — but version-lessly, so currency is unproven).
|
|
38
|
+
- **`serverOverrides` — per-server `initializationOptions` in project config** (#434) — `.pi-lens/lsp.json` (or `.pi-lens.json` / `pi-lsp.json`) accepts a `serverOverrides` key mapping a built-in server `id` (`"rust"`, `"nix"`, …) to an `initializationOptions` object that is deep-merged onto the server's built-in defaults at spawn time (user wins on conflicts; arrays replaced, not merged). Brings pi-lens diagnostics in line with a user's editor LSP setup (e.g. rust-analyzer `check.command: "clippy"`, nixd options expressions) without forking. Contributed by @vkarasen.
|
|
39
|
+
- `lens_diagnostics` (and MCP `pilens_diagnostics`) accept `paths` to scope any mode to an explicit file/directory list — enables wrappers like "check exactly the git-staged files" (#461).
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- perf: cascade diagnostics now run concurrently after each edit instead of blocking the write pipeline (~26% median per-edit latency reduction); settled at turn_end with a bounded wait (#450)
|
|
44
|
+
- perf: write-path micro batch — ESLint autofix runs a single `--fix` spawn (was dry-run + fix, double cold-start), LSP quick-fix lookups for blocking diagnostics run in parallel, and the lsp runner reuses its already-read file content for nosemgrep suppression (#453)
|
|
45
|
+
- perf/refactor: the eight NDJSON debug loggers (latency, cascade, read-guard, tree-sitter, dead-code, actionable-warnings, ast-grep-tool, diagnostic) now share one buffered async writer (clients/ndjson-logger.ts) — no more synchronous appendFileSync on the per-edit hot path; best-effort sync flush at process exit (#454)
|
|
46
|
+
- perf: the review-graph freshness check now uses RuntimeCoordinator sequence state to skip the per-build O(project) walk+stat sweep when only pi-observed edits occurred (seq fast path; periodic full re-verify every 20 builds/5 min catches external changes; PI_LENS_GRAPH_SEQ_FASTPATH=0 disables) (#451)
|
|
47
|
+
- perf: skip the reverse-dependency index rebuild (O(graph edges)) and its project-snapshot disk write on cascade runs where the review graph didn't actually change — the index is a pure function of the graph, so a cache-hit graph build (or a seq-fastpath build that found nothing graph-relevant to re-parse) now reuses the last-built index instead of redoing both. Freshness keys on a new `ReviewGraph.buildGeneration` stamp that travels with the returned graph instance (`mode` alone can't distinguish a true seq-fastpath no-op from one that re-parsed files, and the global build-info slot can be clobbered by overlapping deferred cascades); per-workspace cache, `PI_LENS_REVERSE_DEPS_REUSE=0` disables (#459)
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **`no-init-return` no longer flags factory functions** (#439) — the ast-grep rule matched `return` inside any `function_definition` whose *body text* regex-contained `def __init__`, so a factory that returns a class with an `__init__` (and its sibling methods' returns) tripped it. It now matches a `function_definition` whose **name field** is `__init__` (`has: field: name`), so only real `__init__` returns are flagged. Regression fixtures added (factory + sibling-method cases).
|
|
52
|
+
- **`python-assert-production` no longer fires in test files** (#440) — `assert` is the idiomatic test assertion, so flagging every `assert` in `tests/**` was pure noise that trained users to ignore the rule. Tree-sitter rules gain an opt-in `skip_test_files` field (the runner otherwise runs on test files, since structural issues matter there); `python-assert-production` sets it, so production `assert` (the `-O` strip risk) is still flagged while test asserts are skipped. Exercised through the real runner (prod fires, `tests/` skips).
|
|
53
|
+
- **The opengrep/Semgrep runner now honors `# nosemgrep` suppression** (#441) — the canonical Semgrep inline suppression (`# nosemgrep` and `# nosemgrep: <rule-id>[,<rule-id>]`, also `//`) was ignored, leaving only `.pi-lens.json` path globs or code restructuring as escapes. The auxiliary-LSP runner now drops opengrep findings suppressed by a `nosemgrep` comment on the finding's own line (inline) or a standalone comment on the line above — matching Semgrep's placement semantics (an inline comment doesn't leak to the next line).
|
|
54
|
+
- **`lens_diagnostics mode=full` now honors inline `# pi-lens-ignore` comments** (#442) — inline suppression (`// pi-lens-ignore: rule` / `# pi-lens-ignore: rule`) was applied only in the per-edit dispatch path (`mode=all`), so a site cleanly suppressed there reappeared as **blocking** in the project-wide `mode=full` sweep — making `mode=full` unusable as a "clean" gate for any project with a legitimate suppression. The suppression filter is now shared (`clients/dispatch/inline-suppressions.ts`) and applied to the merged `mode=full` summaries too: each flagged file is read and its inline ignores honored (fail-safe — a read error never hides a finding), and counts are re-summarized so a fully-suppressed file reports clean. Rule matching also normalizes `ast-grep:<id>` / `<id>-js` forms, so a bare `pi-lens-ignore: <id>` suppresses the finding in both modes. (The reporter's secondary ask — per-rule enable/disable in `.pi-lens.json` — is tracked separately as a follow-up.)
|
|
55
|
+
- **The review-graph snapshot is now stamped with git HEAD + worktree root, and the read-substitute path drops it on mismatch** (#300) — `git worktree remove` followed by `add` at the same path for a different branch reuses the cwd-derived data-dir slug, so a `module_report`/blast-radius read fired before the first rebuild could return the previous branch's symbols/edges. The persisted snapshot now carries an optional git stamp (HEAD commit + worktree top-level path), resolved purely by reading `.git`/`HEAD`/`refs` files — no `git` subprocess, since the persist path includes the synchronous flush-on-exit handler and spawning at teardown crashes libuv on Windows (#234). The blind read path (`getCachedReviewGraph`, which trusts disk with no other verification) drops a stamped snapshot that mismatches the current repo; the build path deliberately keeps loading it unverified, because its signature/content-hash confirm (#202) already proves file-level freshness — so a plain `git commit` (HEAD moves, files unchanged) still cold-starts as a cheap "cached" reuse, never a full rebuild. An absent stamp (older snapshot, or a non-git cwd) behaves exactly as before. Separately, a cwd that isn't the git worktree top-level is now logged once per process (observability only, no hard-fail) — the review graph's cross-worktree isolation has always rested on that assumption, and it was previously invisible.
|
|
56
|
+
|
|
13
57
|
## [3.8.66] - 2026-07-07
|
|
14
58
|
|
|
15
59
|
### Added
|
package/README.md
CHANGED
|
@@ -57,7 +57,6 @@ pi-lens is released under the [MIT License](LICENSE).
|
|
|
57
57
|
## Contributors
|
|
58
58
|
|
|
59
59
|
Thanks goes to these wonderful people:
|
|
60
|
-
|
|
61
60
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
62
61
|
<!-- prettier-ignore-start -->
|
|
63
62
|
<!-- markdownlint-disable -->
|
|
@@ -150,6 +149,8 @@ Thanks goes to these wonderful people:
|
|
|
150
149
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cmptr"><img src="https://avatars.githubusercontent.com/u/32746889?v=4" width="100px;" alt=""/><br /><sub><b>Aaron Bell</b></sub></a><br /><a href="#code-cmptr" title="Code">💻</a> <a href="#bug-cmptr" title="Bug reports">🐛</a></td>
|
|
151
150
|
<td align="center" valign="top" width="14.28%"><a href="http://jasonrimmer.com/"><img src="https://avatars.githubusercontent.com/u/629?v=4" width="100px;" alt=""/><br /><sub><b>Jason Rimmer</b></sub></a><br /><a href="#bug-jrimmer" title="Bug reports">🐛</a></td>
|
|
152
151
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moj02090"><img src="https://avatars.githubusercontent.com/u/57255166?v=4" width="100px;" alt=""/><br /><sub><b>moj02090</b></sub></a><br /><a href="#code-moj02090" title="Code">💻</a></td>
|
|
152
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vkarasen"><img src="https://avatars.githubusercontent.com/u/7932536?v=4" width="100px;" alt=""/><br /><sub><b>Vitali Karasenko</b></sub></a><br /><a href="#code-vkarasen" title="Code">💻</a></td>
|
|
153
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mehalter"><img src="https://avatars.githubusercontent.com/u/1591837?v=4" width="100px;" alt=""/><br /><sub><b>Micah Halter</b></sub></a><br /><a href="#code-mehalter" title="Code">💻</a></td>
|
|
153
154
|
</tr>
|
|
154
155
|
</tbody>
|
|
155
156
|
</table>
|
|
@@ -1,51 +1,26 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
1
|
import * as path from "node:path";
|
|
3
2
|
import { isTestMode } from "./env-utils.js";
|
|
4
3
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
4
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
5
5
|
const AW_LOG_DIR = getGlobalPiLensDir();
|
|
6
6
|
const AW_LOG_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log");
|
|
7
7
|
const AW_LOG_BACKUP_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log.1");
|
|
8
8
|
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) || 1048576);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
catch (err) {
|
|
15
|
-
void err;
|
|
16
|
-
}
|
|
17
|
-
function rotateIfNeeded() {
|
|
18
|
-
try {
|
|
19
|
-
if (!fs.existsSync(AW_LOG_FILE))
|
|
20
|
-
return;
|
|
21
|
-
const size = fs.statSync(AW_LOG_FILE).size;
|
|
22
|
-
if (size < MAX_LOG_BYTES)
|
|
23
|
-
return;
|
|
24
|
-
try {
|
|
25
|
-
fs.rmSync(AW_LOG_BACKUP_FILE, { force: true });
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
void err;
|
|
29
|
-
}
|
|
30
|
-
fs.renameSync(AW_LOG_FILE, AW_LOG_BACKUP_FILE);
|
|
31
|
-
}
|
|
32
|
-
catch (err) {
|
|
33
|
-
void err;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
9
|
+
const writer = createNdjsonLogger({
|
|
10
|
+
filePath: AW_LOG_FILE,
|
|
11
|
+
maxBytes: MAX_LOG_BYTES,
|
|
12
|
+
backupPath: AW_LOG_BACKUP_FILE,
|
|
13
|
+
});
|
|
36
14
|
export function logActionableWarningsEvent(entry) {
|
|
37
15
|
if (isTestMode()) {
|
|
38
16
|
return;
|
|
39
17
|
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
rotateIfNeeded();
|
|
43
|
-
fs.appendFileSync(AW_LOG_FILE, line);
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
void err;
|
|
47
|
-
}
|
|
18
|
+
writer.log({ ts: new Date().toISOString(), ...entry });
|
|
48
19
|
}
|
|
49
20
|
export function getActionableWarningsLogPath() {
|
|
50
21
|
return AW_LOG_FILE;
|
|
51
22
|
}
|
|
23
|
+
/** Resolve once all enqueued actionable-warnings writes are on disk. */
|
|
24
|
+
export function flushActionableWarningsLog() {
|
|
25
|
+
return writer.flush();
|
|
26
|
+
}
|
|
@@ -9,22 +9,19 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Mirrors `actionable-warnings-logger.ts` for shape + rotation behaviour.
|
|
11
11
|
*/
|
|
12
|
-
import * as fs from "node:fs";
|
|
13
12
|
import * as path from "node:path";
|
|
14
13
|
import { isTestMode } from "./env-utils.js";
|
|
15
14
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
15
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
16
16
|
const AG_LOG_DIR = getGlobalPiLensDir();
|
|
17
17
|
const AG_LOG_FILE = path.join(AG_LOG_DIR, "ast-grep-tools.log");
|
|
18
18
|
const AG_LOG_BACKUP_FILE = path.join(AG_LOG_DIR, "ast-grep-tools.log.1");
|
|
19
19
|
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AST_GREP_LOG_MAX_BYTES ?? "1048576", 10) || 1048576);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
catch (err) {
|
|
26
|
-
void err;
|
|
27
|
-
}
|
|
20
|
+
const writer = createNdjsonLogger({
|
|
21
|
+
filePath: AG_LOG_FILE,
|
|
22
|
+
maxBytes: MAX_LOG_BYTES,
|
|
23
|
+
backupPath: AG_LOG_BACKUP_FILE,
|
|
24
|
+
});
|
|
28
25
|
const PATTERN_TRUNCATE_AT = 500;
|
|
29
26
|
const ERROR_TRUNCATE_AT = 300;
|
|
30
27
|
function truncate(value, max) {
|
|
@@ -105,25 +102,6 @@ export function astGrepRemediationHint(kind) {
|
|
|
105
102
|
return "Hint: verify the pattern is a single valid AST node for this `lang` (use ast_grep_dump to discover node kinds), or fall back to grep for plain-text search.";
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
|
-
function rotateIfNeeded() {
|
|
109
|
-
try {
|
|
110
|
-
if (!fs.existsSync(AG_LOG_FILE))
|
|
111
|
-
return;
|
|
112
|
-
const size = fs.statSync(AG_LOG_FILE).size;
|
|
113
|
-
if (size < MAX_LOG_BYTES)
|
|
114
|
-
return;
|
|
115
|
-
try {
|
|
116
|
-
fs.rmSync(AG_LOG_BACKUP_FILE, { force: true });
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
void err;
|
|
120
|
-
}
|
|
121
|
-
fs.renameSync(AG_LOG_FILE, AG_LOG_BACKUP_FILE);
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
void err;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
105
|
export function logAstGrepToolEvent(event) {
|
|
128
106
|
if (isTestMode())
|
|
129
107
|
return;
|
|
@@ -135,16 +113,13 @@ export function logAstGrepToolEvent(event) {
|
|
|
135
113
|
rewriteLineCount: event.rewriteLineCount,
|
|
136
114
|
errorRaw: truncate(event.errorRaw, ERROR_TRUNCATE_AT),
|
|
137
115
|
};
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
rotateIfNeeded();
|
|
141
|
-
fs.appendFileSync(AG_LOG_FILE, line);
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
void err;
|
|
145
|
-
}
|
|
116
|
+
writer.log({ ts: new Date().toISOString(), ...payload });
|
|
146
117
|
}
|
|
147
118
|
export function getAstGrepToolLogPath() {
|
|
148
119
|
return AG_LOG_FILE;
|
|
149
120
|
}
|
|
121
|
+
/** Resolve once all enqueued ast-grep-tool writes are on disk. */
|
|
122
|
+
export function flushAstGrepToolLog() {
|
|
123
|
+
return writer.flush();
|
|
124
|
+
}
|
|
150
125
|
export { countLines as _countLinesForTest };
|
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
1
|
import * as path from "node:path";
|
|
3
2
|
import { isTestMode } from "./env-utils.js";
|
|
4
3
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
4
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
5
5
|
const CASCADE_LOG_DIR = getGlobalPiLensDir();
|
|
6
6
|
const CASCADE_LOG_FILE = path.join(CASCADE_LOG_DIR, "cascade.log");
|
|
7
|
-
|
|
8
|
-
if (!fs.existsSync(CASCADE_LOG_DIR)) {
|
|
9
|
-
fs.mkdirSync(CASCADE_LOG_DIR, { recursive: true });
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
catch { }
|
|
7
|
+
const writer = createNdjsonLogger({ filePath: CASCADE_LOG_FILE });
|
|
13
8
|
export function logCascade(entry) {
|
|
14
9
|
if (isTestMode()) {
|
|
15
10
|
return;
|
|
16
11
|
}
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
fs.appendFileSync(CASCADE_LOG_FILE, line);
|
|
20
|
-
}
|
|
21
|
-
catch { }
|
|
12
|
+
writer.log({ ts: new Date().toISOString(), ...entry });
|
|
22
13
|
}
|
|
23
14
|
export function getCascadeLogPath() {
|
|
24
15
|
return CASCADE_LOG_FILE;
|
|
25
16
|
}
|
|
17
|
+
/** Resolve once all enqueued cascade writes are on disk (tests/shutdown). */
|
|
18
|
+
export function flushCascadeLog() {
|
|
19
|
+
return writer.flush();
|
|
20
|
+
}
|
|
@@ -5,26 +5,20 @@
|
|
|
5
5
|
* input to phasing decisions in the issue). Mirrors `ast-grep-tool-logger.ts`
|
|
6
6
|
* for shape + size-based rotation.
|
|
7
7
|
*/
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
8
|
import * as path from "node:path";
|
|
10
9
|
import { isTestMode } from "./env-utils.js";
|
|
11
10
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
11
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
12
12
|
const LOG_DIR = getGlobalPiLensDir();
|
|
13
13
|
const LOG_FILE = path.join(LOG_DIR, "dead-code.log");
|
|
14
14
|
const LOG_BACKUP_FILE = path.join(LOG_DIR, "dead-code.log.1");
|
|
15
15
|
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_DEAD_CODE_LOG_MAX_BYTES ?? "1048576", 10) ||
|
|
16
16
|
1048576);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
// no file yet, or rename raced — nothing to rotate
|
|
26
|
-
}
|
|
27
|
-
}
|
|
17
|
+
const writer = createNdjsonLogger({
|
|
18
|
+
filePath: LOG_FILE,
|
|
19
|
+
maxBytes: MAX_LOG_BYTES,
|
|
20
|
+
backupPath: LOG_BACKUP_FILE,
|
|
21
|
+
});
|
|
28
22
|
/**
|
|
29
23
|
* Append one scan event. Fire-and-forget: telemetry must never break a scan, so
|
|
30
24
|
* every fs error is swallowed. Skipped under test mode to keep the suite from
|
|
@@ -33,14 +27,9 @@ function rotateIfNeeded() {
|
|
|
33
27
|
export function logDeadCodeScan(event) {
|
|
34
28
|
if (isTestMode())
|
|
35
29
|
return;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
fs.appendFileSync(LOG_FILE, row + "\n");
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// telemetry is best-effort
|
|
45
|
-
}
|
|
30
|
+
writer.log({ ts: new Date().toISOString(), ...event });
|
|
31
|
+
}
|
|
32
|
+
/** Resolve once all enqueued dead-code writes are on disk (tests/shutdown). */
|
|
33
|
+
export function flushDeadCodeLog() {
|
|
34
|
+
return writer.flush();
|
|
46
35
|
}
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized LAZY accessor for `@ast-grep/napi` (a native addon — loaded on
|
|
3
|
+
* demand, never at module-eval). See ./typescript.ts for the rationale.
|
|
4
|
+
* Types are re-exported; the module itself is fetched via `loadAstGrepNapi()`.
|
|
5
|
+
*
|
|
6
|
+
* Resolved to an absolute `file://` URL via `createRequire` before importing: an
|
|
7
|
+
* absolute-path dynamic import works under pi's bundled host, a bare specifier
|
|
8
|
+
* does not. The path is converted to a `file://` URL (a raw Windows path is not
|
|
9
|
+
* a valid import specifier); bare import kept as a fallback.
|
|
10
|
+
*/
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
const _require = createRequire(import.meta.url);
|
|
1
14
|
export function loadAstGrepNapi() {
|
|
2
|
-
|
|
15
|
+
try {
|
|
16
|
+
const entry = _require.resolve("@ast-grep/napi");
|
|
17
|
+
return import(pathToFileURL(entry).href);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return import("@ast-grep/napi");
|
|
21
|
+
}
|
|
3
22
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized accessor for `@earendil-works/pi-tui`. See ./typescript.ts for the
|
|
3
3
|
* rationale. (pi-tui is a pi-bundled core package, host-provided at runtime.)
|
|
4
|
+
*
|
|
5
|
+
* Re-export named bindings, not `export *`: with the package kept external, a
|
|
6
|
+
* wildcard re-export leaves the namespace undefined at runtime under the bundle.
|
|
4
7
|
*/
|
|
5
|
-
export
|
|
8
|
+
export { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized accessor for `typebox`. See ./typescript.ts for the rationale.
|
|
3
3
|
* (typebox is a pi-bundled core package, so it resolves from the host at
|
|
4
|
-
* runtime
|
|
4
|
+
* runtime -- but it's still routed through here for a uniform dep surface.)
|
|
5
|
+
*
|
|
6
|
+
* Re-export named bindings, not `export *`: with the package kept external, a
|
|
7
|
+
* wildcard re-export leaves the namespace undefined at runtime under the bundle.
|
|
5
8
|
*/
|
|
6
|
-
export
|
|
9
|
+
export { Type } from "typebox";
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized LAZY accessor for `web-tree-sitter` (wasm — loaded on demand). See
|
|
3
|
+
* ./typescript.ts for the rationale. Types are re-exported; the module itself is
|
|
4
|
+
* fetched via `loadWebTreeSitter()`.
|
|
5
|
+
*
|
|
6
|
+
* Resolved to an absolute `file://` URL via `createRequire` before importing: an
|
|
7
|
+
* absolute-path dynamic import works under pi's bundled host, a bare specifier
|
|
8
|
+
* does not. The package `exports` map exposes only the `.` entry (no custom
|
|
9
|
+
* subpath), so we resolve the bare package name and convert it to a `file://`
|
|
10
|
+
* URL (a raw Windows path is not a valid import specifier); bare import kept as
|
|
11
|
+
* a fallback.
|
|
12
|
+
*/
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
const _require = createRequire(import.meta.url);
|
|
1
16
|
export function loadWebTreeSitter() {
|
|
2
|
-
|
|
17
|
+
try {
|
|
18
|
+
const entry = _require.resolve("web-tree-sitter");
|
|
19
|
+
return import(pathToFileURL(entry).href);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return import("web-tree-sitter");
|
|
23
|
+
}
|
|
3
24
|
}
|
|
@@ -3,17 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Log file: ~/.pi-lens/logs/{date}.jsonl
|
|
5
5
|
*/
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
6
|
import * as os from "node:os";
|
|
8
7
|
import * as path from "node:path";
|
|
9
8
|
import { isTestMode } from "./env-utils.js";
|
|
9
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
10
10
|
function getLogDir() {
|
|
11
|
-
|
|
12
|
-
const logDir = path.join(home, ".pi-lens", "logs");
|
|
13
|
-
if (!fs.existsSync(logDir)) {
|
|
14
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
15
|
-
}
|
|
16
|
-
return logDir;
|
|
11
|
+
return path.join(os.homedir(), ".pi-lens", "logs");
|
|
17
12
|
}
|
|
18
13
|
function getLogFile() {
|
|
19
14
|
const date = new Date().toISOString().split("T")[0];
|
|
@@ -28,30 +23,15 @@ export function getDiagnosticLogger() {
|
|
|
28
23
|
return _logger;
|
|
29
24
|
}
|
|
30
25
|
export function createDiagnosticLogger() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
if (writing || pending.length === 0)
|
|
35
|
-
return;
|
|
36
|
-
writing = true;
|
|
37
|
-
const toWrite = pending.splice(0, pending.length);
|
|
38
|
-
const lines = toWrite.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
39
|
-
try {
|
|
40
|
-
await fs.promises.appendFile(getLogFile(), lines);
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
// pi-lens-ignore: missing-error-propagation — fire-and-forget log write, must not throw
|
|
44
|
-
console.error("Failed to write diagnostic log:", err);
|
|
45
|
-
}
|
|
46
|
-
writing = false;
|
|
47
|
-
};
|
|
26
|
+
// Lazy filePath: the log file is keyed on the current date, resolved per
|
|
27
|
+
// drain so a long-lived logger rolls over at midnight.
|
|
28
|
+
const writer = createNdjsonLogger({ filePath: () => getLogFile() });
|
|
48
29
|
return {
|
|
49
30
|
log(entry) {
|
|
50
31
|
if (isTestMode()) {
|
|
51
32
|
return;
|
|
52
33
|
}
|
|
53
|
-
|
|
54
|
-
writePending(); // async, non-blocking
|
|
34
|
+
writer.log(entry); // async, non-blocking
|
|
55
35
|
},
|
|
56
36
|
logCaught(d, context, shownInline = false) {
|
|
57
37
|
this.log({
|
|
@@ -77,11 +57,7 @@ export function createDiagnosticLogger() {
|
|
|
77
57
|
});
|
|
78
58
|
},
|
|
79
59
|
async flush() {
|
|
80
|
-
|
|
81
|
-
await writePending();
|
|
82
|
-
while (writing) {
|
|
83
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
84
|
-
}
|
|
60
|
+
await writer.flush();
|
|
85
61
|
},
|
|
86
62
|
};
|
|
87
63
|
}
|
|
@@ -20,6 +20,42 @@ import { findLocalOpengrepConfig } from "../opengrep-config.js";
|
|
|
20
20
|
import { findLocalTyposConfig } from "../typos-config.js";
|
|
21
21
|
import { findLocalZizmorConfig } from "../zizmor-config.js";
|
|
22
22
|
import { classifyDefect } from "./diagnostic-taxonomy.js";
|
|
23
|
+
/**
|
|
24
|
+
* Semgrep/opengrep `# nosemgrep` / `# nosemgrep: <rule-id>[,<rule-id>]` inline
|
|
25
|
+
* suppression (#441). A bare `# nosemgrep` drops every finding on its line; the
|
|
26
|
+
* `: <ids>` form drops only the listed rule ids. `d.code` is the semgrep rule id.
|
|
27
|
+
* Also accepts the `//` comment form.
|
|
28
|
+
*
|
|
29
|
+
* Matches Semgrep placement: honored on the finding's OWN line (inline or not),
|
|
30
|
+
* and on the line ABOVE only when that line is a STANDALONE comment (no code before
|
|
31
|
+
* it) — so `a() # nosemgrep` suppresses a finding on `a()` but not the next line.
|
|
32
|
+
*/
|
|
33
|
+
const NOSEMGREP_RE = /(?:#|\/\/)\s*nosemgrep(?::\s*(.+))?/i;
|
|
34
|
+
const NOSEMGREP_STANDALONE_RE = /^\s*(?:#|\/\/)\s*nosemgrep(?::\s*(.+))?\s*$/i;
|
|
35
|
+
export function isNosemgrepSuppressed(d, content) {
|
|
36
|
+
const startLine = d.range?.start?.line; // 0-based
|
|
37
|
+
if (startLine == null)
|
|
38
|
+
return false;
|
|
39
|
+
const lines = content.split("\n");
|
|
40
|
+
const ruleId = String(d.code ?? "");
|
|
41
|
+
const checkLine = (text, standaloneOnly) => {
|
|
42
|
+
if (!text)
|
|
43
|
+
return false;
|
|
44
|
+
const m = (standaloneOnly ? NOSEMGREP_STANDALONE_RE : NOSEMGREP_RE).exec(text);
|
|
45
|
+
if (!m)
|
|
46
|
+
return false;
|
|
47
|
+
if (m[1] === undefined)
|
|
48
|
+
return true; // bare nosemgrep → suppress the line
|
|
49
|
+
return m[1]
|
|
50
|
+
.split(",")
|
|
51
|
+
.map((s) => s.trim())
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.includes(ruleId);
|
|
54
|
+
};
|
|
55
|
+
// The finding's own line (inline OK), then the line above (standalone comment only).
|
|
56
|
+
return (checkLine(lines[startLine], false) ||
|
|
57
|
+
checkLine(lines[startLine - 1], true));
|
|
58
|
+
}
|
|
23
59
|
export const AUXILIARY_LSP_PROFILES = [
|
|
24
60
|
{
|
|
25
61
|
serverId: "opengrep",
|
|
@@ -37,6 +73,8 @@ export const AUXILIARY_LSP_PROFILES = [
|
|
|
37
73
|
allowBlocking: (cwd) => Boolean(findLocalOpengrepConfig(cwd)),
|
|
38
74
|
semantic: (d, { blockingAllowed }) => blockingAllowed && d.severity === 1 ? "blocking" : "warning",
|
|
39
75
|
defectClass: (d) => classifyDefect(String(d.code ?? ""), "opengrep", d.message ?? ""),
|
|
76
|
+
// Honor the canonical Semgrep suppression the user already knows (#441).
|
|
77
|
+
isSuppressed: isNosemgrepSuppressed,
|
|
40
78
|
},
|
|
41
79
|
{
|
|
42
80
|
serverId: "ast-grep",
|
|
@@ -27,6 +27,7 @@ import { loadPiLensProjectConfig } from "../project-lens-config.js";
|
|
|
27
27
|
import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
|
|
28
28
|
import { safeSpawnAsync } from "../safe-spawn.js";
|
|
29
29
|
import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
|
|
30
|
+
import { applyInlineSuppressions } from "./inline-suppressions.js";
|
|
30
31
|
import { getToolPlan } from "./plan.js";
|
|
31
32
|
import { resolveRunnerPath } from "./runner-context.js";
|
|
32
33
|
import { getToolProfile } from "./tool-profile.js";
|
|
@@ -187,42 +188,6 @@ function dedupeOverlappingDiagnostics(diagnostics) {
|
|
|
187
188
|
}
|
|
188
189
|
return [...byKey.values()];
|
|
189
190
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Apply inline suppression comments.
|
|
192
|
-
* Syntax: `// pi-lens-ignore: rule-id` (JS/TS) or `# pi-lens-ignore: rule-id` (Python/Ruby/etc.)
|
|
193
|
-
* Place on the same line as the diagnostic or the line immediately above it.
|
|
194
|
-
*/
|
|
195
|
-
function applyInlineSuppressions(diagnostics, content) {
|
|
196
|
-
if (!content || !diagnostics.length)
|
|
197
|
-
return diagnostics;
|
|
198
|
-
// Build a set of (line, ruleId) pairs that are suppressed.
|
|
199
|
-
// Line numbers are 1-based to match diagnostic line numbers.
|
|
200
|
-
const suppressed = new Set();
|
|
201
|
-
const lines = content.split("\n");
|
|
202
|
-
const SUPPRESS_RE = /(?:\/\/|#)\s*pi-lens-ignore:\s*(.+)/;
|
|
203
|
-
for (let i = 0; i < lines.length; i++) {
|
|
204
|
-
const m = SUPPRESS_RE.exec(lines[i]);
|
|
205
|
-
if (!m)
|
|
206
|
-
continue;
|
|
207
|
-
const rules = m[1]
|
|
208
|
-
.split(",")
|
|
209
|
-
.map((r) => r.trim())
|
|
210
|
-
.filter(Boolean);
|
|
211
|
-
const suppressedLine = i + 1; // same line (1-based)
|
|
212
|
-
const nextLine = i + 2; // next line (1-based)
|
|
213
|
-
for (const ruleId of rules) {
|
|
214
|
-
suppressed.add(`${suppressedLine}:${ruleId}`);
|
|
215
|
-
suppressed.add(`${nextLine}:${ruleId}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (suppressed.size === 0)
|
|
219
|
-
return diagnostics;
|
|
220
|
-
return diagnostics.filter((d) => {
|
|
221
|
-
const ruleId = d.rule ?? d.id ?? "";
|
|
222
|
-
const line = d.line ?? 1;
|
|
223
|
-
return !suppressed.has(`${line}:${ruleId}`);
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
191
|
function suppressLintOverlapsWithLsp(diagnostics) {
|
|
227
192
|
const lspBySpanClass = new Set();
|
|
228
193
|
const lspByLine = new Set();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline `pi-lens-ignore` suppression — shared between the per-edit dispatch
|
|
3
|
+
* pipeline (`lens_diagnostics mode=all`) and the project-wide `mode=full` sweep so
|
|
4
|
+
* BOTH honor the same comments (#442). Previously this lived privately in the
|
|
5
|
+
* dispatcher, so a site suppressed on the write path reappeared as blocking in the
|
|
6
|
+
* full scan, making `mode=full` unusable as a clean gate.
|
|
7
|
+
*
|
|
8
|
+
* Syntax: `// pi-lens-ignore: rule-id` (JS/TS) or `# pi-lens-ignore: rule-id`
|
|
9
|
+
* (Python/Ruby/…), comma-separated for multiple rules, on the same line as the
|
|
10
|
+
* diagnostic or the line immediately above it.
|
|
11
|
+
*/
|
|
12
|
+
const SUPPRESS_RE = /(?:\/\/|#)\s*pi-lens-ignore:\s*(.+)/;
|
|
13
|
+
/**
|
|
14
|
+
* Normalize a rule id to the form a user writes in a `pi-lens-ignore` comment.
|
|
15
|
+
* The napi scan and the ast-grep LSP tag the same rule as `ast-grep:<id>` /
|
|
16
|
+
* `<id>-js` in some surfaces (see `normalizeRuleForDedup` in lens-diagnostics);
|
|
17
|
+
* a user's bare `<id>` must still suppress those, so we match the normalized form
|
|
18
|
+
* as well as the raw one.
|
|
19
|
+
*/
|
|
20
|
+
function normalizeSuppressRule(ruleId) {
|
|
21
|
+
return ruleId.replace(/^ast-grep:/, "").replace(/-js$/, "");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Drop diagnostics suppressed by an inline `pi-lens-ignore: <rule[,rule2]>`
|
|
25
|
+
* comment in `content` (the file the diagnostics belong to). A diagnostic is
|
|
26
|
+
* suppressed when its rule id — raw OR normalized — is listed on its own line or
|
|
27
|
+
* the line immediately above. Returns the surviving diagnostics (same array if
|
|
28
|
+
* nothing is suppressed).
|
|
29
|
+
*/
|
|
30
|
+
export function applyInlineSuppressions(diagnostics, content) {
|
|
31
|
+
if (!content || !diagnostics.length)
|
|
32
|
+
return diagnostics;
|
|
33
|
+
// Build the set of (1-based line, rule-id) pairs that are suppressed.
|
|
34
|
+
const suppressed = new Set();
|
|
35
|
+
const lines = content.split("\n");
|
|
36
|
+
for (let i = 0; i < lines.length; i++) {
|
|
37
|
+
const m = SUPPRESS_RE.exec(lines[i]);
|
|
38
|
+
if (!m)
|
|
39
|
+
continue;
|
|
40
|
+
const rules = m[1]
|
|
41
|
+
.split(",")
|
|
42
|
+
.map((r) => r.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
const suppressedLine = i + 1; // same line (1-based)
|
|
45
|
+
const nextLine = i + 2; // next line (1-based)
|
|
46
|
+
for (const ruleId of rules) {
|
|
47
|
+
suppressed.add(`${suppressedLine}:${ruleId}`);
|
|
48
|
+
suppressed.add(`${nextLine}:${ruleId}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (suppressed.size === 0)
|
|
52
|
+
return diagnostics;
|
|
53
|
+
return diagnostics.filter((d) => {
|
|
54
|
+
const rawId = d.rule ?? d.id ?? "";
|
|
55
|
+
const line = d.line ?? 1;
|
|
56
|
+
if (suppressed.has(`${line}:${rawId}`))
|
|
57
|
+
return false;
|
|
58
|
+
const normId = normalizeSuppressRule(rawId);
|
|
59
|
+
return normId === rawId || !suppressed.has(`${line}:${normId}`);
|
|
60
|
+
});
|
|
61
|
+
}
|