pi-lens 4.2.0 → 4.2.1
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 +45 -0
- package/dist/clients/actionable-warnings.js +21 -9
- package/dist/clients/cascade-format.js +24 -0
- package/dist/clients/extension-log.js +48 -1
- package/dist/clients/finding-delivery-gate.js +11 -12
- package/dist/clients/lsp/cascade-tier.js +3 -0
- package/dist/clients/lsp/index.js +41 -7
- package/dist/clients/mcp/analyze.js +9 -2
- package/dist/clients/path-utils.js +51 -9
- package/dist/clients/persistent-reverify.js +271 -0
- package/dist/clients/pipeline.js +15 -1
- package/dist/clients/runtime-turn.js +124 -7
- package/dist/clients/skills-resolver.js +38 -2
- package/dist/clients/widget-state.js +152 -6
- package/dist/index.js +729 -344
- package/dist/tools/lens-diagnostic-mark.js +14 -2
- package/dist/tools/lens-diagnostics.js +84 -17
- package/dist/tools/lsp-diagnostics.js +32 -5
- package/package.json +1 -1
|
@@ -45,8 +45,9 @@ import * as path from "node:path";
|
|
|
45
45
|
import { Type } from "../clients/deps/typebox.js";
|
|
46
46
|
import { markDisposition, normalizeMessage, } from "../clients/diagnostic-dispositions.js";
|
|
47
47
|
import { insertSuppressComment } from "../clients/dispatch/suppress-writer.js";
|
|
48
|
-
import {
|
|
48
|
+
import { normalizeMapKey } from "../clients/path-utils.js";
|
|
49
49
|
import { resolveLensToolName } from "../clients/tool-config.js";
|
|
50
|
+
import { getFileDiagnostics, } from "../clients/widget-state.js";
|
|
50
51
|
const DISPOSITIONS = [
|
|
51
52
|
"false-positive",
|
|
52
53
|
"suppress",
|
|
@@ -70,11 +71,22 @@ function isBlank(text) {
|
|
|
70
71
|
* violation) are ambiguous; per #802 this picks the match CLOSEST to the
|
|
71
72
|
* caller's line rather than guessing — the conservative choice for suppress,
|
|
72
73
|
* which must never place a comment above the wrong site.
|
|
74
|
+
*
|
|
75
|
+
* #3160: `getFileDiagnostics` keys with `normalizeEphemeralMapKey` — case-
|
|
76
|
+
* preserving on POSIX by design (no filesystem I/O on that hot path) — but
|
|
77
|
+
* the widget's own writers key from `ctx.filePath`, which
|
|
78
|
+
* `createDispatchContext` already canonicalizes to the on-disk casing
|
|
79
|
+
* (#2016/#3098). `absPath` here is the caller's RAW spelling, so on a
|
|
80
|
+
* case-insensitive filesystem a mis-cased mark call must derive the same
|
|
81
|
+
* canonical key the writers used or it silently misses the live diagnostic
|
|
82
|
+
* and falls through to the fuzzy fallback below — do NOT case-fold
|
|
83
|
+
* `normalizeEphemeralMapKey` itself for this: on a case-sensitive filesystem
|
|
84
|
+
* that would merge two genuinely different files.
|
|
73
85
|
*/
|
|
74
86
|
function widgetCrossCheck(absPath, tool, rule, message, callerLine) {
|
|
75
87
|
let diagnostics;
|
|
76
88
|
try {
|
|
77
|
-
diagnostics = getFileDiagnostics(absPath);
|
|
89
|
+
diagnostics = getFileDiagnostics(normalizeMapKey(absPath));
|
|
78
90
|
}
|
|
79
91
|
catch {
|
|
80
92
|
return undefined;
|
|
@@ -18,13 +18,13 @@ import { DEPENDENCY_DRIFT_MAX_DELIVERIES } from "../clients/blocker-freshness.js
|
|
|
18
18
|
import { freshnessFromMtime } from "../clients/freshness.js";
|
|
19
19
|
import { applyFindingPolicy, loadProjectRulePolicyMap, } from "../clients/dispatch/finding-policy.js";
|
|
20
20
|
import { gateFindingsByPathFreshness } from "../clients/advisory-provenance.js";
|
|
21
|
-
import { markUnreconciledFindings } from "../clients/finding-delivery-gate.js";
|
|
21
|
+
import { formatCacheAgeLabel, markUnreconciledFindings, } from "../clients/finding-delivery-gate.js";
|
|
22
22
|
import { normalizeRuleId } from "../clients/dispatch/rule-id-normalize.js";
|
|
23
23
|
import { applyRulePolicy, } from "../clients/dispatch/rule-policy.js";
|
|
24
24
|
import { compactRenderResult } from "./render-compact.js";
|
|
25
25
|
import { combineAbortSignals } from "../clients/deadline-utils.js";
|
|
26
26
|
import { getProjectIgnoreMatcher } from "../clients/file-utils.js";
|
|
27
|
-
import { isAtOrAboveHomeDir, normalizeEphemeralMapKey, normalizeFilePath, realpathOrResolve, } from "../clients/path-utils.js";
|
|
27
|
+
import { isAtOrAboveHomeDir, normalizeEphemeralMapKey, normalizeFilePath, normalizeMapKey, realpathOrResolve, } from "../clients/path-utils.js";
|
|
28
28
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
29
29
|
import { retireInlineBlockerAndResyncGuard } from "../clients/git-guard.js";
|
|
30
30
|
import { primaryServerId, resolveLspCwdForFile, } from "../clients/lsp/config.js";
|
|
@@ -508,7 +508,7 @@ function formatProjectDeltaDiagnostic(diagnostic, stale) {
|
|
|
508
508
|
* rule/message), live -> unchanged. See `clients/finding-delivery-gate.ts`
|
|
509
509
|
* surface `lens-diagnostics:mode-delta`.
|
|
510
510
|
*/
|
|
511
|
-
function appendProjectDiagnosticsDeltaLines(
|
|
511
|
+
function appendProjectDiagnosticsDeltaLines(groups, cwd, report, severity, includeFile) {
|
|
512
512
|
const scoped = (report?.diagnostics ?? []).filter((diagnostic) => includeFile(diagnostic.filePath) &&
|
|
513
513
|
matchesSeverity(projectDiagnosticToWidget(diagnostic, diagnostic.filePath), severity));
|
|
514
514
|
const gated = gateFindingsByPathFreshness({
|
|
@@ -532,11 +532,9 @@ function appendProjectDiagnosticsDeltaLines(lines, cwd, report, severity, includ
|
|
|
532
532
|
byFile.set(filePath, bucket);
|
|
533
533
|
}
|
|
534
534
|
for (const [filePath, fileDiagnostics] of byFile) {
|
|
535
|
-
const
|
|
536
|
-
if (!lines.includes(rel))
|
|
537
|
-
lines.push(rel);
|
|
535
|
+
const group = getDeltaFileGroup(groups, cwd, filePath);
|
|
538
536
|
for (const diagnostic of fileDiagnostics) {
|
|
539
|
-
|
|
537
|
+
group.projectLines.push(formatProjectDeltaDiagnostic(diagnostic, staleSet.has(diagnostic)));
|
|
540
538
|
}
|
|
541
539
|
}
|
|
542
540
|
return diagnostics.length;
|
|
@@ -723,7 +721,15 @@ function applyDeltaFreshnessGate(files, cwd, generatedAt) {
|
|
|
723
721
|
}
|
|
724
722
|
for (const f of gated.stale) {
|
|
725
723
|
const arr = byFile.get(f.filePath) ?? [];
|
|
726
|
-
|
|
724
|
+
// Fix B (#3167): carry the stamp the row was judged against so the render
|
|
725
|
+
// can emit one age label per file group — the row's own observation stamp,
|
|
726
|
+
// not the report-level one (the #2504 r4 multi-stamp case).
|
|
727
|
+
arr.push({
|
|
728
|
+
...f.warning,
|
|
729
|
+
stale: true,
|
|
730
|
+
line: undefined,
|
|
731
|
+
staleAsOf: effectiveAt,
|
|
732
|
+
});
|
|
727
733
|
byFile.set(f.filePath, arr);
|
|
728
734
|
}
|
|
729
735
|
return files
|
|
@@ -733,10 +739,34 @@ function applyDeltaFreshnessGate(files, cwd, generatedAt) {
|
|
|
733
739
|
}))
|
|
734
740
|
.filter((file) => file.warnings.length > 0);
|
|
735
741
|
}
|
|
742
|
+
function getDeltaFileGroup(groups, cwd, filePath) {
|
|
743
|
+
const rel = path.relative(cwd, filePath);
|
|
744
|
+
let group = groups.get(rel);
|
|
745
|
+
if (!group) {
|
|
746
|
+
group = {
|
|
747
|
+
rel,
|
|
748
|
+
actionableLines: [],
|
|
749
|
+
qualityLines: [],
|
|
750
|
+
projectLines: [],
|
|
751
|
+
incomplete: false,
|
|
752
|
+
};
|
|
753
|
+
groups.set(rel, group);
|
|
754
|
+
}
|
|
755
|
+
return group;
|
|
756
|
+
}
|
|
736
757
|
// @delivery-surface: lens-diagnostics:mode-delta
|
|
737
758
|
function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
|
|
738
759
|
const actionableEntry = cacheManager.readCache("actionable-warnings", cwd);
|
|
739
760
|
const qualityEntry = cacheManager.readCache("code-quality-warnings", cwd);
|
|
761
|
+
// #3170: files whose re-verify pass could not complete inside its budget —
|
|
762
|
+
// their rows render as carried, plus this explicit gap label (never a
|
|
763
|
+
// false clean). Computed from the RAW actionable-warnings cache entries
|
|
764
|
+
// because the freshness pipeline below rebuilds the file shape. The
|
|
765
|
+
// re-verify only ever writes the actionable-warnings cache — the quality
|
|
766
|
+
// report's file shape carries no such marker.
|
|
767
|
+
const reverifyIncompletePaths = new Set((actionableEntry?.data?.files ?? [])
|
|
768
|
+
.filter((file) => file.reVerifyIncomplete)
|
|
769
|
+
.map((file) => normalizeMapKey(file.filePath)));
|
|
740
770
|
const actionable = actionableEntry?.data;
|
|
741
771
|
const quality = qualityEntry?.data;
|
|
742
772
|
// Project rule policy (`.pi-lens.json` `rules.<id>.disable` /
|
|
@@ -778,32 +808,69 @@ function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
|
|
|
778
808
|
warnings: matchingWarnings(file.warnings),
|
|
779
809
|
}))
|
|
780
810
|
.filter((file) => file.warnings.length > 0);
|
|
781
|
-
|
|
811
|
+
// #3196: one grouping pass keyed by file — every tier below appends into
|
|
812
|
+
// the SAME per-file group (see `DeltaFileGroup`/`getDeltaFileGroup`), so a
|
|
813
|
+
// file present in more than one report always renders its rows under its
|
|
814
|
+
// own header, wherever that header ends up in the final buffer.
|
|
815
|
+
const groups = new Map();
|
|
782
816
|
// Fixable warnings from actionable-warnings and quality cache entries retain
|
|
783
817
|
// their own severity tier. Apply the same threshold semantics as the LSP path.
|
|
818
|
+
// F5/F10 (#3168): at most ONE label group (age and/or #3170's re-verify
|
|
819
|
+
// gap) per file. The actionable tier is folded first, so its stale row (if
|
|
820
|
+
// any) wins the group's age label; the quality tier only supplies one when
|
|
821
|
+
// the actionable tier had none for that file (#3168 F10(c)'s quality-only
|
|
822
|
+
// shape). The `(re-verify incomplete)` flag is a fact about the file (from
|
|
823
|
+
// the raw actionable cache, independent of which tier's rows happen to
|
|
824
|
+
// render it), so it is recorded on the group the first time either tier
|
|
825
|
+
// touches that file and never predicted.
|
|
784
826
|
if (filteredActionableFiles.length > 0) {
|
|
785
827
|
for (const file of filteredActionableFiles) {
|
|
786
|
-
const
|
|
787
|
-
lines.push(`${rel}`);
|
|
828
|
+
const group = getDeltaFileGroup(groups, cwd, file.filePath);
|
|
788
829
|
for (const w of file.warnings) {
|
|
789
830
|
const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
|
|
790
|
-
|
|
831
|
+
group.actionableLines.push(` ⚠ ${where} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
|
|
832
|
+
}
|
|
833
|
+
const key = normalizeMapKey(file.filePath);
|
|
834
|
+
if (reverifyIncompletePaths.has(key))
|
|
835
|
+
group.incomplete = true;
|
|
836
|
+
if (!group.staleRow) {
|
|
837
|
+
const staleWarning = file.warnings.find((w) => w.stale);
|
|
838
|
+
if (staleWarning)
|
|
839
|
+
group.staleRow = { staleAsOf: staleWarning.staleAsOf };
|
|
791
840
|
}
|
|
792
841
|
}
|
|
793
842
|
}
|
|
794
843
|
// Quality issues
|
|
795
844
|
if (filteredQualityFiles.length > 0) {
|
|
796
845
|
for (const file of filteredQualityFiles) {
|
|
797
|
-
const
|
|
798
|
-
if (!lines.includes(rel))
|
|
799
|
-
lines.push(rel);
|
|
846
|
+
const group = getDeltaFileGroup(groups, cwd, file.filePath);
|
|
800
847
|
for (const w of file.warnings) {
|
|
801
848
|
const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
|
|
802
|
-
|
|
849
|
+
group.qualityLines.push(` ℹ ${where} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
|
|
850
|
+
}
|
|
851
|
+
const key = normalizeMapKey(file.filePath);
|
|
852
|
+
if (reverifyIncompletePaths.has(key))
|
|
853
|
+
group.incomplete = true;
|
|
854
|
+
if (!group.staleRow) {
|
|
855
|
+
const staleWarning = file.warnings.find((w) => w.stale);
|
|
856
|
+
if (staleWarning)
|
|
857
|
+
group.staleRow = { staleAsOf: staleWarning.staleAsOf };
|
|
803
858
|
}
|
|
804
859
|
}
|
|
805
860
|
}
|
|
806
|
-
const projectDeltaCount = appendProjectDiagnosticsDeltaLines(
|
|
861
|
+
const projectDeltaCount = appendProjectDiagnosticsDeltaLines(groups, cwd, projectDelta, severity, includeFile);
|
|
862
|
+
const lines = [];
|
|
863
|
+
for (const group of groups.values()) {
|
|
864
|
+
lines.push(group.rel);
|
|
865
|
+
lines.push(...group.actionableLines);
|
|
866
|
+
lines.push(...group.qualityLines);
|
|
867
|
+
lines.push(...group.projectLines);
|
|
868
|
+
if (group.staleRow) {
|
|
869
|
+
lines.push(` (${formatCacheAgeLabel(group.staleRow.staleAsOf)})`);
|
|
870
|
+
}
|
|
871
|
+
if (group.incomplete)
|
|
872
|
+
lines.push(" (re-verify incomplete)");
|
|
873
|
+
}
|
|
807
874
|
const selectedActionableFiles = filteredActionableFiles;
|
|
808
875
|
const selectedQualityFiles = filteredQualityFiles;
|
|
809
876
|
const aw = selectedActionableFiles.reduce((count, file) => count + file.warnings.length, 0);
|
|
@@ -23,7 +23,8 @@ import { classifyCascadeWaitTier } from "../clients/lsp/wait-policy/index.js";
|
|
|
23
23
|
import { attemptTsserverSyncDiagnostics } from "../clients/lsp/tsserver-sync.js";
|
|
24
24
|
import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
|
|
25
25
|
import { demoteInferredProjectDiagnostics } from "../clients/lsp/inferred-project.js";
|
|
26
|
-
import {
|
|
26
|
+
import { normalizeMapKey } from "../clients/path-utils.js";
|
|
27
|
+
import { countRetainedSuppressedRows, isBlocking, reconcileScanDiagnostics, } from "../clients/widget-state.js";
|
|
27
28
|
import { makeProgressReporter } from "./scan-progress.js";
|
|
28
29
|
import { isWarmAttached, tryWarmAttachedDiagnostics, } from "../clients/warm-attach.js";
|
|
29
30
|
import { extensionsForLanguage, SCAN_LANGUAGE_PRIORITY, } from "../clients/language-registry.js";
|
|
@@ -239,7 +240,18 @@ onConfirmedNoBlockers, getService = getLSPService) {
|
|
|
239
240
|
// Preserve input order (including duplicate entries); normalize each
|
|
240
241
|
// path before grouping so Windows separators and dot segments cannot
|
|
241
242
|
// change cache/group identity. Explicit lists never enter the walker.
|
|
242
|
-
|
|
243
|
+
// #3160/#3182: `path.resolve(cwd, entry)` folds dot segments (and
|
|
244
|
+
// separators) structurally — it ignores `cwd` whenever `entry` is
|
|
245
|
+
// already absolute, so the old `path.isAbsolute` ternary is
|
|
246
|
+
// redundant. `normalizeMapKey` then adopts on-disk CASING where the
|
|
247
|
+
// path exists — an agent-typed, possibly mis-cased `paths` entry
|
|
248
|
+
// must key `reconcileScanDiagnostics`'s widget-state write the same
|
|
249
|
+
// way a canonical writer (clients/pipeline.ts's ctx.filePath) or the
|
|
250
|
+
// #3160-fixed lens_diagnostic_mark reader would. `path.resolve`
|
|
251
|
+
// stays required for the relative→absolute step: `normalizeMapKey`
|
|
252
|
+
// folds dot segments itself since #3184, but never resolves against
|
|
253
|
+
// `cwd` (a cwd fold there broke every monorepo, #2490).
|
|
254
|
+
const absPaths = rawPaths.map((entry) => normalizeMapKey(path.resolve(cwd, entry)));
|
|
243
255
|
return runBatchFileDiagnostics(absPaths, severity, lspService, {
|
|
244
256
|
concurrency,
|
|
245
257
|
waitMs,
|
|
@@ -264,9 +276,12 @@ onConfirmedNoBlockers, getService = getLSPService) {
|
|
|
264
276
|
details: {},
|
|
265
277
|
};
|
|
266
278
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
// #3184: the single-`path` sibling of the `paths` batch above, and
|
|
280
|
+
// keyed the same way — `runFileDiagnostics` hands this straight to
|
|
281
|
+
// `reconcileScanDiagnostics`, whose `normalizeEphemeralMapKey` key
|
|
282
|
+
// derivation folds neither dot segments nor casing. Same expression as
|
|
283
|
+
// :444 so both modes of this tool write under ONE key per file.
|
|
284
|
+
const absPath = normalizeMapKey(path.resolve(cwd, rawPath));
|
|
270
285
|
let stat;
|
|
271
286
|
try {
|
|
272
287
|
stat = fs.statSync(absPath);
|
|
@@ -616,6 +631,17 @@ severity) {
|
|
|
616
631
|
// otherwise have SEEN, never one per finding (AGENTS.md "bounded
|
|
617
632
|
// observability"). `total` is the in-scope population for the same
|
|
618
633
|
// reason the count is, so the record and the rendered line agree.
|
|
634
|
+
//
|
|
635
|
+
// #3158 round 2 F4: `retainedSuppressed` is the widget store's
|
|
636
|
+
// SUCCESS-path number — how many suppressed rows the store is carrying
|
|
637
|
+
// INTO this scan, written by the scans before it. Until this, retention
|
|
638
|
+
// was observable only when its per-file cap truncated
|
|
639
|
+
// (`widget-suppressed-retention-capped`), so a healthy footer chip had no
|
|
640
|
+
// record behind it at all. Folded into the record this lane already emits
|
|
641
|
+
// rather than added as a second one: same cardinality (one per filtered
|
|
642
|
+
// file per scan, never one per row), no new sink, and the two numbers are
|
|
643
|
+
// read together — `suppressed` is what THIS scan dropped,
|
|
644
|
+
// `retainedSuppressed` is what the footer chip is still counting.
|
|
619
645
|
logLatency({
|
|
620
646
|
type: "phase",
|
|
621
647
|
toolName: "lsp_diagnostics",
|
|
@@ -625,6 +651,7 @@ severity) {
|
|
|
625
651
|
metadata: {
|
|
626
652
|
suppressed,
|
|
627
653
|
total: inScopeBefore,
|
|
654
|
+
retainedSuppressed: countRetainedSuppressedRows(file),
|
|
628
655
|
},
|
|
629
656
|
});
|
|
630
657
|
}
|