pi-lens 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/README.md +106 -0
  3. package/dist/clients/actionable-warnings-logger.js +2 -1
  4. package/dist/clients/actionable-warnings.js +14 -5
  5. package/dist/clients/advisory-provenance.js +48 -18
  6. package/dist/clients/ast-grep-client.js +1 -3
  7. package/dist/clients/atomic-write-staging.js +16 -4
  8. package/dist/clients/bash-file-access.js +99 -6
  9. package/dist/clients/biome-client.js +1 -3
  10. package/dist/clients/blocker-freshness.js +21 -8
  11. package/dist/clients/blocker-past-eof.js +16 -6
  12. package/dist/clients/bounded-cache.js +24 -8
  13. package/dist/clients/bounded-pid-file-lock.js +4 -3
  14. package/dist/clients/bounded-telemetry.js +14 -0
  15. package/dist/clients/cache-manager.js +6 -7
  16. package/dist/clients/cache-observability.js +782 -110
  17. package/dist/clients/call-graph.js +238 -63
  18. package/dist/clients/codebase-model.js +13 -5
  19. package/dist/clients/complexity-client.js +1 -3
  20. package/dist/clients/dead-code-client.js +19 -8
  21. package/dist/clients/dead-code-logger.js +1 -2
  22. package/dist/clients/deadline-utils.js +3 -1
  23. package/dist/clients/debug-heap.js +8 -1
  24. package/dist/clients/degradation-ledger.js +22 -1
  25. package/dist/clients/demoted-finding-render.js +155 -0
  26. package/dist/clients/dependency-checker.js +11 -7
  27. package/dist/clients/deps/pi-tui.js +8 -2
  28. package/dist/clients/deps/typebox.js +8 -3
  29. package/dist/clients/diagnostic-dispositions.js +39 -2
  30. package/dist/clients/diagnostic-line-freshness.js +5 -1
  31. package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
  32. package/dist/clients/dispatch/dispatcher.js +13 -1
  33. package/dist/clients/dispatch/facts/function-facts.js +3 -1
  34. package/dist/clients/dispatch/facts/import-facts.js +2 -1
  35. package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
  36. package/dist/clients/dispatch/integration.js +9 -4
  37. package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
  38. package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
  39. package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
  40. package/dist/clients/dispatch/runners/actionlint.js +5 -2
  41. package/dist/clients/dispatch/runners/biome-check.js +10 -14
  42. package/dist/clients/dispatch/runners/cpp-check.js +5 -1
  43. package/dist/clients/dispatch/runners/detekt.js +9 -6
  44. package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
  45. package/dist/clients/dispatch/runners/elixir-check.js +3 -1
  46. package/dist/clients/dispatch/runners/eslint.js +25 -6
  47. package/dist/clients/dispatch/runners/fish-indent.js +1 -1
  48. package/dist/clients/dispatch/runners/gleam-check.js +1 -1
  49. package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
  50. package/dist/clients/dispatch/runners/hadolint.js +22 -15
  51. package/dist/clients/dispatch/runners/helm-lint.js +4 -1
  52. package/dist/clients/dispatch/runners/htmlhint.js +33 -16
  53. package/dist/clients/dispatch/runners/javac.js +1 -1
  54. package/dist/clients/dispatch/runners/ktlint.js +1 -1
  55. package/dist/clients/dispatch/runners/lsp.js +1 -1
  56. package/dist/clients/dispatch/runners/markdownlint.js +34 -12
  57. package/dist/clients/dispatch/runners/mypy.js +15 -17
  58. package/dist/clients/dispatch/runners/oxlint.js +231 -15
  59. package/dist/clients/dispatch/runners/php-lint.js +1 -1
  60. package/dist/clients/dispatch/runners/phpstan.js +52 -15
  61. package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -6
  62. package/dist/clients/dispatch/runners/pyright.js +23 -14
  63. package/dist/clients/dispatch/runners/rust-clippy.js +1 -1
  64. package/dist/clients/dispatch/runners/shellcheck.js +7 -9
  65. package/dist/clients/dispatch/runners/shfmt.js +5 -2
  66. package/dist/clients/dispatch/runners/spellcheck.js +10 -15
  67. package/dist/clients/dispatch/runners/sqlfluff.js +25 -17
  68. package/dist/clients/dispatch/runners/stylelint.js +40 -20
  69. package/dist/clients/dispatch/runners/swiftlint.js +8 -8
  70. package/dist/clients/dispatch/runners/taplo.js +110 -23
  71. package/dist/clients/dispatch/runners/terragrunt.js +13 -8
  72. package/dist/clients/dispatch/runners/tflint.js +25 -15
  73. package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
  74. package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
  75. package/dist/clients/dispatch/runners/utils/runner-helpers.js +24 -1
  76. package/dist/clients/dispatch/runners/utils/tool-failure.js +152 -1
  77. package/dist/clients/dispatch/runners/vale.js +16 -14
  78. package/dist/clients/dispatch/runners/yaml-rule-parser.js +1 -3
  79. package/dist/clients/dispatch/runners/yamllint.js +13 -16
  80. package/dist/clients/dispatch/runners/zig-check.js +1 -1
  81. package/dist/clients/dispatch/suppress-writer.js +2 -1
  82. package/dist/clients/dispatch/types.js +6 -1
  83. package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
  84. package/dist/clients/extension-log.js +5 -3
  85. package/dist/clients/feature-hints.js +8 -1
  86. package/dist/clients/file-kinds.js +34 -129
  87. package/dist/clients/file-utils.js +17 -1
  88. package/dist/clients/finding-delivery-gate.js +79 -2
  89. package/dist/clients/formatters.js +144 -27
  90. package/dist/clients/freshness.js +38 -0
  91. package/dist/clients/git-guard.js +43 -13
  92. package/dist/clients/go-client.js +1 -3
  93. package/dist/clients/grammar-source.js +1 -1
  94. package/dist/clients/gzip-stage-write.js +30 -4
  95. package/dist/clients/host-edit-normalize.js +1 -3
  96. package/dist/clients/install-diagnostics.js +4 -2
  97. package/dist/clients/installer/index.js +86 -77
  98. package/dist/clients/instance-reaper.js +5 -2
  99. package/dist/clients/instance-registry.js +4 -2
  100. package/dist/clients/jscpd-client.js +11 -3
  101. package/dist/clients/knip-client.js +29 -15
  102. package/dist/clients/language-profile.js +11 -3
  103. package/dist/clients/latency-logger.js +13 -2
  104. package/dist/clients/lens-config.js +1 -1
  105. package/dist/clients/lens-map.js +8 -3
  106. package/dist/clients/live-bus-emitter.js +1 -1
  107. package/dist/clients/lsp/cascade-tier.js +27 -2
  108. package/dist/clients/lsp/client.js +144 -5
  109. package/dist/clients/lsp/config.js +4 -6
  110. package/dist/clients/lsp/edits.js +119 -32
  111. package/dist/clients/lsp/index.js +245 -41
  112. package/dist/clients/lsp/inferred-project.js +2 -1
  113. package/dist/clients/lsp/jvm-runtime.js +4 -1
  114. package/dist/clients/lsp/launch.js +11 -3
  115. package/dist/clients/lsp/pending-aux-coverage.js +152 -0
  116. package/dist/clients/lsp/server.js +19 -7
  117. package/dist/clients/lsp/tsserver-sync.js +10 -2
  118. package/dist/clients/lsp/workspace-diagnostics-cache.js +9 -4
  119. package/dist/clients/lsp-document-symbols.js +1 -3
  120. package/dist/clients/lsp-mutation.js +10 -20
  121. package/dist/clients/mcp/host-shim.js +1 -1
  122. package/dist/clients/memory-sampler.js +73 -7
  123. package/dist/clients/metrics-client.js +1 -3
  124. package/dist/clients/middle-man-analysis.js +8 -3
  125. package/dist/clients/module-report-lsp.js +7 -1
  126. package/dist/clients/module-report.js +28 -18
  127. package/dist/clients/ndjson-logger.js +117 -28
  128. package/dist/clients/opaque-mutation-scan.js +220 -0
  129. package/dist/clients/package-manager.js +6 -1
  130. package/dist/clients/pipeline.js +57 -9
  131. package/dist/clients/project-conventions.js +2 -8
  132. package/dist/clients/project-diagnostics/cache.js +10 -7
  133. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
  134. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
  135. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
  136. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
  137. package/dist/clients/project-diagnostics/scanner.js +22 -10
  138. package/dist/clients/project-lens-config.js +12 -5
  139. package/dist/clients/project-report.js +62 -27
  140. package/dist/clients/project-snapshot-fingerprint.js +48 -0
  141. package/dist/clients/project-snapshot-persist-worker.js +15 -1
  142. package/dist/clients/project-snapshot.js +472 -66
  143. package/dist/clients/read-expansion.js +12 -2
  144. package/dist/clients/read-guard-logger.js +12 -6
  145. package/dist/clients/read-guard-tool-lines.js +5 -2
  146. package/dist/clients/read-guard.js +88 -19
  147. package/dist/clients/resource-sampler.js +1 -1
  148. package/dist/clients/reverse-deps.js +6 -5
  149. package/dist/clients/review-graph/builder.js +236 -48
  150. package/dist/clients/review-graph/git-identity.js +8 -2
  151. package/dist/clients/review-graph/import-resolvers.js +10 -1
  152. package/dist/clients/review-graph/query.js +2 -1
  153. package/dist/clients/review-graph/revision-drift.js +21 -0
  154. package/dist/clients/review-graph/tsconfig-paths.js +4 -2
  155. package/dist/clients/review-graph/workspace-modules.js +8 -5
  156. package/dist/clients/ruff-client.js +6 -5
  157. package/dist/clients/runtime-agent-end.js +63 -31
  158. package/dist/clients/runtime-context.js +12 -4
  159. package/dist/clients/runtime-coordinator.js +127 -4
  160. package/dist/clients/runtime-session.js +55 -0
  161. package/dist/clients/runtime-tool-call.js +59 -3
  162. package/dist/clients/runtime-tool-result.js +148 -34
  163. package/dist/clients/runtime-turn.js +194 -8
  164. package/dist/clients/rust-client.js +1 -3
  165. package/dist/clients/safe-spawn.js +150 -11
  166. package/dist/clients/security-scan-client.js +1 -3
  167. package/dist/clients/session-event-guard.js +184 -0
  168. package/dist/clients/session-summary.js +3 -1
  169. package/dist/clients/sg-runner.js +1 -3
  170. package/dist/clients/source-filter.js +16 -8
  171. package/dist/clients/source-walker.js +1 -1
  172. package/dist/clients/spawn-timeout-cooldown.js +96 -0
  173. package/dist/clients/startup-scan.js +9 -3
  174. package/dist/clients/tool-definition.js +7 -0
  175. package/dist/clients/tool-policy.js +26 -11
  176. package/dist/clients/tool-render.js +2 -1
  177. package/dist/clients/tree-sitter-client.js +10 -4
  178. package/dist/clients/tree-sitter-logger.js +2 -1
  179. package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
  180. package/dist/clients/warm-attach.js +12 -5
  181. package/dist/clients/widget-state.js +27 -11
  182. package/dist/clients/word-index.js +3 -1
  183. package/dist/clients/workspace-topology.js +8 -2
  184. package/dist/clients/zizmor-config.js +9 -1
  185. package/dist/i18n.js +13 -5
  186. package/dist/index.js +15811 -12810
  187. package/dist/mcp/server.js +3 -1
  188. package/dist/tools/ast-grep-replace.js +68 -12
  189. package/dist/tools/lens-diagnostic-mark.js +21 -7
  190. package/dist/tools/lens-diagnostics.js +174 -38
  191. package/dist/tools/lsp-diagnostics.js +24 -7
  192. package/dist/tools/lsp-navigation.js +3 -1
  193. package/dist/tools/module-report.js +1 -1
  194. package/dist/tools/project-report.js +1 -1
  195. package/dist/tools/symbol-search.js +5 -2
  196. package/docs/durable-store-audit-1202.md +1 -1
  197. package/docs/servercapabilities.md +1 -1
  198. package/package.json +160 -143
  199. package/rules/typos/_typos.toml +2 -2
  200. package/scripts/analyze-pi-lens-logs.mjs +31 -13
  201. package/scripts/download-grammars.js +9 -3
  202. package/scripts/install-selftest.mjs +99 -11
  203. package/scripts/lib/host-provided-deps.mjs +75 -0
  204. package/scripts/lib/warm-loader-cache.mjs +285 -0
  205. package/scripts/rpc-load-check.mjs +30 -9
  206. package/scripts/warm-loader-cache.mjs +233 -0
