pi-lens 4.2.0 → 4.3.0
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 +543 -0
- package/README.md +45 -2
- package/dist/clients/actionable-warnings.js +44 -16
- package/dist/clients/advisory-provenance.js +187 -51
- package/dist/clients/cache-manager.js +64 -5
- package/dist/clients/cascade-format.js +24 -0
- package/dist/clients/child-unref.js +17 -8
- package/dist/clients/config-diagnostic-codes.js +2 -0
- package/dist/clients/config-locations.js +237 -9
- package/dist/clients/config-resolve.js +7 -1
- package/dist/clients/degradation-ledger.js +33 -3
- package/dist/clients/dependency-checker.js +87 -64
- package/dist/clients/diagnostic-dispositions.js +39 -3
- package/dist/clients/dispatch/finding-policy.js +118 -1
- package/dist/clients/dispatch/indent-detect.js +8 -2
- package/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/dist/clients/dispatch/rule-ignores.js +14 -7
- package/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/dist/clients/dispatch/runners/credo.js +16 -14
- package/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/dist/clients/dispatch/runners/detekt.js +11 -4
- package/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/dist/clients/dispatch/runners/eslint.js +29 -46
- package/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/dist/clients/dispatch/runners/javac.js +31 -21
- package/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/dist/clients/dispatch/runners/lsp.js +6 -5
- package/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/dist/clients/dispatch/runners/mypy.js +2 -1
- package/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/dist/clients/dispatch/runners/ruff.js +18 -19
- package/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/dist/clients/dispatch/runners/taplo.js +12 -6
- package/dist/clients/dispatch/runners/tflint.js +30 -15
- package/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/dist/clients/dispatch/runners/vale.js +15 -3
- package/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/dist/clients/dispatch/suppress-writer.js +13 -3
- package/dist/clients/effective-config.js +3 -1
- package/dist/clients/extension-log.js +48 -1
- package/dist/clients/finding-delivery-gate.js +53 -23
- package/dist/clients/formatters.js +49 -11
- package/dist/clients/git-guard.js +247 -34
- package/dist/clients/gradle-ktfmt-style.js +121 -25
- package/dist/clients/inline-blocker-dispositions.js +186 -0
- package/dist/clients/install-diagnostics.js +19 -6
- package/dist/clients/installer/index.js +281 -32
- package/dist/clients/jscpd-client.js +8 -0
- package/dist/clients/latency-logger.js +8 -0
- package/dist/clients/lens-config.js +66 -8
- package/dist/clients/lsp/cascade-tier.js +3 -0
- package/dist/clients/lsp/client.js +139 -11
- package/dist/clients/lsp/config.js +9 -7
- package/dist/clients/lsp/content-limits.js +41 -0
- package/dist/clients/lsp/index.js +42 -8
- package/dist/clients/lsp/launch.js +23 -2
- package/dist/clients/lsp/server.js +106 -25
- package/dist/clients/lsp/sync-kind.js +14 -0
- package/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/dist/clients/mcp/analyze.js +9 -2
- package/dist/clients/mcp/ipc.js +232 -44
- package/dist/clients/mcp/review.js +53 -7
- package/dist/clients/opengrep-client.js +11 -6
- package/dist/clients/path-utils.js +51 -9
- package/dist/clients/persistent-reverify.js +271 -0
- package/dist/clients/pipeline.js +136 -67
- package/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/dist/clients/project-lens-config.js +11 -3
- package/dist/clients/review-graph/builder.js +34 -26
- package/dist/clients/review-graph/git-identity.js +16 -10
- package/dist/clients/ruff-client.js +31 -4
- package/dist/clients/runtime-coordinator.js +38 -4
- package/dist/clients/runtime-tool-call.js +16 -5
- package/dist/clients/runtime-tool-result.js +27 -12
- package/dist/clients/runtime-turn.js +571 -287
- package/dist/clients/safe-spawn.js +256 -65
- package/dist/clients/skills-resolver.js +38 -2
- package/dist/clients/spawn-output-cap.js +84 -1
- package/dist/clients/tree-sitter-client.js +119 -58
- package/dist/clients/turn-end/lane.js +51 -0
- package/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/dist/clients/warm-attach.js +59 -0
- package/dist/clients/widget-state.js +294 -17
- package/dist/clients/workspace-topology.js +57 -3
- package/dist/clients/zizmor-config.js +26 -13
- package/dist/index.js +6342 -3995
- package/dist/mcp/analyze-cli.js +4 -0
- package/dist/mcp/server.js +33 -21
- package/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/dist/tools/lens-diagnostic-mark.js +14 -2
- package/dist/tools/lens-diagnostics.js +127 -33
- package/dist/tools/lsp-diagnostics.js +150 -12
- package/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/docs/adr/0009-reported-path-attribution.md +114 -0
- package/docs/adr/README.md +26 -0
- package/docs/agent-tools.md +18 -1
- package/docs/ast-grep_rules_catalog.md +4 -3
- package/docs/configuration.md +55 -1
- package/docs/engineering-principles.md +202 -0
- package/docs/environment-variables.md +61 -4
- package/docs/features.md +2 -2
- package/docs/globalconfig.md +6 -1
- package/docs/language-coverage.md +17 -0
- package/docs/lsp-capability-matrix.md +98 -31
- package/docs/pi-lens-fixer.md +57 -3
- package/docs/pi-lens-investigator.md +9 -0
- package/docs/pi-lens-reviewer.md +16 -0
- package/docs/pi-lens-subagent.md +9 -0
- package/docs/public-api-stability.md +1 -0
- package/docs/release-qa-baseline.md +2 -0
- package/docs/servercapabilities.md +21 -11
- package/docs/settings.md +12 -3
- package/docs/subagent-compat.md +1 -1
- package/package.json +5 -3
- package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/scripts/install-selftest.mjs +21 -17
- package/scripts/lib/web-tree-sitter-dir.mjs +123 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3170 — the bounded persistent-reverify pass.
|
|
3
|
+
*
|
|
4
|
+
* A finding whose own file is unchanged is re-delivered turn after turn from
|
|
5
|
+
* the persisted actionable-warnings report without ever being re-observed
|
|
6
|
+
* against the live server: the in-band publish carries forward DEFERRED-origin
|
|
7
|
+
* file entries while their file has not moved
|
|
8
|
+
* (`mergeActionableWarningsReports`'s scope guard), and the delta freshness
|
|
9
|
+
* gate passes them because the file's mtime has not moved either. When the
|
|
10
|
+
* root cause was fixed in a DIFFERENT file, the carried finding is stale in
|
|
11
|
+
* fact but fresh by every on-disk axis.
|
|
12
|
+
*
|
|
13
|
+
* This module re-observes those files at turn_end, BEFORE the advisory
|
|
14
|
+
* assembles, and the replacement entries fold into the turn's SINGLE in-band
|
|
15
|
+
* publish (#3176 F1: a separate replacement publish spends the carry marker
|
|
16
|
+
* and the main publish then drops the entries at the scope guard — the
|
|
17
|
+
* blocker the review caught).
|
|
18
|
+
*
|
|
19
|
+
* Review-round contracts (#3176):
|
|
20
|
+
* - **F2**: the fresh observation converts through the PRODUCER's own
|
|
21
|
+
* pipeline (`enrichFileFromLsp` — severity-2, code-action enrichment,
|
|
22
|
+
* fixable-only, the per-file caps), fed the touch's collected diagnostics
|
|
23
|
+
* as the cached arm so no second pull happens. No re-implementation.
|
|
24
|
+
* - **F3**: an empty result is clean ONLY when the touch itself answers
|
|
25
|
+
* `confirmation: "confirmed"` — the silent-on-clean rule (#240/#533/#1253).
|
|
26
|
+
* A bare `{diags: []}` (the house double's silent empty, a tier-3 server
|
|
27
|
+
* still analyzing, a `skipReason`) is UNCONFIRMED: the carried warnings are
|
|
28
|
+
* kept verbatim and the render labels the gap. `touched.inconclusive` is
|
|
29
|
+
* `resolveTouchVerdict`'s output computed service-side and is honored
|
|
30
|
+
* directly.
|
|
31
|
+
* - **F4**: every await is bound-wrapped (`bounded()`); `maxClientWaitMs` is
|
|
32
|
+
* only the spawn/ready budget, not a request ceiling.
|
|
33
|
+
* - **F6**: no skip guard — every deferred-origin entry re-verifies each
|
|
34
|
+
* turn (re-arm), per the issue's failure table.
|
|
35
|
+
*
|
|
36
|
+
* Bounds: at most {@link MAX_REVERIFY_FILES} files per turn_end, a wall
|
|
37
|
+
* budget of {@link REVERIFY_BUDGET_MS}, the turn-end abort signal, and no
|
|
38
|
+
* new durable state — candidates come from the persisted report itself,
|
|
39
|
+
* which is what was delivered last turn.
|
|
40
|
+
*/
|
|
41
|
+
import * as fs from "node:fs";
|
|
42
|
+
import { enrichFileFromLsp } from "./actionable-warnings.js";
|
|
43
|
+
import { bounded } from "./deadline-utils.js";
|
|
44
|
+
import { logLatency } from "./latency-logger.js";
|
|
45
|
+
import { touchCompletedConfirmationPolicy, } from "./lsp/diagnostic-binding.js";
|
|
46
|
+
import { findAuxiliaryProfileForSource } from "./dispatch/auxiliary-lsp.js";
|
|
47
|
+
/** At most this many files are re-observed per turn_end (the drift backstop's
|
|
48
|
+
* own 4-resyncs-per-pass precedent). */
|
|
49
|
+
export const MAX_REVERIFY_FILES = 4;
|
|
50
|
+
/** Wall-clock budget for the whole pass; a touch that cannot answer inside
|
|
51
|
+
* its remaining slice is recorded unconfirmed, never waited out. */
|
|
52
|
+
export const REVERIFY_BUDGET_MS = 3000;
|
|
53
|
+
/** The touch options the `source=lsp` probe uses, with this pass's source. */
|
|
54
|
+
const TOUCH_SOURCE = "persistent_reverify";
|
|
55
|
+
/**
|
|
56
|
+
* Select the report's re-verify candidates: DEFERRED-origin file entries (the
|
|
57
|
+
* only population the in-band publish carries forward, so the only population
|
|
58
|
+
* that re-serves across turns — F6: with no skip guard, so every carried
|
|
59
|
+
* entry re-verifies each turn) whose file stat is UNCHANGED since the entry's
|
|
60
|
+
* own observation stamp. A file that changed is skipped and counted — the
|
|
61
|
+
* edit path already re-observed it. Missing files are skipped too: the delta
|
|
62
|
+
* freshness gate drops them outright.
|
|
63
|
+
*/
|
|
64
|
+
export function selectPersistentReverifyFiles(report, nowMs = Date.now()) {
|
|
65
|
+
const candidates = [];
|
|
66
|
+
let skippedChanged = 0;
|
|
67
|
+
for (const entry of report.files) {
|
|
68
|
+
if (entry.origin !== "deferred")
|
|
69
|
+
continue;
|
|
70
|
+
// F3 (population half): the pass re-observes through a PRIMARY-scope
|
|
71
|
+
// touch, so an auxiliary-lane finding can never be re-confirmed — an
|
|
72
|
+
// entry carrying aux-sourced warnings is skipped (kept verbatim),
|
|
73
|
+
// never scored dropped. The aux lookup is the one shared seam.
|
|
74
|
+
if (entry.warnings.some((w) => findAuxiliaryProfileForSource(w.source ?? "") !== undefined)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const stamp = entry.generatedAt ? Date.parse(entry.generatedAt) : NaN;
|
|
78
|
+
if (!Number.isFinite(stamp) || stamp > nowMs)
|
|
79
|
+
continue;
|
|
80
|
+
let mtimeMs;
|
|
81
|
+
try {
|
|
82
|
+
mtimeMs = fs.statSync(entry.filePath).mtimeMs;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (mtimeMs > stamp) {
|
|
88
|
+
skippedChanged += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (candidates.length < MAX_REVERIFY_FILES)
|
|
92
|
+
candidates.push(entry);
|
|
93
|
+
}
|
|
94
|
+
return { candidates, skippedChanged };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run the bounded re-verify pass over the report's candidates and build the
|
|
98
|
+
* replacement entries for the caller's single in-band publish. Never throws:
|
|
99
|
+
* an unexpected per-file failure degrades that file to `unconfirmed` (kept
|
|
100
|
+
* verbatim, labeled), which is the shape-48 direction — the harm reaching the
|
|
101
|
+
* user is a hidden real finding, worse than re-delivering a stale one.
|
|
102
|
+
*/
|
|
103
|
+
export async function runPersistentReverify(args) {
|
|
104
|
+
const started = Date.now();
|
|
105
|
+
const deadlineAt = started + (args.budgetMs ?? REVERIFY_BUDGET_MS);
|
|
106
|
+
const nowMs = args.nowMs ?? started;
|
|
107
|
+
const { candidates, skippedChanged } = selectPersistentReverifyFiles(args.report, nowMs);
|
|
108
|
+
const outcomes = [];
|
|
109
|
+
const replacementFiles = [];
|
|
110
|
+
let touched = 0;
|
|
111
|
+
// F2: the producer's own pipeline (enrichFileFromLsp) — constructed once,
|
|
112
|
+
// reused per candidate. deltaOnly: false (a re-observation is not gated on
|
|
113
|
+
// this turn's modified ranges — the file was not edited).
|
|
114
|
+
const buildArgs = {
|
|
115
|
+
cwd: args.cwd,
|
|
116
|
+
sessionId: args.report.sessionId,
|
|
117
|
+
turnIndex: 0,
|
|
118
|
+
files: [],
|
|
119
|
+
modifiedRangesByFile: new Map(),
|
|
120
|
+
dispatchWarnings: [],
|
|
121
|
+
includeLspCodeActions: true,
|
|
122
|
+
deltaOnly: false,
|
|
123
|
+
...(args.signal !== undefined ? { signal: args.signal } : {}),
|
|
124
|
+
lspBudgetMs: REVERIFY_BUDGET_MS,
|
|
125
|
+
dbg: () => { },
|
|
126
|
+
};
|
|
127
|
+
// R3-4: every candidate the budget or the abort cut, so the phase row
|
|
128
|
+
// distinguishes "the pass ran out of time" from "the server answered
|
|
129
|
+
// nothing". Counted from what the loop actually reached, never predicted.
|
|
130
|
+
let processed = 0;
|
|
131
|
+
for (const entry of candidates) {
|
|
132
|
+
if (args.signal?.aborted || Date.now() >= deadlineAt)
|
|
133
|
+
break;
|
|
134
|
+
processed += 1;
|
|
135
|
+
let content;
|
|
136
|
+
try {
|
|
137
|
+
content = fs.readFileSync(entry.filePath, "utf-8");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
outcomes.push({
|
|
141
|
+
filePath: entry.filePath,
|
|
142
|
+
displayPath: entry.displayPath,
|
|
143
|
+
outcome: "unconfirmed",
|
|
144
|
+
dropped: 0,
|
|
145
|
+
kept: entry.warnings.length,
|
|
146
|
+
});
|
|
147
|
+
replacementFiles.push({ ...entry, reVerifyIncomplete: true });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// F4: the touch is bound-wrapped — `maxClientWaitMs` bounds the
|
|
151
|
+
// spawn/ready window, `bounded()` is the request ceiling.
|
|
152
|
+
let touchedResult;
|
|
153
|
+
try {
|
|
154
|
+
touchedResult = await bounded(args.lspService.touchFile(entry.filePath, content, {
|
|
155
|
+
diagnostics: "document",
|
|
156
|
+
collectDiagnostics: true,
|
|
157
|
+
source: TOUCH_SOURCE,
|
|
158
|
+
clientScope: "primary",
|
|
159
|
+
}), {
|
|
160
|
+
ms: Math.max(250, deadlineAt - Date.now()),
|
|
161
|
+
signal: args.signal,
|
|
162
|
+
hook: "turn_end",
|
|
163
|
+
label: "persistent_reverify_touch",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// The never-throws contract: a wedged touch degrades to unconfirmed
|
|
168
|
+
// (kept verbatim, labeled), never an escaped error.
|
|
169
|
+
touchedResult = undefined;
|
|
170
|
+
}
|
|
171
|
+
// F3 (#3176): an empty result is clean ONLY when the touch COMPLETED
|
|
172
|
+
// its confirmation policy — the silent-on-clean rule (#240/#533/#1253).
|
|
173
|
+
// A bare `{diags: []}` (the house double's silent empty, a tier-3
|
|
174
|
+
// server still analyzing) or a `skipReason` is UNCONFIRMED: the carried
|
|
175
|
+
// warnings are kept verbatim and the render labels the gap.
|
|
176
|
+
// `touched.inconclusive` is resolveTouchVerdict's output, computed
|
|
177
|
+
// service-side, and is honored directly.
|
|
178
|
+
// R3-3: read the policy through `touchCompletedConfirmationPolicy`, the
|
|
179
|
+
// #1470 seam, NOT the literal `confirmation !== "confirmed"` that
|
|
180
|
+
// `diagnostic-binding.ts:274-288` exists to forbid: `partial` means
|
|
181
|
+
// every server except the named cut-off auxiliaries answered, so the
|
|
182
|
+
// primary's silent-clean gates ran to completion and its observation is
|
|
183
|
+
// as trustworthy as a full confirmation. The literal labels a file the
|
|
184
|
+
// primary fully answered and keeps a finding the touch just re-derived.
|
|
185
|
+
if (touchedResult === undefined ||
|
|
186
|
+
touchedResult.inconclusive === true ||
|
|
187
|
+
touchedResult.skipReason !== undefined ||
|
|
188
|
+
(touchedResult.diagnosticsUnsupportedServerIds ?? []).length > 0 ||
|
|
189
|
+
!touchCompletedConfirmationPolicy(touchedResult)) {
|
|
190
|
+
outcomes.push({
|
|
191
|
+
filePath: entry.filePath,
|
|
192
|
+
displayPath: entry.displayPath,
|
|
193
|
+
outcome: "unconfirmed",
|
|
194
|
+
dropped: 0,
|
|
195
|
+
kept: entry.warnings.length,
|
|
196
|
+
});
|
|
197
|
+
replacementFiles.push({ ...entry, reVerifyIncomplete: true });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
touched += 1;
|
|
201
|
+
// F2: the touch's collected diagnostics feed the producer's pipeline
|
|
202
|
+
// as the cached arm — no second pull, the producer's filters and
|
|
203
|
+
// caps are reused verbatim.
|
|
204
|
+
// F4: the outer await is bound-wrapped too — the module carries ZERO
|
|
205
|
+
// syntactic unbounded awaits (#2523 slice-3 worklist), and the hard
|
|
206
|
+
// ceiling composes with enrichFileFromLsp's own internal deadline.
|
|
207
|
+
const freshRecords = (await bounded(enrichFileFromLsp(args.cwd, buildArgs, { filePath: entry.filePath, cached: touchedResult.diags }, {
|
|
208
|
+
lspService: args.lspService,
|
|
209
|
+
pullTimeoutMs: 2000,
|
|
210
|
+
deadlineAt,
|
|
211
|
+
...(args.signal !== undefined ? { signal: args.signal } : {}),
|
|
212
|
+
site: { hook: "turn_end", label: "persistent_reverify" },
|
|
213
|
+
}), {
|
|
214
|
+
ms: Math.max(250, deadlineAt - Date.now()),
|
|
215
|
+
signal: args.signal,
|
|
216
|
+
hook: "turn_end",
|
|
217
|
+
label: "persistent_reverify_enrich",
|
|
218
|
+
})) ?? [];
|
|
219
|
+
let dropped = 0;
|
|
220
|
+
let kept = 0;
|
|
221
|
+
for (const warning of entry.warnings) {
|
|
222
|
+
const stillThere = freshRecords.some((record) => record.rule === warning.rule && record.message === warning.message);
|
|
223
|
+
if (stillThere)
|
|
224
|
+
kept += 1;
|
|
225
|
+
else
|
|
226
|
+
dropped += 1;
|
|
227
|
+
}
|
|
228
|
+
const outcome = freshRecords.length === 0
|
|
229
|
+
? "clean"
|
|
230
|
+
: dropped === 0
|
|
231
|
+
? "reconfirmed"
|
|
232
|
+
: "mixed";
|
|
233
|
+
outcomes.push({
|
|
234
|
+
filePath: entry.filePath,
|
|
235
|
+
displayPath: entry.displayPath,
|
|
236
|
+
outcome,
|
|
237
|
+
dropped,
|
|
238
|
+
kept,
|
|
239
|
+
});
|
|
240
|
+
replacementFiles.push({
|
|
241
|
+
...entry,
|
|
242
|
+
warnings: freshRecords,
|
|
243
|
+
reVerified: true,
|
|
244
|
+
generatedAt: new Date(nowMs).toISOString(),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
logLatency({
|
|
248
|
+
type: "phase",
|
|
249
|
+
toolName: "turn_end",
|
|
250
|
+
filePath: args.cwd,
|
|
251
|
+
phase: "persistent_reverify",
|
|
252
|
+
durationMs: Date.now() - started,
|
|
253
|
+
metadata: {
|
|
254
|
+
candidates: candidates.length,
|
|
255
|
+
touched,
|
|
256
|
+
clean: outcomes.filter((o) => o.outcome === "clean").length,
|
|
257
|
+
reconfirmed: outcomes.filter((o) => o.outcome === "reconfirmed").length,
|
|
258
|
+
mixed: outcomes.filter((o) => o.outcome === "mixed").length,
|
|
259
|
+
unconfirmed: outcomes.filter((o) => o.outcome === "unconfirmed").length,
|
|
260
|
+
skippedChanged,
|
|
261
|
+
skippedBudget: candidates.length - processed,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
outcomes,
|
|
266
|
+
replacementFiles,
|
|
267
|
+
candidates: candidates.length,
|
|
268
|
+
touched,
|
|
269
|
+
skippedChanged,
|
|
270
|
+
};
|
|
271
|
+
}
|
package/dist/clients/pipeline.js
CHANGED
|
@@ -25,6 +25,7 @@ import { loadDispatchIntegration } from "./dispatch/lazy.js";
|
|
|
25
25
|
import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
|
|
26
26
|
import { classifyProbeFailure, createAvailabilityChecker, createAvailabilityLatch, describeProbeEvidence, logAvailabilityDecision, startHostStallSampler, resolveAvailableOrInstall, resolveCommandArgsWithInstallFallback, resolveToolCommand, resolveToolCommandWithInstallFallback, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
27
27
|
import { findDetektConfig } from "./dispatch/runners/detekt.js";
|
|
28
|
+
import { formatDiagnostics } from "./dispatch/utils/format-utils.js";
|
|
28
29
|
import { detectFileKind, getFileKindLabel } from "./file-kinds.js";
|
|
29
30
|
import { detectFileChangedAfterCommand, getProjectIgnoreMatcher, isExcludedDirName, } from "./file-utils.js";
|
|
30
31
|
import { logLatency } from "./latency-logger.js";
|
|
@@ -40,12 +41,11 @@ import { probeToolAsync } from "./tool-probe.js";
|
|
|
40
41
|
import { bounded } from "./deadline-utils.js";
|
|
41
42
|
import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
|
|
42
43
|
import { enabledAuxiliaryLspServerIds } from "./dispatch/auxiliary-lsp.js";
|
|
43
|
-
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
44
|
+
import { incrementDegradationCount, recordDegradationOnce, } from "./degradation-ledger.js";
|
|
44
45
|
import { establishToolAgreement } from "./tool-agreement.js";
|
|
45
46
|
import { dropFindingsForMissingPaths } from "./advisory-provenance.js";
|
|
46
47
|
import { getAutofixPolicyForFile, getPreferredAutofixTools, getRubocopCommand, hasBiomeConfig, hasDetektConfig, hasEslintConfig, hasGolangciConfig, hasKtfmtConfig, hasKtlintConfig, hasOxlintConfig, hasRubocopConfig, hasSqlfluffConfig, hasStylelintConfig, markdownlintConfigArgs, } from "./tool-policy.js";
|
|
47
|
-
|
|
48
|
-
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
48
|
+
import { exceedsLspSyncLimits } from "./lsp/content-limits.js";
|
|
49
49
|
const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
|
|
50
50
|
const AUTOFIX_CHANGED_FILE_SCAN_LIMIT = 5000;
|
|
51
51
|
/**
|
|
@@ -60,10 +60,6 @@ function lspSyncBudgetMs() {
|
|
|
60
60
|
const raw = Number(process.env.PI_LENS_LSP_SYNC_BUDGET_MS);
|
|
61
61
|
return Number.isFinite(raw) && raw > 0 ? raw : 3000;
|
|
62
62
|
}
|
|
63
|
-
// Scan one directory's entries into `snapshot`, pushing walkable subdirs onto
|
|
64
|
-
// `stack`. Extracted from the walk loop to keep each function's cognitive
|
|
65
|
-
// complexity low. Excluded/ignored dirs are not descended; ignored/vanished
|
|
66
|
-
// files are skipped.
|
|
67
63
|
// Files stat'd between event-loop yields. The walk stays on the tool_result
|
|
68
64
|
// hot path; yielding every N keeps its longest synchronous stretch well under
|
|
69
65
|
// the <50ms hook-burst budget even at the AUTOFIX_CHANGED_FILE_SCAN_LIMIT cap.
|
|
@@ -144,23 +140,6 @@ async function diffProjectSnapshot(root, before) {
|
|
|
144
140
|
}
|
|
145
141
|
return [...changed].sort((a, b) => a.localeCompare(b));
|
|
146
142
|
}
|
|
147
|
-
function exceedsLspSyncLimits(_filePath, content) {
|
|
148
|
-
const sizeBytes = Buffer.byteLength(content, "utf-8");
|
|
149
|
-
if (sizeBytes > LSP_MAX_FILE_BYTES) {
|
|
150
|
-
return {
|
|
151
|
-
tooLarge: true,
|
|
152
|
-
reason: `${Math.round(sizeBytes / 1024)}KB exceeds ${Math.round(LSP_MAX_FILE_BYTES / 1024)}KB`,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
const lineCount = content.split("\n").length;
|
|
156
|
-
if (lineCount > LSP_MAX_FILE_LINES) {
|
|
157
|
-
return {
|
|
158
|
-
tooLarge: true,
|
|
159
|
-
reason: `${lineCount} lines exceeds ${LSP_MAX_FILE_LINES}`,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
return { tooLarge: false, reason: "" };
|
|
163
|
-
}
|
|
164
143
|
function createPhaseTracker(toolName, filePath) {
|
|
165
144
|
const phases = [];
|
|
166
145
|
return {
|
|
@@ -677,7 +656,13 @@ export async function resyncLspFile(filePath, fileContent, needsContentRefresh,
|
|
|
677
656
|
return;
|
|
678
657
|
if (!needsContentRefresh && lspSyncCompleted)
|
|
679
658
|
return;
|
|
680
|
-
|
|
659
|
+
// #3405 r2: the bound is `clients/lsp/content-limits.ts` now, shared with the
|
|
660
|
+
// dispatch runner and the didSave payload. THIS call stays: a file past the
|
|
661
|
+
// bound is not synced to the server AT ALL, which is a stricter policy than
|
|
662
|
+
// the save seam's (drop the redundant text, still send the save) and the one
|
|
663
|
+
// a post-write sync owes — removing it would start writing whole-file
|
|
664
|
+
// didOpen frames for files this pipeline has always refused.
|
|
665
|
+
const limitCheck = exceedsLspSyncLimits(fileContent);
|
|
681
666
|
if (limitCheck.tooLarge)
|
|
682
667
|
return;
|
|
683
668
|
try {
|
|
@@ -711,6 +696,11 @@ export async function resyncLspFile(filePath, fileContent, needsContentRefresh,
|
|
|
711
696
|
source: "lsp_sync",
|
|
712
697
|
clientScope: "primary",
|
|
713
698
|
maxClientWaitMs: LSP_SPAWN_BUDGET_MS,
|
|
699
|
+
// #3405: pi-lens just wrote these bytes to disk, so this is the one
|
|
700
|
+
// touch that is a save. A save-triggered server (Expert recompiles
|
|
701
|
+
// the project on didSave and on nothing else) publishes for the edit
|
|
702
|
+
// only because of this flag.
|
|
703
|
+
saved: true,
|
|
714
704
|
})
|
|
715
705
|
.then(() => "done")
|
|
716
706
|
.catch((err) => {
|
|
@@ -923,6 +913,15 @@ export async function runFormatPhase(filePath, getFormatService, dbg, signal, bu
|
|
|
923
913
|
* → const randomInt = Math.floor(result);
|
|
924
914
|
*/
|
|
925
915
|
function buildEnrichedBlockerOutput(blockers, fileContent = "") {
|
|
916
|
+
// #3188: no blockers, no banner. The delivery gate above can retract every
|
|
917
|
+
// blocker (all cited files deleted), and the header interpolates the count,
|
|
918
|
+
// so without this the agent reads "🔴 STOP — 0 issue(s) must be fixed:" with
|
|
919
|
+
// nothing under it. Same guard, same reason, as `formatDiagnostics` in
|
|
920
|
+
// dispatch/utils/format-utils.ts — it belongs to the renderer, not to each
|
|
921
|
+
// call site, because the #2028 branch below cannot gate on the count without
|
|
922
|
+
// falling through to the UNGATED raw output.
|
|
923
|
+
if (blockers.length === 0)
|
|
924
|
+
return "";
|
|
926
925
|
// Empty fileContent (readback failed, e.g. deleted-file race) still
|
|
927
926
|
// renders the gated blocker list - just without per-line snippets.
|
|
928
927
|
const fileLines = fileContent ? fileContent.split("\n") : [];
|
|
@@ -952,6 +951,14 @@ function buildEnrichedBlockerOutput(blockers, fileContent = "") {
|
|
|
952
951
|
export async function runPipeline(ctx, deps) {
|
|
953
952
|
const { filePath, cwd, toolName, getFlag, getFlagSource, dbg } = ctx;
|
|
954
953
|
const { getFormatService } = deps;
|
|
954
|
+
const allowAutonomousWriters = ctx.allowAutonomousWriters !== false;
|
|
955
|
+
if (!allowAutonomousWriters) {
|
|
956
|
+
incrementDegradationCount({
|
|
957
|
+
kind: "opaque-mutation-ownership-boundary",
|
|
958
|
+
subject: "pipeline",
|
|
959
|
+
reason: `observed mutation is not evidence of agent authorship (${filePath})`,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
955
962
|
admitWidgetDiagnosticsWrite(filePath, ctx.telemetry?.writeIndex);
|
|
956
963
|
const phase = createPhaseTracker(toolName, filePath);
|
|
957
964
|
const pipelineStart = Date.now();
|
|
@@ -975,8 +982,14 @@ export async function runPipeline(ctx, deps) {
|
|
|
975
982
|
const piChangedFiles = new Set();
|
|
976
983
|
const autoformatDisabled = !!getFlag("no-autoformat", filePath);
|
|
977
984
|
const immediateFormat = !!getFlag("immediate-format");
|
|
978
|
-
const formatDeferred =
|
|
979
|
-
|
|
985
|
+
const formatDeferred = allowAutonomousWriters &&
|
|
986
|
+
!autoformatDisabled &&
|
|
987
|
+
!immediateFormat &&
|
|
988
|
+
!!fileContent;
|
|
989
|
+
if (allowAutonomousWriters &&
|
|
990
|
+
!autoformatDisabled &&
|
|
991
|
+
immediateFormat &&
|
|
992
|
+
fileContent) {
|
|
980
993
|
const formatResult = await runFormatPhase(filePath, getFormatService, dbg, ctx.signal);
|
|
981
994
|
formatChanged = formatResult.formatChanged;
|
|
982
995
|
formattersUsed = formatResult.formattersUsed;
|
|
@@ -1001,6 +1014,9 @@ export async function runPipeline(ctx, deps) {
|
|
|
1001
1014
|
else if (formatDeferred) {
|
|
1002
1015
|
dbg(`autoformat: deferred until agent_end for ${filePath}`);
|
|
1003
1016
|
}
|
|
1017
|
+
else if (!allowAutonomousWriters) {
|
|
1018
|
+
dbg(`autoformat: skipped for ${filePath} (mutation ownership is unproven)`);
|
|
1019
|
+
}
|
|
1004
1020
|
else if (autoformatDisabled) {
|
|
1005
1021
|
const source = getFlagSource?.("no-autoformat", filePath);
|
|
1006
1022
|
dbg(`autoformat: skipped for ${filePath} (--no-autoformat${source ? `, source=${source}` : ""})`);
|
|
@@ -1018,7 +1034,11 @@ export async function runPipeline(ctx, deps) {
|
|
|
1018
1034
|
let autofixChangedFiles = [];
|
|
1019
1035
|
let fixRefresh = false;
|
|
1020
1036
|
let autofixSkipReason;
|
|
1021
|
-
if (
|
|
1037
|
+
if (!allowAutonomousWriters) {
|
|
1038
|
+
autofixSkipReason = "mutation_ownership_unproven";
|
|
1039
|
+
dbg(`autofix: skipped for ${filePath} (mutation ownership is unproven)`);
|
|
1040
|
+
}
|
|
1041
|
+
else if (ctx.autofixMode === "deferred") {
|
|
1022
1042
|
autofixSkipReason = "deferred_to_agent_end";
|
|
1023
1043
|
dbg(`autofix: deferred until agent_end for ${filePath}`);
|
|
1024
1044
|
}
|
|
@@ -1157,34 +1177,22 @@ export async function runPipeline(ctx, deps) {
|
|
|
1157
1177
|
});
|
|
1158
1178
|
}
|
|
1159
1179
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1180
|
+
// Opaque recovery is a valid analysis input, but its bytes are not an agent
|
|
1181
|
+
// authored surface. Keep diagnostics and advisory output visible while
|
|
1182
|
+
// withholding the edit-directed channels that would tell the agent to mutate
|
|
1183
|
+
// the observed artifact (#3226).
|
|
1184
|
+
const hasBlockers = allowAutonomousWriters && dispatchResult.hasBlockers;
|
|
1185
|
+
const actionableWarnings = allowAutonomousWriters
|
|
1186
|
+
? dispatchResult.warnings
|
|
1187
|
+
.map((diagnostic) => recordFromDispatchDiagnostic(diagnostic, cwd))
|
|
1188
|
+
.filter((warning) => Boolean(warning))
|
|
1189
|
+
: [];
|
|
1164
1190
|
const codeQualityWarnings = dispatchResult.warnings
|
|
1165
1191
|
.map((diagnostic) => recordFromCodeQualityDiagnostic(diagnostic, cwd))
|
|
1166
1192
|
.filter((warning) => Boolean(warning));
|
|
1167
1193
|
if (dispatchResult.diagnostics.length > 0) {
|
|
1168
|
-
const logger = getDiagnosticLogger();
|
|
1169
1194
|
const tracker = getDiagnosticTracker();
|
|
1170
1195
|
tracker.trackShown(dispatchResult.diagnostics);
|
|
1171
|
-
const toKey = (d) => [
|
|
1172
|
-
d.tool || "",
|
|
1173
|
-
d.id || "",
|
|
1174
|
-
d.rule || "",
|
|
1175
|
-
d.filePath || "",
|
|
1176
|
-
d.line || 0,
|
|
1177
|
-
d.column || 0,
|
|
1178
|
-
].join("|");
|
|
1179
|
-
const inlineKeys = new Set([...dispatchResult.blockers, ...dispatchResult.fixed].map(toKey));
|
|
1180
|
-
for (const d of dispatchResult.diagnostics) {
|
|
1181
|
-
logger.logCaught(d, {
|
|
1182
|
-
model: ctx.telemetry?.model ?? "unknown",
|
|
1183
|
-
sessionId: ctx.telemetry?.sessionId ?? "unknown",
|
|
1184
|
-
turnIndex: ctx.telemetry?.turnIndex ?? 0,
|
|
1185
|
-
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
1186
|
-
}, inlineKeys.has(toKey(d)));
|
|
1187
|
-
}
|
|
1188
1196
|
}
|
|
1189
1197
|
if (fixedCount > 0)
|
|
1190
1198
|
getDiagnosticTracker().trackAutoFixed(fixedCount);
|
|
@@ -1198,15 +1206,48 @@ export async function runPipeline(ctx, deps) {
|
|
|
1198
1206
|
// the deleted file's content), so it is dropped here rather than re-asserted.
|
|
1199
1207
|
// One bounded stat per unique cited path, only when blockers exist — zero
|
|
1200
1208
|
// cost on the clean/fast paths.
|
|
1201
|
-
const deliverableBlockers =
|
|
1209
|
+
const deliverableBlockers = hasBlockers
|
|
1202
1210
|
? dropFindingsForMissingPaths({
|
|
1203
1211
|
store: "stop-blocker",
|
|
1204
1212
|
findings: dispatchResult.blockers,
|
|
1205
1213
|
cwd,
|
|
1206
1214
|
citedPath: (b) => b.filePath || undefined,
|
|
1207
1215
|
})
|
|
1208
|
-
:
|
|
1209
|
-
|
|
1216
|
+
: [];
|
|
1217
|
+
// #3190: ONE gate, applied once here, for EVERY surface. The durable record
|
|
1218
|
+
// this function hands `runtime.recordInlineBlockers` (and through it the
|
|
1219
|
+
// turn-end `Unresolved from this turn` re-serve) used to be built from the
|
|
1220
|
+
// UNGATED `dispatchResult.blockerOutput`/`blockers`, so a blocker retracted
|
|
1221
|
+
// from the tool result above was re-delivered at turn end — and the ungated
|
|
1222
|
+
// verdict still latched the commit gate through `updateGitGuardStatus`. It is
|
|
1223
|
+
// exactly `hasBlockers && nothing-survived-the-gate` inverted: the gate
|
|
1224
|
+
// returns `[]` whenever `hasBlockers` is false, so this one expression is the
|
|
1225
|
+
// whole condition.
|
|
1226
|
+
const hasDeliverableBlockers = deliverableBlockers.length > 0;
|
|
1227
|
+
if (dispatchResult.diagnostics.length > 0) {
|
|
1228
|
+
const logger = getDiagnosticLogger();
|
|
1229
|
+
const toKey = (d) => [
|
|
1230
|
+
d.tool || "",
|
|
1231
|
+
d.id || "",
|
|
1232
|
+
d.rule || "",
|
|
1233
|
+
d.filePath || "",
|
|
1234
|
+
d.line || 0,
|
|
1235
|
+
d.column || 0,
|
|
1236
|
+
].join("|");
|
|
1237
|
+
const inlineKeys = new Set([...deliverableBlockers, ...dispatchResult.fixed].map(toKey));
|
|
1238
|
+
for (const d of dispatchResult.diagnostics) {
|
|
1239
|
+
logger.logCaught(d, {
|
|
1240
|
+
model: ctx.telemetry?.model ?? "unknown",
|
|
1241
|
+
sessionId: ctx.telemetry?.sessionId ?? "unknown",
|
|
1242
|
+
turnIndex: ctx.telemetry?.turnIndex ?? 0,
|
|
1243
|
+
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
1244
|
+
}, inlineKeys.has(toKey(d)));
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const deliveredOutput = !allowAutonomousWriters && dispatchResult.hasBlockers
|
|
1248
|
+
? dispatchResult.output.slice(dispatchResult.blockerOutput.length)
|
|
1249
|
+
: dispatchResult.output;
|
|
1250
|
+
if (hasBlockers && fileContent) {
|
|
1210
1251
|
// Enrich blocker output with a code snippet so the agent can see the
|
|
1211
1252
|
// exact line it wrote that caused each violation — no re-read needed.
|
|
1212
1253
|
if (deliverableBlockers.length > 0) {
|
|
@@ -1218,20 +1259,25 @@ export async function runPipeline(ctx, deps) {
|
|
|
1218
1259
|
if (rest)
|
|
1219
1260
|
output += rest;
|
|
1220
1261
|
}
|
|
1221
|
-
else if (
|
|
1262
|
+
else if (deliveredOutput) {
|
|
1222
1263
|
// #2028 review P3: this path fires when readback FAILED - raw output
|
|
1223
1264
|
// still cites possibly-deleted files. Re-render from the gated set
|
|
1224
1265
|
// (no snippets without fileContent) and keep the post-blocker slice.
|
|
1225
|
-
if (
|
|
1266
|
+
if (hasBlockers &&
|
|
1226
1267
|
deliverableBlockers.length !== dispatchResult.blockers.length) {
|
|
1227
1268
|
let gatedOut = buildEnrichedBlockerOutput(deliverableBlockers);
|
|
1228
1269
|
const rest = dispatchResult.output.slice(dispatchResult.blockerOutput.length);
|
|
1229
1270
|
if (rest)
|
|
1230
1271
|
gatedOut += rest;
|
|
1231
|
-
|
|
1272
|
+
// #3188: every blocker retracted and nothing after the blocker
|
|
1273
|
+
// section leaves nothing to say — appending the separator alone would
|
|
1274
|
+
// still deliver a (whitespace-only) tool-result block, because
|
|
1275
|
+
// `handleToolResult` decides delivery on `output` truthiness.
|
|
1276
|
+
if (gatedOut)
|
|
1277
|
+
output += `\n\n${gatedOut}`;
|
|
1232
1278
|
}
|
|
1233
1279
|
else {
|
|
1234
|
-
output += `\n\n${
|
|
1280
|
+
output += `\n\n${deliveredOutput}`;
|
|
1235
1281
|
}
|
|
1236
1282
|
}
|
|
1237
1283
|
if (fixedCount > 0) {
|
|
@@ -1326,7 +1372,7 @@ export async function runPipeline(ctx, deps) {
|
|
|
1326
1372
|
turnIndex: ctx.telemetry?.turnIndex ?? 0,
|
|
1327
1373
|
writeIndex: ctx.telemetry?.writeIndex ?? 0,
|
|
1328
1374
|
diagnostics: dispatchResult.diagnostics,
|
|
1329
|
-
blockers: dispatchResult.blockers,
|
|
1375
|
+
blockers: hasBlockers ? dispatchResult.blockers : [],
|
|
1330
1376
|
warnings: dispatchResult.warnings,
|
|
1331
1377
|
fixed: dispatchResult.fixed,
|
|
1332
1378
|
resolvedCount: dispatchResult.resolvedCount,
|
|
@@ -1337,28 +1383,44 @@ export async function runPipeline(ctx, deps) {
|
|
|
1337
1383
|
});
|
|
1338
1384
|
return {
|
|
1339
1385
|
output,
|
|
1340
|
-
|
|
1386
|
+
// #3190: the verdict every consumer reads — `updateGitGuardStatus`'s commit
|
|
1387
|
+
// latch above all — is what was DELIVERED, not what the dispatch found. The
|
|
1388
|
+
// two disagreed whenever the deleted-path gate retracted every blocker: the
|
|
1389
|
+
// tool result said `✓ clean` while the commit gate stayed latched, its
|
|
1390
|
+
// summary falling back to that very all-clear line. `emitLensAnalysisComplete`
|
|
1391
|
+
// above deliberately keeps the raw dispatch counts — it records what was
|
|
1392
|
+
// observed, not what was served.
|
|
1393
|
+
hasBlockers: hasDeliverableBlockers,
|
|
1341
1394
|
cascadePromise,
|
|
1342
1395
|
isError: false,
|
|
1343
1396
|
fileModified,
|
|
1344
1397
|
postWriteStateHash,
|
|
1345
1398
|
changedFiles,
|
|
1346
|
-
|
|
1347
|
-
|
|
1399
|
+
// #3190: re-rendered from the GATED set with `formatDiagnostics(...,
|
|
1400
|
+
// "blocking")` — the very expression `dispatcher.ts:1409` builds
|
|
1401
|
+
// `blockerOutput` with, over a subset of the very array it rendered. When
|
|
1402
|
+
// the gate drops nothing the two are byte-identical (same renderer, same
|
|
1403
|
+
// array, same display cap), so an unretracted record is unchanged; when it
|
|
1404
|
+
// drops something the stored text can no longer disagree with the tool
|
|
1405
|
+
// result, and `inline-blocker-dispositions.ts`' re-render at turn end still
|
|
1406
|
+
// matches this string exactly.
|
|
1407
|
+
inlineBlockerSummary: hasDeliverableBlockers
|
|
1408
|
+
? formatDiagnostics(deliverableBlockers, "blocking").trim() || undefined
|
|
1348
1409
|
: undefined,
|
|
1349
1410
|
// #1561 F1: taken from the very diagnostics `blockerOutput` was rendered
|
|
1350
1411
|
// from, so the provenance can never disagree with the text it guards. An
|
|
1351
1412
|
// untagged diagnostic contributes the literal "unknown", which no verdict
|
|
1352
1413
|
// claims coverage for — it pins the entry rather than silently widening
|
|
1353
1414
|
// what an LSP check is allowed to clear.
|
|
1354
|
-
inlineBlockerSources:
|
|
1415
|
+
inlineBlockerSources: hasDeliverableBlockers
|
|
1355
1416
|
? [
|
|
1356
|
-
...new Set(
|
|
1417
|
+
...new Set(deliverableBlockers.map((d) => d.tool?.trim() || "unknown")),
|
|
1357
1418
|
]
|
|
1358
1419
|
: undefined,
|
|
1359
|
-
inlineBlockerLines:
|
|
1360
|
-
?
|
|
1361
|
-
// #1641 review F2:
|
|
1420
|
+
inlineBlockerLines: hasDeliverableBlockers
|
|
1421
|
+
? deliverableBlockers
|
|
1422
|
+
// #1641 review F2: the blocker array (#3190: the deliverable subset
|
|
1423
|
+
// of `dispatchResult.blockers`) is NOT guaranteed to be
|
|
1362
1424
|
// scoped to THIS file — a chart-wide runner (helm-lint, helm-render)
|
|
1363
1425
|
// reports blocking diagnostics against other files in the chart
|
|
1364
1426
|
// (e.g. `values.yaml`) alongside `ctx.filePath`. The precedent every
|
|
@@ -1404,8 +1466,15 @@ export async function runPipeline(ctx, deps) {
|
|
|
1404
1466
|
source: "autofix",
|
|
1405
1467
|
}
|
|
1406
1468
|
: undefined,
|
|
1407
|
-
inlineBlockerFileContent:
|
|
1469
|
+
inlineBlockerFileContent: hasBlockers
|
|
1408
1470
|
? inlineBlockerFileContent
|
|
1409
1471
|
: undefined,
|
|
1472
|
+
// #3246: the very array `blockerOutput` above was rendered from, so the
|
|
1473
|
+
// turn-end policy filter and its re-render can never disagree with the
|
|
1474
|
+
// text they guard — the same provenance argument `inlineBlockerSources`
|
|
1475
|
+
// makes one field up.
|
|
1476
|
+
inlineBlockerDiagnostics: hasDeliverableBlockers
|
|
1477
|
+
? deliverableBlockers
|
|
1478
|
+
: undefined,
|
|
1410
1479
|
};
|
|
1411
1480
|
}
|