@@ -2,6 +2,7 @@
2
2
  import { logExtension } from "./extension-log.js";
3
3
  import { LEDGER_FIELD_MAX, truncateForLedger } from "./ledger-bounds.js";
4
4
  import { logLatency } from "./latency-logger.js";
5
+ import { getSinkWriteFailures, resetSinkWriteFailures, } from "./ndjson-logger.js";
5
6
  // Re-exported so existing importers keep one name for the ledger's bound.
6
7
  export { LEDGER_FIELD_MAX, truncateForLedger };
7
8
  const ENTRIES_PER_KIND = 20;
@@ -143,12 +144,28 @@ function boundedKind(value) {
143
144
  return groups.size < MAX_DISTINCT_KINDS - 1 ? kind : OVERFLOW_KIND;
144
145
  }
145
146
  export function getDegradationSummary() {
146
- return [...groups.entries()].map(([kind, group]) => ({
147
+ const summary = [...groups.entries()].map(([kind, group]) => ({
147
148
  kind,
148
149
  count: group.count,
149
150
  droppedCount: group.count - group.entries.length,
150
151
  latestReasons: group.entries.map((entry) => ({ ...entry })),
151
152
  }));
153
+ // Folded in at read time, not written into `groups` (#1970) — see the
154
+ // `log-sink-write-failure` doc comment on `DegradationKind` for why this
155
+ // kind never goes through `recordDegradation`.
156
+ const sinkFailures = getSinkWriteFailures();
157
+ if (sinkFailures.length > 0) {
158
+ summary.push({
159
+ kind: "log-sink-write-failure",
160
+ count: sinkFailures.reduce((total, sink) => total + sink.droppedCount, 0),
161
+ droppedCount: 0,
162
+ latestReasons: sinkFailures.map((sink) => ({
163
+ subject: truncateForLedger(sink.file),
164
+ reason: truncateForLedger(`${sink.droppedCount} dropped write(s) after reopen-retry failed`),
165
+ })),
166
+ });
167
+ }
168
+ return summary;
152
169
  }
153
170
  function isRenderableSummary(value) {
154
171
  if (!Array.isArray(value))
@@ -197,6 +214,10 @@ export function resetDegradationLedger() {
197
214
  onceKeys.clear();
198
215
  tallies.clear();
199
216
  ledgerGeneration++;
217
+ // #1970, catalog shape 17: the sink write-failure tally is a
218
+ // process-lifetime latch too — it re-arms alongside the rest of the
219
+ // ledger rather than surviving past the session that observed it.
220
+ resetSinkWriteFailures();
200
221
  }
201
222
  export const DEGRADATION_ENTRIES_PER_KIND = ENTRIES_PER_KIND;
202
223
  export const DEGRADATION_MAX_DISTINCT_KINDS = MAX_DISTINCT_KINDS;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Body degradation for a DEMOTED finding (#1944).
3
+ *
4
+ * The demotion gates (#1631 dependency-drift, #1641 past-EOF) move a record
5
+ * out of the authoritative blocker channel and into the advisory channel.
6
+ * Until this module existed they changed the CHANNEL and nothing else: the
7
+ * advisory embedded the blocker body verbatim, so the agent still read
8
+ * "🔴 STOP — 11 issue(s) must be fixed" with dead line numbers under two
9
+ * hedge lines it ignored. Live evidence (session 01a0234c, 2026-08-21): the
10
+ * agent spent four re-run detours and about 7.4k tokens on a record whose
11
+ * coordinates no longer existed, then explained an unrelated REAL blocker
12
+ * away as "mid-edit state" in the same session.
13
+ *
14
+ * The rule this module owns, once, for every gated surface: a demoted body
15
+ * loses its authority vocabulary and its dead coordinates, and it says so in
16
+ * its own words. `clients/finding-delivery-gate.ts` documents the surrounding
17
+ * contract; this is the one implementation of its "Demotion rendering" arm
18
+ * for surfaces that carry a RENDERED BODY rather than a per-diagnostic row.
19
+ * Per-row surfaces (`widget-state.ts`, `tools/lens-diagnostics.ts`) already
20
+ * swap the coordinate for `PAST_EOF_STALE_MARKER` at render time and have no
21
+ * body to degrade.
22
+ *
23
+ * Pure and leaf: no store, no coordinator, no I/O. The caller supplies the
24
+ * dead lines it already computed while deciding to demote, so this module
25
+ * never re-derives a verdict from rendered prose (the
26
+ * re-derivation-vs-correlation screen).
27
+ */
28
+ /** Suffix that replaces a cited coordinate the file no longer has. */
29
+ export const DEAD_LINE_ANNOTATION = "(line no longer exists)";
30
+ /**
31
+ * Authority vocabulary a demoted body must not carry, with its degraded
32
+ * replacement. Ordered: the full banner rewrite runs first so the narrower
33
+ * fallbacks only ever see text the banner rule did not already cover.
34
+ *
35
+ * This table is the single source of truth for the rule. A surface must not
36
+ * hand-roll its own "strip the STOP" pass — that is how #1631, #1641, and
37
+ * #1664 each ended up with a different answer to the same question.
38
+ *
39
+ * POSITION-ANCHORED (#1944 review F1). These patterns run over the BANNER
40
+ * region only, never the whole body. Diagnostic messages are arbitrary text
41
+ * from arbitrary runners, and they legitimately contain both tokens: the
42
+ * reviewer's probe turned ts(2304) `Cannot find name 'STOP'` into `Cannot
43
+ * find name 'Previously flagged'`, and mangled an English "must be fixed"
44
+ * inside a rule message. Rewriting a finding's own words is a worse defect
45
+ * than the banner it was cleaning up. See `splitBannerRegion`.
46
+ */
47
+ const AUTHORITY_REWRITES = [
48
+ [
49
+ /[^\S\n]*(?:🔴|⛔|❌)?[^\S\n]*STOP[^\S\n]*[—–-][^\S\n]*(\d+)[^\S\n]+issue\(s\)[^\S\n]+must be fixed:?/g,
50
+ "$1 issue(s) were flagged before this file changed; the coordinates below may no longer exist:",
51
+ ],
52
+ [/(?:🔴|⛔|❌)[^\S\n]*STOP\b[^\S\n]*[—–-]?/g, "Previously flagged —"],
53
+ [/\bSTOP\b/g, "Previously flagged"],
54
+ [/\bmust be fixed\b/g, "were flagged"],
55
+ ];
56
+ /** ` L310: message` / `L310 message` at the head of a rendered row. */
57
+ const CITED_LINE_RE = /^([^\S\n]*)L(\d+)\b/;
58
+ /**
59
+ * Split a rendered body into its BANNER region and its CONTENT region
60
+ * (#1944 review F1).
61
+ *
62
+ * The body this module degrades is `InlineBlockerRecord.summary`, and its one
63
+ * producer is `formatDiagnostics(inlineBlockers, "blocking")`
64
+ * (`clients/dispatch/utils/format-utils.ts`), reaching the store as
65
+ * `dispatchResult.blockerOutput` -> `PipelineResult.inlineBlockerSummary` ->
66
+ * `recordInlineBlockers`. (`buildEnrichedBlockerOutput` in
67
+ * `clients/pipeline.ts` renders the same shape with code snippets, but it
68
+ * feeds the TOOL-RESULT output shown inline during the write, not this
69
+ * stored summary. #1944 review round: naming it here as a producer was
70
+ * wrong.) The shape is a header, then one ` L<n>: <message>` row per
71
+ * diagnostic, then indented continuations (`💡 Fix: …`) and an optional
72
+ * `… and N more` tail. So the first cited-line row is the boundary:
73
+ * everything above it is ours to rewrite, everything from it down is the
74
+ * findings' own words.
75
+ *
76
+ * A body with no cited row at all — `formatDiagnostics` omits `L<n>` for a
77
+ * lineless diagnostic — falls back to the first NON-BLANK line. Leading
78
+ * blanks must be skipped rather than counted (#1944 verify round, Edge-D):
79
+ * `formatDiagnostics` opens its output with a newline, so a naive "first
80
+ * line is the banner" rule degrades an empty string and leaves the real
81
+ * header at full authority. Production is currently saved from that only by
82
+ * an undocumented `.trim()` at `clients/pipeline.ts` (`inlineBlockerSummary`),
83
+ * and this module claims to own the rule, so it must hold on its own input.
84
+ */
85
+ function splitBannerRegion(lines) {
86
+ const firstCitedRow = lines.findIndex((line) => CITED_LINE_RE.test(line));
87
+ if (firstCitedRow !== -1)
88
+ return { bannerEnd: firstCitedRow };
89
+ const firstNonBlank = lines.findIndex((line) => line.trim() !== "");
90
+ return { bannerEnd: firstNonBlank === -1 ? 0 : firstNonBlank + 1 };
91
+ }
92
+ /**
93
+ * Degrade one demoted finding's rendered body.
94
+ *
95
+ * Two independent transforms, deliberately separable so each can be proven by
96
+ * its own test (and so deleting either one reds a different assertion):
97
+ *
98
+ * 1. Authority vocabulary: the STOP banner and the "must be fixed"
99
+ * imperative are rewritten to past-tense, non-directive wording.
100
+ * 2. Dead coordinates: every cited `L<n>` whose line the file no longer has
101
+ * renders as `L<n> (line no longer exists)`. The message text stays —
102
+ * demote, never drop (#1419).
103
+ */
104
+ export function degradeDemotedFindingBody(summary, options = {}) {
105
+ const deadLines = new Set(options.deadLines ?? []);
106
+ let authorityMarkersRemoved = 0;
107
+ const sourceLines = summary.split("\n");
108
+ const { bannerEnd } = splitBannerRegion(sourceLines);
109
+ const degradedLines = sourceLines.map((line, index) => {
110
+ // Content rows keep every character. Only the banner region is ours.
111
+ if (index >= bannerEnd)
112
+ return line;
113
+ let rewritten = line;
114
+ for (const [pattern, replacement] of AUTHORITY_REWRITES) {
115
+ rewritten = rewritten.replace(pattern, (...args) => {
116
+ authorityMarkersRemoved += 1;
117
+ // Expand `$n` against this match's capture groups by hand. A nested
118
+ // `String.replace` with the SAME global regex would clobber the
119
+ // outer call's `lastIndex` mid-iteration.
120
+ return replacement.replace(/\$(\d)/g, (_, digit) => String(args[Number(digit)] ?? ""));
121
+ });
122
+ }
123
+ return rewritten;
124
+ });
125
+ let body = degradedLines.join("\n");
126
+ const deadLinesAnnotated = [];
127
+ if (deadLines.size > 0) {
128
+ body = degradedLines
129
+ .map((line) => {
130
+ const hit = CITED_LINE_RE.exec(line);
131
+ if (!hit)
132
+ return line;
133
+ const cited = Number.parseInt(hit[2] ?? "", 10);
134
+ if (!Number.isFinite(cited) || !deadLines.has(cited))
135
+ return line;
136
+ deadLinesAnnotated.push(cited);
137
+ return `${hit[1] ?? ""}L${cited} ${DEAD_LINE_ANNOTATION}${line.slice(hit[0].length)}`;
138
+ })
139
+ .join("\n");
140
+ }
141
+ return { body, authorityMarkersRemoved, deadLinesAnnotated };
142
+ }
143
+ /**
144
+ * The one-line note a surface appends when it RETIRES a demoted finding after
145
+ * its single degraded delivery. Rendered inside the advisory itself, so an
146
+ * empty advisory section can only ever mean "nothing to say" — if something
147
+ * was dropped, this line is in the payload (#1944 AC3).
148
+ */
149
+ export function formatRetirementNote(deadLines) {
150
+ const cited = deadLines.length > 0
151
+ ? ` (cited ${deadLines.length === 1 ? "line" : "lines"} ${deadLines.join(", ")})`
152
+ : "";
153
+ return (`Retired after this delivery${cited}: the file no longer has these lines, ` +
154
+ "so this finding cannot be re-confirmed and will not be served again.");
155
+ }
@@ -55,8 +55,7 @@ export function parseMadgeSkips(stderr) {
55
55
  const headerIdx = lines.findIndex((l) => /Skipped\s+\d+\s+file/i.test(l));
56
56
  if (headerIdx === -1)
57
57
  return { total: 0, local: [] };
58
- const total = Number.parseInt(lines[headerIdx].match(/Skipped\s+(\d+)/i)?.[1] ?? "0", 10) ||
59
- 0;
58
+ const total = Number.parseInt(lines[headerIdx].match(/Skipped\s+(\d+)/i)?.[1] ?? "0", 10) || 0;
60
59
  const specifiers = lines
61
60
  .slice(headerIdx + 1)
62
61
  .map((l) => l.trim())
@@ -203,9 +202,7 @@ export class DependencyChecker {
203
202
  // next time the registry is walked.
204
203
  static instances = new Set();
205
204
  constructor(verbose = false) {
206
- this.log = verbose
207
- ? createSubsystemLogger("deps")
208
- : () => { };
205
+ this.log = verbose ? createSubsystemLogger("deps") : () => { };
209
206
  DependencyChecker.instances.add(new WeakRef(this));
210
207
  }
211
208
  /** Drop this instance's memoized madge resolution for every project root. */
@@ -435,7 +432,9 @@ export class DependencyChecker {
435
432
  const cached = this.importCache.get(normalized);
436
433
  // Fast path: neither mtime NOR size moved (#1105 — size guards the
437
434
  // mtime-preserving content change that mtime alone would miss).
438
- if (cached && cached.timestamp >= stat.mtimeMs && cached.size === stat.size) {
435
+ if (cached &&
436
+ cached.timestamp >= stat.mtimeMs &&
437
+ cached.size === stat.size) {
439
438
  return false;
440
439
  }
441
440
  // Compare actual imports
@@ -567,7 +566,12 @@ export class DependencyChecker {
567
566
  if (skips.local.length > 0) {
568
567
  this.log(`madge skipped ${skips.local.length} local file(s) (possible silent cycle-miss): ${skips.local.slice(0, 5).join(", ")}`);
569
568
  }
570
- return { ok: true, circular, circularFiles, localSkips: skips.local.length };
569
+ return {
570
+ ok: true,
571
+ circular,
572
+ circularFiles,
573
+ localSkips: skips.local.length,
574
+ };
571
575
  }
572
576
  catch (err) {
573
577
  this.log(`Check error: ${err.message}`);
@@ -1,6 +1,12 @@
1
1
  /**
2
- * Centralized accessor for `@earendil-works/pi-tui`. See ./typescript.ts for the
3
- * rationale. (pi-tui is a pi-bundled core package, host-provided at runtime.)
2
+ * Centralized accessor for `@earendil-works/pi-tui`, routed through here for a
3
+ * uniform dep surface. pi-tui is a pi-bundled core package: the host resolves
4
+ * the bare specifier from its own runtime, so pi-lens declares it as an OPTIONAL
5
+ * peer plus a devDependency and never as a runtime dependency. A runtime
6
+ * dependency makes `npm install --omit=dev` vendor a private second copy that
7
+ * Node evaluates at import — ~97ms of the 838ms git-install module import in
8
+ * #1926. `scripts/lib/host-provided-deps.mjs` holds the list, and
9
+ * `tests/packaging.test.ts` pins the declaration.
4
10
  *
5
11
  * Re-export named bindings, not `export *`: with the package kept external, a
6
12
  * wildcard re-export leaves the namespace undefined at runtime under the bundle.
@@ -1,7 +1,12 @@
1
1
  /**
2
- * Centralized accessor for `typebox`. See ./typescript.ts for the rationale.
3
- * (typebox is a pi-bundled core package, so it resolves from the host at
4
- * runtime -- but it's still routed through here for a uniform dep surface.)
2
+ * Centralized accessor for `typebox`, routed through here for a uniform dep
3
+ * surface. typebox is a pi-bundled core package: the host resolves the bare
4
+ * specifier from its own runtime, so pi-lens declares it as an OPTIONAL peer
5
+ * plus a devDependency and never as a runtime dependency. A runtime dependency
6
+ * makes `npm install --omit=dev` vendor a private second copy that Node
7
+ * evaluates at import — 688 files, ~400ms of the 838ms git-install module
8
+ * import in #1926. `scripts/lib/host-provided-deps.mjs` holds the list, and
9
+ * `tests/packaging.test.ts` pins the declaration.
5
10
  *
6
11
  * Re-export named bindings, not `export *`: with the package kept external, a
7
12
  * wildcard re-export leaves the namespace undefined at runtime under the bundle.
@@ -171,6 +171,35 @@ export function _setBeforeDispositionCacheRefreshForTests(hook) {
171
171
  export function _setDispositionStatForTests(statSync) {
172
172
  dispositionStatSync = statSync ?? fs.statSync;
173
173
  }
174
+ /**
175
+ * Zero-I/O guard for cache-only callers (lens_diagnostics delta/all): true
176
+ * when ANY disposition mark or session defer exists, so a caller can skip its
177
+ * whole strict/weak filter — including every file stat/read the strict branch
178
+ * would need — in the overwhelmingly common no-marks case. Same hoist shape as
179
+ * applyDispositionsMultiFile's #1625 F2 empty-store early return; `readState`
180
+ * is mtime+size stat-cached, so repeated calls cost one stat.
181
+ */
182
+ export function hasAnyDispositionMarks(cwd) {
183
+ return Boolean(readState(cwd).dispositions) || deferredThisSession.size > 0;
184
+ }
185
+ /**
186
+ * True when the project's disposition store holds at least one STRICT-anchored
187
+ * `false-positive` mark — the only disposition kind whose immediate application
188
+ * needs file content. A store with only weak suppress/defer marks can never
189
+ * produce a strict-anchor match, so cache-only callers can serve them through
190
+ * the zero-I/O weak filter and skip every stat/read the strict path would
191
+ * charge.
192
+ */
193
+ export function hasStrictDispositionMarks(cwd) {
194
+ const dispositions = readState(cwd).dispositions;
195
+ if (!dispositions)
196
+ return false;
197
+ for (const key of Object.keys(dispositions)) {
198
+ if (dispositions[key]?.disposition === "false-positive")
199
+ return true;
200
+ }
201
+ return false;
202
+ }
174
203
  // Test seam for `applyDispositionsMultiFile`'s per-group content read (#1625
175
204
  // F2) — Vitest can't `vi.spyOn` a bare ESM `node:fs` named export directly
176
205
  // ("Module namespace is not configurable in ESM"), so the F2 regression test
@@ -191,7 +220,13 @@ function readState(cwd) {
191
220
  return stateCache.state;
192
221
  }
193
222
  const empty = {};
194
- stateCache = { path: p, missing: true, mtimeMs: -1, size: -1, state: empty };
223
+ stateCache = {
224
+ path: p,
225
+ missing: true,
226
+ mtimeMs: -1,
227
+ size: -1,
228
+ state: empty,
229
+ };
195
230
  return empty;
196
231
  }
197
232
  if (stateCache &&
@@ -205,7 +240,9 @@ function readState(cwd) {
205
240
  try {
206
241
  const parsed = JSON.parse(fs.readFileSync(p, "utf-8"));
207
242
  state =
208
- parsed && typeof parsed === "object" ? parsed : {};
243
+ parsed && typeof parsed === "object"
244
+ ? parsed
245
+ : {};
209
246
  }
210
247
  catch {
211
248
  // Now that writeState is tmp+rename atomic, a torn read (another process
@@ -228,7 +228,11 @@ export function demotePastEofDiagnostics(args) {
228
228
  return d; // no transition either direction
229
229
  if (isPastEof)
230
230
  risingEdgeLines.push(d.line);
231
- return { ...d, stale: isPastEof, staleReason: isPastEof ? "past-eof" : undefined };
231
+ return {
232
+ ...d,
233
+ stale: isPastEof,
234
+ staleReason: isPastEof ? "past-eof" : undefined,
235
+ };
232
236
  });
233
237
  if (risingEdgeLines.length === 0) {
234
238
  return { diagnostics: out, demotedCount: 0 };
@@ -55,8 +55,7 @@ export function isNosemgrepSuppressed(d, content) {
55
55
  .includes(ruleId);
56
56
  };
57
57
  // The finding's own line (inline OK), then the line above (standalone comment only).
58
- return (checkLine(lines[startLine], false) ||
59
- checkLine(lines[startLine - 1], true));
58
+ return (checkLine(lines[startLine], false) || checkLine(lines[startLine - 1], true));
60
59
  }
61
60
  /**
62
61
  * zizmor's own native inline suppression: `# zizmor: ignore[audit-id[,audit-id]]`
@@ -91,7 +90,7 @@ export function isZizmorIgnoreSuppressed(d, content) {
91
90
  // findings block only when the workspace opted into curated/authored rules;
92
91
  // everything else stays advisory) — shared here instead of copy-pasted per
93
92
  // profile so a future policy change (e.g. a WARNING-blocking tier) is one edit.
94
- const blockOnErrorWhenAllowed = (d, { blockingAllowed }) => (blockingAllowed && d.severity === 1 ? "blocking" : "warning");
93
+ const blockOnErrorWhenAllowed = (d, { blockingAllowed }) => blockingAllowed && d.severity === 1 ? "blocking" : "warning";
95
94
  export const AUXILIARY_LSP_PROFILES = [
96
95
  {
97
96
  serverId: "opengrep",
@@ -36,6 +36,7 @@ import { getToolPlan } from "./plan.js";
36
36
  import { resolveRunnerPath } from "./runner-context.js";
37
37
  import { applyRulePolicy, rulePolicyMapFromConfig } from "./rule-policy.js";
38
38
  import { getToolProfile } from "./tool-profile.js";
39
+ import { isRunnerSkipReason } from "./types.js";
39
40
  import { formatDiagnostics } from "./utils/format-utils.js";
40
41
  // --- Runner Registry ---
41
42
  export class RunnerRegistry {
@@ -606,6 +607,12 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
606
607
  onRunnerResult?.(runnerId, result);
607
608
  const runnerEnd = Date.now();
608
609
  const duration = runnerEnd - runnerStart;
610
+ // Runner definitions are a typed API, but embedders/plugins can still
611
+ // return untyped objects at runtime. Admit only the closed taxonomy and
612
+ // only on actual skips so free text cannot enter durable latency metadata.
613
+ const skipReason = result.status === "skipped" && isRunnerSkipReason(result.skipReason)
614
+ ? result.skipReason
615
+ : undefined;
609
616
  latencies.push({
610
617
  runnerId,
611
618
  startTime: runnerStart,
@@ -614,6 +621,9 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
614
621
  status: result.status,
615
622
  diagnosticCount: result.diagnostics.length,
616
623
  semantic: result.semantic ?? semantic,
624
+ ...(skipReason !== undefined && {
625
+ skipReason,
626
+ }),
617
627
  ...(result.unconfirmedServerIds !== undefined && {
618
628
  unconfirmedServerIds: result.unconfirmedServerIds,
619
629
  }),
@@ -640,7 +650,9 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
640
650
  failureKind: result.failureKind,
641
651
  failureMessage: result.failureMessage,
642
652
  }
643
- : undefined,
653
+ : skipReason
654
+ ? { skipReason }
655
+ : undefined,
644
656
  });
645
657
  recordRunner(ctx.filePath, runnerId, result.status, result.diagnostics.length, duration, ctx.writeIndex);
646
658
  diagnostics.push(...result.diagnostics);
@@ -76,7 +76,9 @@ function parameterName(param) {
76
76
  // JavaScript's grammar keeps a leaf parameter as the identifier itself;
77
77
  // there is no named child to unwrap. TypeScript parameter wrapper nodes still
78
78
  // use their first named child (type annotation/default/rest target).
79
- return param.type === "identifier" ? param.text : firstNamedChild(param)?.text ?? "";
79
+ return param.type === "identifier"
80
+ ? param.text
81
+ : (firstNamedChild(param)?.text ?? "");
80
82
  }
81
83
  function getFunctionName(node) {
82
84
  if (node.type === "function_declaration") {
@@ -122,7 +122,8 @@ export const importFactProvider = {
122
122
  if (callee?.type === "import") {
123
123
  dynamicRaw.push({ source, kind: "import" });
124
124
  }
125
- else if (callee?.type === "identifier" && callee.text === "require") {
125
+ else if (callee?.type === "identifier" &&
126
+ callee.text === "require") {
126
127
  dynamicRaw.push({ source, kind: "require" });
127
128
  hasCjs = true;
128
129
  }
@@ -8,7 +8,7 @@
8
8
  * shell.
9
9
  */
10
10
  import { childrenOfType, firstChildOfType, withTreeSitterRoot, walk, } from "../../tree-sitter-shared.js";
11
- export { childrenOfType, firstChildOfType, walk, };
11
+ export { childrenOfType, firstChildOfType, walk };
12
12
  export const withFactTree = withTreeSitterRoot;
13
13
  /**
14
14
  * Provider shell for tree-sitter-backed fact extractors: read `file.content`,
@@ -789,7 +789,8 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
789
789
  action: "reused_unchanged",
790
790
  savedToSnapshot: reverseDepsSaved,
791
791
  importsFileCount: Object.keys(reverseDepsIndex.imports).length,
792
- importedByFileCount: Object.keys(reverseDepsIndex.importedBy).length,
792
+ importedByFileCount: Object.keys(reverseDepsIndex.importedBy)
793
+ .length,
793
794
  },
794
795
  });
795
796
  }
@@ -841,7 +842,8 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
841
842
  action: "refresh_from_review_graph",
842
843
  savedToSnapshot: reverseDepsSaved,
843
844
  importsFileCount: Object.keys(reverseDepsIndex.imports).length,
844
- importedByFileCount: Object.keys(reverseDepsIndex.importedBy).length,
845
+ importedByFileCount: Object.keys(reverseDepsIndex.importedBy)
846
+ .length,
845
847
  importEdgeCount: Object.values(reverseDepsIndex.imports).reduce((total, imports) => total + imports.length, 0),
846
848
  },
847
849
  });
@@ -1517,7 +1519,9 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1517
1519
  // #1179: `.filter()` here operates on `rawDiags.diags`; the
1518
1520
  // `inconclusive`/`binding` flags read above stay on the `rawDiags`
1519
1521
  // wrapper and are unaffected by this copy (the shape-5 fix).
1520
- const diags = convertLspDiagnostics(rawDiags.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
1522
+ const diags = convertLspDiagnostics(rawDiags.diags
1523
+ .filter((d) => d.severity === 1)
1524
+ .slice(0, MAX_PER_FILE), neighborPath);
1521
1525
  const durationMs = Date.now() - neighborStart;
1522
1526
  // #1899: the A5 same-write cache write stood here.
1523
1527
  if (diags.length === 0) {
@@ -1827,7 +1831,8 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1827
1831
  if (entry.idleTimer !== undefined)
1828
1832
  clearTimeout(entry.idleTimer);
1829
1833
  entry.idleTimer = undefined;
1830
- if (!reverseDepsTimersToRelease.has(key) && reverseDepsEntriesAtStart.has(entry)) {
1834
+ if (!reverseDepsTimersToRelease.has(key) &&
1835
+ reverseDepsEntriesAtStart.has(entry)) {
1831
1836
  reverseDepsTimersToRelease.add(key);
1832
1837
  }
1833
1838
  }
@@ -10,7 +10,9 @@ export const errorSwallowingRule = {
10
10
  return [];
11
11
  const diagnostics = [];
12
12
  for (const s of summaries) {
13
- if (s.isEmpty && !s.isDocumentedLocalFallback && s.boundaryCategory !== "fs") {
13
+ if (s.isEmpty &&
14
+ !s.isDocumentedLocalFallback &&
15
+ s.boundaryCategory !== "fs") {
14
16
  diagnostics.push({
15
17
  id: `error-swallowing:${ctx.filePath}:${s.line}:${s.column}`,
16
18
  tool: "fact-rules",
@@ -1,7 +1,9 @@
1
1
  import { isTestFile } from "../../file-utils.js";
2
2
  const ALIAS_COMMENT_RE = /\b(alias|backward\s*compat|backwards\s*compat|compatibility|shim|adapter)\b/i;
3
3
  function hasAliasCommentNear(line, comments) {
4
- return comments.some((comment) => comment.line >= line - 2 && comment.line <= line && ALIAS_COMMENT_RE.test(comment.text));
4
+ return comments.some((comment) => comment.line >= line - 2 &&
5
+ comment.line <= line &&
6
+ ALIAS_COMMENT_RE.test(comment.text));
5
7
  }
6
8
  export const passThroughWrappersRule = {
7
9
  id: "pass-through-wrappers",
@@ -16,7 +18,9 @@ export const passThroughWrappersRule = {
16
18
  return [];
17
19
  const diagnostics = [];
18
20
  for (const fn of summaries) {
19
- if (!fn.isPassThroughWrapper || fn.statementCount !== 1 || fn.isBoundaryWrapper) {
21
+ if (!fn.isPassThroughWrapper ||
22
+ fn.statementCount !== 1 ||
23
+ fn.isBoundaryWrapper) {
20
24
  continue;
21
25
  }
22
26
  if (hasAliasCommentNear(fn.line, comments))
@@ -10,16 +10,40 @@
10
10
  // All other entries are exact callee names — prevents e.g. "spawn" matching "spawned.map"
11
11
  // or "fetch" matching "fetchWithRetry".
12
12
  const IO_NAMESPACE_PREFIXES = [
13
- "db.", "prisma.", "knex.", "mongoose.", "sequelize.",
14
- "http.", "https.", "net.", "dns.",
15
- "redis.", "mongo.", "pg.", "mysql.",
16
- "s3.", "storage.", "bucket.",
13
+ "db.",
14
+ "prisma.",
15
+ "knex.",
16
+ "mongoose.",
17
+ "sequelize.",
18
+ "http.",
19
+ "https.",
20
+ "net.",
21
+ "dns.",
22
+ "redis.",
23
+ "mongo.",
24
+ "pg.",
25
+ "mysql.",
26
+ "s3.",
27
+ "storage.",
28
+ "bucket.",
17
29
  "fs.promises.",
18
30
  ];
19
31
  const IO_EXACT_CALLEES = new Set([
20
- "fetch", "axios", "got", "request",
21
- "spawn", "exec", "execSync", "spawnSync",
22
- "readFile", "writeFile", "appendFile", "readdir", "mkdir", "unlink", "stat",
32
+ "fetch",
33
+ "axios",
34
+ "got",
35
+ "request",
36
+ "spawn",
37
+ "exec",
38
+ "execSync",
39
+ "spawnSync",
40
+ "readFile",
41
+ "writeFile",
42
+ "appendFile",
43
+ "readdir",
44
+ "mkdir",
45
+ "unlink",
46
+ "stat",
23
47
  ]);
24
48
  const CC_THRESHOLD = 6; // raised from 4 — avoids flagging simple async wrappers with one IO call
25
49
  function callsToBoundary(outgoingCalls) {
@@ -12,7 +12,10 @@ function toDiagnostic(issue, filePath) {
12
12
  const column = issue.column && issue.column > 0 ? issue.column : 1;
13
13
  const rule = issue.kind || "actionlint";
14
14
  const message = issue.message || "GitHub Actions workflow issue";
15
- const idMessage = message.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 80);
15
+ const idMessage = message
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, "-")
18
+ .slice(0, 80);
16
19
  return {
17
20
  id: `actionlint-${rule}-${line}-${column}-${idMessage}`,
18
21
  message,
@@ -66,7 +69,7 @@ const actionlintRunner = {
66
69
  async run(ctx) {
67
70
  const cwd = ctx.cwd || process.cwd();
68
71
  let cmd = null;
69
- if (await (actionlint.isAvailableAsync(cwd))) {
72
+ if (await actionlint.isAvailableAsync(cwd)) {
70
73
  cmd = actionlint.getCommand(cwd);
71
74
  }
72
75
  else {
@@ -12,6 +12,7 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
12
12
  import { biomeConfigArgs, getAutofixCapability, getJstsLintPolicyForCwd, } from "../../tool-policy.js";
13
13
  import { PRIORITY } from "../priorities.js";
14
14
  import { resolveToolCommandWithInstallFallback } from "./utils/runner-helpers.js";
15
+ import { finishParsedRun } from "./utils/tool-failure.js";
15
16
  /**
16
17
  * Map biome's own severity vocabulary onto the four-tier `Diagnostic.severity`
17
18
  * (clients/dispatch/types.ts), the same way `normalizeRuleSeverity` does for
@@ -254,7 +255,9 @@ const biomeCheckJsonRunner = {
254
255
  diagnostics: [
255
256
  {
256
257
  id: "biome:parse-error:1",
257
- message: "Biome JSON parse failed: " + parsed.parseError + (preview ? " (output preview: " + preview + ")" : ""),
258
+ message: "Biome JSON parse failed: " +
259
+ parsed.parseError +
260
+ (preview ? " (output preview: " + preview + ")" : ""),
258
261
  filePath: ctx.filePath,
259
262
  line: 1,
260
263
  column: 1,
@@ -266,19 +269,12 @@ const biomeCheckJsonRunner = {
266
269
  semantic: "warning",
267
270
  };
268
271
  }
269
- const diagnostics = parsed.diagnostics;
270
- let semantic = "none";
271
- if (diagnostics.some((d) => d.semantic === "blocking")) {
272
- semantic = "blocking";
273
- }
274
- else if (diagnostics.length > 0) {
275
- semantic = "warning";
276
- }
277
- return {
278
- status: semantic === "blocking" ? "failed" : "succeeded",
279
- diagnostics,
280
- semantic,
281
- };
272
+ return finishParsedRun({
273
+ tool: "biome",
274
+ ctx,
275
+ result: checkResult,
276
+ diagnostics: parsed.diagnostics,
277
+ });
282
278
  },
283
279
  };
284
280
  export default biomeCheckJsonRunner;