pi-lens 4.1.5 → 4.2.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 +415 -0
- package/README.md +13 -8
- package/config/dependency-cruiser-eager-allowlist.json +6 -1
- package/dist/clients/actionable-warnings.js +30 -4
- package/dist/clients/agent-behavior-client.js +2 -0
- package/dist/clients/analysed-root.js +1 -0
- package/dist/clients/ast-grep-tool-logger.js +1 -1
- package/dist/clients/bash-file-access.js +26 -0
- package/dist/clients/biome-client.js +13 -2
- package/dist/clients/blocker-freshness.js +403 -35
- package/dist/clients/bootstrap.js +6 -1
- package/dist/clients/bounded-telemetry.js +2 -0
- package/dist/clients/cache-observability.js +122 -1
- package/dist/clients/cascade-format.js +200 -4
- package/dist/clients/code-quality-warnings.js +10 -2
- package/dist/clients/complexity-client.js +5 -0
- package/dist/clients/config-diagnostic-codes.js +2 -0
- package/dist/clients/config-resolve.js +6 -3
- package/dist/clients/config-schema.js +17 -0
- package/dist/clients/config-warn.js +2 -2
- package/dist/clients/dead-code-client.js +8 -1
- package/dist/clients/degradation-ledger.js +16 -1
- package/dist/clients/dependency-checker.js +5 -1
- package/dist/clients/diagnostic-dispositions.js +12 -1
- package/dist/clients/dispatch/ast-grep-catalog.js +65 -0
- package/dist/clients/dispatch/auxiliary-lsp.js +28 -0
- package/dist/clients/dispatch/dispatcher.js +47 -17
- package/dist/clients/dispatch/facts/function-facts.js +1 -1
- package/dist/clients/dispatch/facts/import-facts.js +5 -1
- package/dist/clients/dispatch/finding-policy.js +222 -0
- package/dist/clients/dispatch/indent-detect.js +389 -13
- package/dist/clients/dispatch/integration.js +175 -25
- package/dist/clients/dispatch/rule-ignores.js +62 -0
- package/dist/clients/dispatch/runners/actionlint.js +2 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
- package/dist/clients/dispatch/runners/biome-check.js +8 -4
- package/dist/clients/dispatch/runners/cpp-check.js +4 -3
- package/dist/clients/dispatch/runners/credo.js +2 -1
- package/dist/clients/dispatch/runners/cue-vet.js +2 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +2 -1
- package/dist/clients/dispatch/runners/detekt.js +2 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +2 -1
- package/dist/clients/dispatch/runners/elixir-check.js +2 -1
- package/dist/clients/dispatch/runners/eslint.js +2 -1
- package/dist/clients/dispatch/runners/fish-indent.js +2 -1
- package/dist/clients/dispatch/runners/gleam-check.js +2 -1
- package/dist/clients/dispatch/runners/go-vet.js +2 -1
- package/dist/clients/dispatch/runners/golangci-lint.js +2 -1
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +3 -1
- package/dist/clients/dispatch/runners/helm-render.js +2 -1
- package/dist/clients/dispatch/runners/htmlhint.js +2 -1
- package/dist/clients/dispatch/runners/javac.js +2 -1
- package/dist/clients/dispatch/runners/ktlint.js +2 -1
- package/dist/clients/dispatch/runners/lsp.js +19 -1
- package/dist/clients/dispatch/runners/markdownlint.js +3 -2
- package/dist/clients/dispatch/runners/mypy.js +2 -1
- package/dist/clients/dispatch/runners/oxlint.js +2 -1
- package/dist/clients/dispatch/runners/php-lint.js +2 -1
- package/dist/clients/dispatch/runners/phpstan.js +2 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +2 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
- package/dist/clients/dispatch/runners/pyright.js +2 -1
- package/dist/clients/dispatch/runners/rubocop.js +2 -1
- package/dist/clients/dispatch/runners/ruff.js +2 -2
- package/dist/clients/dispatch/runners/rust-clippy.js +12 -11
- package/dist/clients/dispatch/runners/shellcheck.js +2 -1
- package/dist/clients/dispatch/runners/shfmt.js +4 -13
- package/dist/clients/dispatch/runners/spellcheck.js +2 -2
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +3 -2
- package/dist/clients/dispatch/runners/stylelint.js +3 -2
- package/dist/clients/dispatch/runners/swiftlint.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +2 -1
- package/dist/clients/dispatch/runners/tflint.js +2 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +2 -21
- package/dist/clients/dispatch/runners/trivy-config.js +2 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
- package/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
- package/dist/clients/dispatch/runners/vale.js +2 -1
- package/dist/clients/dispatch/runners/yamllint.js +4 -3
- package/dist/clients/dispatch/runners/zig-check.js +2 -1
- package/dist/clients/dispatch/types.js +4 -1
- package/dist/clients/disposition-publish.js +1 -0
- package/dist/clients/effective-config.js +12 -2
- package/dist/clients/extension-log.js +2 -0
- package/dist/clients/file-kinds.js +26 -1
- package/dist/clients/file-role.js +23 -6
- package/dist/clients/file-utils.js +156 -10
- package/dist/clients/finding-delivery-gate.js +60 -9
- package/dist/clients/format-service.js +39 -21
- package/dist/clients/formatters.js +113 -200
- package/dist/clients/generation-guard.js +6 -2
- package/dist/clients/git-guard.js +11 -3
- package/dist/clients/gitleaks-client.js +42 -6
- package/dist/clients/govulncheck-client.js +48 -7
- package/dist/clients/indent-retarget.js +78 -8
- package/dist/clients/installer/index.js +524 -161
- package/dist/clients/instance-reaper-state.js +7 -0
- package/dist/clients/instance-reaper.js +20 -10
- package/dist/clients/jscpd-client.js +9 -1
- package/dist/clients/knip-client.js +9 -2
- package/dist/clients/language-profile.js +35 -3
- package/dist/clients/language-registry.js +5 -1
- package/dist/clients/latency-logger.js +6 -1
- package/dist/clients/lens-config.js +4 -0
- package/dist/clients/lens-engine.js +12 -3
- package/dist/clients/lens-events.js +2 -1
- package/dist/clients/lens-flag-registry.js +79 -0
- package/dist/clients/live-bus-emitter.js +2 -1
- package/dist/clients/lsp/client.js +25 -21
- package/dist/clients/lsp/config.js +36 -7
- package/dist/clients/lsp/diagnostic-binding.js +24 -1
- package/dist/clients/lsp/document-drift.js +78 -0
- package/dist/clients/lsp/index.js +288 -45
- package/dist/clients/lsp/launch.js +16 -4
- package/dist/clients/lsp/server.js +79 -15
- package/dist/clients/lsp/workspace-diagnostics-cache.js +39 -5
- package/dist/clients/lsp-mutation.js +5 -3
- package/dist/clients/map-with-concurrency.js +12 -8
- package/dist/clients/mcp/analyze.js +3 -0
- package/dist/clients/mcp/session.js +38 -4
- package/dist/clients/ndjson-logger.js +24 -7
- package/dist/clients/observed-mutation.js +40 -2
- package/dist/clients/opaque-mutation-scan.js +23 -3
- package/dist/clients/opengrep-client.js +111 -14
- package/dist/clients/package-manager.js +2 -1
- package/dist/clients/path-utils.js +123 -3
- package/dist/clients/performance-report.js +5 -1
- package/dist/clients/php-cs-fixer-config.js +8 -3
- package/dist/clients/pipeline.js +77 -7
- package/dist/clients/project-diagnostics/cache.js +37 -5
- package/dist/clients/project-diagnostics/fresh-fetch.js +173 -18
- package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
- package/dist/clients/project-diagnostics/scanner.js +59 -3
- package/dist/clients/project-lens-config.js +48 -2
- package/dist/clients/read-guard-logger.js +2 -0
- package/dist/clients/read-guard.js +70 -11
- package/dist/clients/resource-sampler.js +147 -9
- package/dist/clients/review-graph-logger.js +2 -0
- package/dist/clients/ruff-client.js +18 -9
- package/dist/clients/runtime-agent-end.js +42 -5
- package/dist/clients/runtime-context.js +6 -2
- package/dist/clients/runtime-coordinator.js +82 -7
- package/dist/clients/runtime-session.js +33 -5
- package/dist/clients/runtime-tool-call.js +43 -12
- package/dist/clients/runtime-tool-result.js +334 -86
- package/dist/clients/runtime-turn.js +195 -18
- package/dist/clients/safe-spawn.js +258 -5
- package/dist/clients/scratch-tree-policy.js +6 -0
- package/dist/clients/security-scan-client.js +2 -2
- package/dist/clients/session-event-guard.js +119 -6
- package/dist/clients/sg-runner.js +25 -12
- package/dist/clients/sgconfig.js +79 -2
- package/dist/clients/situational-tool-telemetry.js +108 -0
- package/dist/clients/test-runner-client.js +258 -78
- package/dist/clients/test-runner-delivery.js +43 -0
- package/dist/clients/tool-agreement.js +423 -0
- package/dist/clients/tool-config.js +249 -0
- package/dist/clients/tool-cwd.js +270 -0
- package/dist/clients/tool-policy.js +163 -7
- package/dist/clients/tool-probe.js +53 -0
- package/dist/clients/tool-set-policy.js +42 -0
- package/dist/clients/tree-sitter-client.js +120 -0
- package/dist/clients/tree-sitter-query-loader.js +127 -189
- package/dist/clients/trivy-client.js +9 -1
- package/dist/clients/turn-context.js +52 -0
- package/dist/clients/widget-state.js +102 -3
- package/dist/clients/word-index.js +5 -2
- package/dist/clients/write-ordering-guard.js +7 -0
- package/dist/index.js +26968 -21741
- package/dist/mcp/analyze-cli.js +3 -2
- package/dist/mcp/server.js +140 -161
- package/dist/tools/activate-tools.js +10 -7
- package/dist/tools/ast-grep-outline.js +2 -13
- package/dist/tools/ast-grep-replace.js +8 -13
- package/dist/tools/ast-grep-search.js +83 -39
- package/dist/tools/effective-config.js +4 -3
- package/dist/tools/lens-diagnostic-mark.js +8 -11
- package/dist/tools/lens-diagnostics.js +516 -133
- package/dist/tools/lsp-diagnostics.js +265 -182
- package/dist/tools/lsp-navigation.js +20 -44
- package/dist/tools/module-report.js +5 -9
- package/dist/tools/project-report.js +9 -17
- package/dist/tools/render-compact.js +280 -1
- package/dist/tools/symbol-search.js +2 -2
- package/docs/agent-guide.md +3 -4
- package/docs/agent-tools.md +46 -22
- package/docs/audit1.md +5 -5
- package/docs/configuration.md +9 -0
- package/docs/dependencies.md +11 -0
- package/docs/dispositions.md +52 -0
- package/docs/environment-variables.md +2 -7
- package/docs/features.md +7 -7
- package/docs/globalconfig.md +14 -2
- package/docs/language-coverage.md +1 -0
- package/docs/mcp.md +13 -1
- package/docs/pi-lens-fixer.md +48 -18
- package/docs/pi-lens-investigator.md +38 -18
- package/docs/pi-lens-monitor.md +88 -0
- package/docs/pi-lens-retro.md +97 -0
- package/docs/pi-lens-reviewer.md +53 -31
- package/docs/pi-lens-subagent.md +12 -4
- package/docs/public-api-stability.md +1 -0
- package/docs/real-harness.md +46 -0
- package/docs/release-qa-baseline.md +27 -1
- package/docs/servercapabilities.md +1 -6
- package/docs/settings.md +44 -6
- package/docs/tools_improvement2.md +4 -4
- package/docs/usage.md +15 -3
- package/package.json +10 -1
- package/rules/tree-sitter-queries/typescript/sql-injection.yml +23 -7
- package/scripts/analyze-pi-lens-logs.mjs +40 -0
- package/skills/pi-lens-ast-grep/SKILL.md +23 -6
- package/skills/pi-lens-lsp-navigation/SKILL.md +26 -9
- package/dist/tools/ast-dump.js +0 -103
|
@@ -176,9 +176,50 @@ function newAttributionState() {
|
|
|
176
176
|
injectedCharsSinceLastUsage: 0,
|
|
177
177
|
newTranscriptCharsSinceLastUsage: 0,
|
|
178
178
|
attributionCharsCapped: false,
|
|
179
|
+
injectedBytes: emptyInjectedBytes(),
|
|
180
|
+
injectedFindingsRepeated: 0,
|
|
181
|
+
findingIdentities: new Set(),
|
|
182
|
+
findingIdentityCapRecorded: false,
|
|
183
|
+
turnEndAdvisoryBytes: 0,
|
|
184
|
+
toolResultBytes: 0,
|
|
185
|
+
toolResultsTruncated: 0,
|
|
179
186
|
summary: newCacheUsageSummary(),
|
|
180
187
|
};
|
|
181
188
|
}
|
|
189
|
+
const MAX_FINDING_IDENTITIES = 2_000;
|
|
190
|
+
function emptyInjectedBytes() {
|
|
191
|
+
return {
|
|
192
|
+
sessionGuidance: 0,
|
|
193
|
+
turnFindings: 0,
|
|
194
|
+
testFindings: 0,
|
|
195
|
+
agentNudge: 0,
|
|
196
|
+
turnEndAdvisory: 0,
|
|
197
|
+
other: 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function sourceKey(source) {
|
|
201
|
+
return {
|
|
202
|
+
"session-guidance": "sessionGuidance",
|
|
203
|
+
"turn-findings": "turnFindings",
|
|
204
|
+
"test-findings": "testFindings",
|
|
205
|
+
"agent-nudge": "agentNudge",
|
|
206
|
+
}[source];
|
|
207
|
+
}
|
|
208
|
+
/** Best-effort, language-neutral identity extraction from rendered findings. */
|
|
209
|
+
function findingIdentities(text) {
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const line of text.split(/\r?\n/)) {
|
|
212
|
+
const location = line.match(/(?:^|\s)([^\s:]+):(\d+)(?::\d+)?/);
|
|
213
|
+
if (!location)
|
|
214
|
+
continue;
|
|
215
|
+
const rule = line.match(/(?:rule|code)\s*[:=]\s*([\w./-]+)/i)?.[1] ??
|
|
216
|
+
line.match(/\(([\w./-]+)\)\s*$/)?.[1] ??
|
|
217
|
+
line.match(/:\d+(?::\d+)?\s+([\w./-]+)/)?.[1];
|
|
218
|
+
if (rule)
|
|
219
|
+
out.push(`${location[1]}:${location[2]}:${rule}`);
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
182
223
|
/**
|
|
183
224
|
* Fetch (creating if absent) one session's attribution state, refreshing LRU
|
|
184
225
|
* recency and evicting the oldest entries past the cap — same bounding as
|
|
@@ -688,10 +729,37 @@ export function observeCacheContext(args) {
|
|
|
688
729
|
countsCapped: sliceSize.capped,
|
|
689
730
|
};
|
|
690
731
|
});
|
|
732
|
+
const state = attributionFor(attributionKey(args.sessionId, args.sessionRole));
|
|
733
|
+
for (const slice of injectionSlices) {
|
|
734
|
+
const key = sourceKey(slice.source);
|
|
735
|
+
const bytes = measureInjectedMessages(slice.messages).bytes;
|
|
736
|
+
state.injectedBytes[key] += bytes;
|
|
737
|
+
for (const message of slice.messages) {
|
|
738
|
+
for (const identity of findingIdentities(typeof message.content === "string" ? message.content : "")) {
|
|
739
|
+
if (state.findingIdentities.has(identity)) {
|
|
740
|
+
state.injectedFindingsRepeated += 1;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (state.findingIdentities.size >= MAX_FINDING_IDENTITIES) {
|
|
744
|
+
if (!state.findingIdentityCapRecorded) {
|
|
745
|
+
state.findingIdentityCapRecorded = true;
|
|
746
|
+
logLatency({
|
|
747
|
+
type: "phase",
|
|
748
|
+
filePath: "<pi-lens>",
|
|
749
|
+
phase: "injected_finding_identity_cap",
|
|
750
|
+
durationMs: 0,
|
|
751
|
+
metadata: { cap: MAX_FINDING_IDENTITIES },
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
state.findingIdentities.add(identity);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
691
760
|
const messageCountCapped = existingMessages.length > MAX_REPORTED_MESSAGES ||
|
|
692
761
|
resultMessages.length > MAX_REPORTED_MESSAGES;
|
|
693
762
|
recordContextAttribution(attributionKey(args.sessionId, args.sessionRole), existingMessages, sizes);
|
|
694
|
-
const state = attributionFor(attributionKey(args.sessionId, args.sessionRole));
|
|
695
763
|
state.sequenceHashTruncatedSinceLastUsage ||=
|
|
696
764
|
beforeSequence.truncated ||
|
|
697
765
|
beforeSequence.contentTruncated ||
|
|
@@ -721,6 +789,10 @@ export function observeCacheContext(args) {
|
|
|
721
789
|
injectedMessageCountCapped: injectedMessages.length > MAX_REPORTED_MESSAGES,
|
|
722
790
|
injectedChars: sizes.chars,
|
|
723
791
|
injectedBytes: sizes.bytes,
|
|
792
|
+
injectedBytesBySource: Object.fromEntries(sourceBreakdown.map((entry) => [
|
|
793
|
+
sourceKey(entry.source),
|
|
794
|
+
entry.bytes,
|
|
795
|
+
])),
|
|
724
796
|
injectedEstimatedTokens: estimateTokens(sizes.chars),
|
|
725
797
|
injectedTokenBasis: "chars-per-token-4-estimate-not-provider-measured",
|
|
726
798
|
injectedCountsCapped: sizes.capped,
|
|
@@ -881,6 +953,13 @@ export function logCacheUsage(message, dbg, context) {
|
|
|
881
953
|
const injectedCharsSinceLastTurn = state.injectedCharsSinceLastUsage;
|
|
882
954
|
const newTranscriptCharsSinceLastTurn = state.newTranscriptCharsSinceLastUsage;
|
|
883
955
|
const attributionCharsCapped = state.attributionCharsCapped;
|
|
956
|
+
const injectedBytes = {
|
|
957
|
+
...state.injectedBytes,
|
|
958
|
+
turnEndAdvisory: state.turnEndAdvisoryBytes + state.injectedBytes.turnEndAdvisory,
|
|
959
|
+
};
|
|
960
|
+
const injectedFindingsRepeated = state.injectedFindingsRepeated;
|
|
961
|
+
const toolResultBytes = state.toolResultBytes;
|
|
962
|
+
const toolResultsTruncated = state.toolResultsTruncated;
|
|
884
963
|
// This record is the turn boundary: reset the per-turn accumulators and
|
|
885
964
|
// re-arm the prefix-break flag so the next verdict describes the NEXT gap.
|
|
886
965
|
state.lastUsageAtMs = nowMs;
|
|
@@ -898,6 +977,11 @@ export function logCacheUsage(message, dbg, context) {
|
|
|
898
977
|
state.injectedCharsSinceLastUsage = 0;
|
|
899
978
|
state.newTranscriptCharsSinceLastUsage = 0;
|
|
900
979
|
state.attributionCharsCapped = false;
|
|
980
|
+
state.injectedBytes = emptyInjectedBytes();
|
|
981
|
+
state.injectedFindingsRepeated = 0;
|
|
982
|
+
state.turnEndAdvisoryBytes = 0;
|
|
983
|
+
state.toolResultBytes = 0;
|
|
984
|
+
state.toolResultsTruncated = 0;
|
|
901
985
|
// Clear the request stamp too: the next turn measures from ITS request, and
|
|
902
986
|
// a turn whose `context` call pi-lens never saw must fall back rather than
|
|
903
987
|
// reuse this one.
|
|
@@ -939,6 +1023,13 @@ export function logCacheUsage(message, dbg, context) {
|
|
|
939
1023
|
injectedCharsSinceLastTurn,
|
|
940
1024
|
newTranscriptCharsSinceLastTurn,
|
|
941
1025
|
attributionCharsCapped,
|
|
1026
|
+
injectedBytes,
|
|
1027
|
+
injectedFindingsRepeated,
|
|
1028
|
+
// Delivered tool-result aggregation over the turn (#2800 item 7):
|
|
1029
|
+
// the sum of each delivered footer's bytes= figure and the count
|
|
1030
|
+
// of results whose bound fired.
|
|
1031
|
+
toolResultBytes,
|
|
1032
|
+
toolResultsTruncated,
|
|
942
1033
|
...(context
|
|
943
1034
|
? {
|
|
944
1035
|
// MessageEndEvent has no request/context id in the host API. These
|
|
@@ -1126,6 +1217,29 @@ export function clearCachePrefixSession(sessionId, sessionRole) {
|
|
|
1126
1217
|
// a stale gap baseline and a stale prefix-break flag.
|
|
1127
1218
|
attributionBySession.delete(key);
|
|
1128
1219
|
}
|
|
1220
|
+
/** Record the exact quiet-window summary content handed to pi.sendMessage. */
|
|
1221
|
+
export function recordTurnEndAdvisoryBytes(sessionId, bytes, sessionRole = "primary") {
|
|
1222
|
+
const state = attributionFor(attributionKey(sessionId, sessionRole));
|
|
1223
|
+
state.turnEndAdvisoryBytes = Math.min(MAX_INJECTED_BYTES, state.turnEndAdvisoryBytes + Math.max(0, bytes));
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Fold one delivered tool result into the per-turn aggregation the
|
|
1227
|
+
* `cache_usage` row reports as `toolResultBytes` / `toolResultsTruncated`
|
|
1228
|
+
* (#2800 item 7). `bytes` is the delivered payload's UTF-8 byte count as
|
|
1229
|
+
* stamped in the result's own footer, and `truncated` is the footer's
|
|
1230
|
+
* truncated flag. Called from each host adapter's delivery seam; the row is
|
|
1231
|
+
* emitted by `logCacheUsage`, which resets both figures at the turn boundary.
|
|
1232
|
+
* The byte sum saturates at the attribution bound so the row stays bounded.
|
|
1233
|
+
*/
|
|
1234
|
+
export function recordToolResultDelivery(args) {
|
|
1235
|
+
const state = attributionFor(attributionKey(args.sessionId, args.sessionRole));
|
|
1236
|
+
const bytes = Number.isFinite(args.bytes)
|
|
1237
|
+
? Math.max(0, Math.floor(args.bytes))
|
|
1238
|
+
: 0;
|
|
1239
|
+
state.toolResultBytes = Math.min(MAX_ATTRIBUTION_BYTES, state.toolResultBytes + bytes);
|
|
1240
|
+
if (args.truncated === true)
|
|
1241
|
+
state.toolResultsTruncated += 1;
|
|
1242
|
+
}
|
|
1129
1243
|
/**
|
|
1130
1244
|
* Emit one bounded session summary and retire its attribution state. The
|
|
1131
1245
|
* summary contains fixed-key counters only; it never serializes transcript or
|
|
@@ -1165,6 +1279,13 @@ export function resetCachePrefixObservation() {
|
|
|
1165
1279
|
prefixHashBySession.clear();
|
|
1166
1280
|
attributionBySession.clear();
|
|
1167
1281
|
}
|
|
1282
|
+
export function resetCacheFindingIdentitiesSession(sessionId, sessionRole) {
|
|
1283
|
+
const state = attributionBySession.get(attributionKey(sessionId, sessionRole));
|
|
1284
|
+
if (!state)
|
|
1285
|
+
return;
|
|
1286
|
+
state.findingIdentities.clear();
|
|
1287
|
+
state.findingIdentityCapRecorded = false;
|
|
1288
|
+
}
|
|
1168
1289
|
export function _attributionBySessionHasForTests(key) {
|
|
1169
1290
|
return attributionBySession.has(key);
|
|
1170
1291
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { retagAuxiliaryDiagnostics } from "./dispatch/auxiliary-lsp.js";
|
|
3
|
+
import { applyFindingPolicy, loadProjectRulePolicyMap, renderedRuleIdentities, } from "./dispatch/finding-policy.js";
|
|
4
|
+
import { detectFileRole } from "./file-role.js";
|
|
5
|
+
import { logLatency } from "./latency-logger.js";
|
|
1
6
|
import { convertLspDiagnostics } from "./dispatch/utils/lsp-diagnostics.js";
|
|
2
7
|
import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
|
|
3
8
|
export function formatCascadeNeighborDiagnostics(cwd, neighbors, options = {}) {
|
|
@@ -60,20 +65,64 @@ export function formatCascadeNeighborDiagnostics(cwd, neighbors, options = {}) {
|
|
|
60
65
|
* Lives here rather than inline in the quiet-window callback so the delivery
|
|
61
66
|
* path (reconcile → run → turn_end) is testable end to end; index.ts only wires
|
|
62
67
|
* it to `runtime.appendCascadeRun`.
|
|
68
|
+
*
|
|
69
|
+
* #3102: the survivors are what the agent READS, so they go through the same
|
|
70
|
+
* `clients/dispatch/finding-policy.ts` stack — inline `pi-lens-ignore` → stored
|
|
71
|
+
* dispositions → the project's `.pi-lens.json` rule policy — that the per-edit
|
|
72
|
+
* dispatcher, `mode=full` and the `source=lsp` probe lane apply. Without it a
|
|
73
|
+
* cold-neighbour ERROR the agent already marked `false-positive` came back on
|
|
74
|
+
* every quiet-window reconcile.
|
|
63
75
|
*/
|
|
64
76
|
export function buildResolvedFoundCascadeRun(cwd, neighbor) {
|
|
65
77
|
const { filePath } = neighbor;
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
78
|
+
const errors = neighbor.diagnostics.filter((d) => d.severity === 1);
|
|
79
|
+
if (errors.length === 0)
|
|
68
80
|
return undefined;
|
|
81
|
+
const policyStart = Date.now();
|
|
82
|
+
// This lane's roots are one and the same: index.ts hands `runtime.projectRoot`
|
|
83
|
+
// in as `cwd`, so the retag's config lookup and the disposition/rule-policy
|
|
84
|
+
// store read the same directory. The in-lane cascade's do NOT — see
|
|
85
|
+
// `applyCascadeDisplayPolicy`.
|
|
86
|
+
const { diagnostics, suppressed, auxSuppressed, total } = applyCascadeDisplayPolicy(errors, {
|
|
87
|
+
cwd,
|
|
88
|
+
policyRoot: cwd,
|
|
89
|
+
filePath,
|
|
90
|
+
});
|
|
91
|
+
// One bounded record per RUN, never one per finding (AGENTS.md "bounded
|
|
92
|
+
// observability"). This is a PUSH surface: silence after a mark is the mark
|
|
93
|
+
// working, not a clean verdict, so the count is recorded here rather than
|
|
94
|
+
// re-announced to the agent on every reconcile. `durationMs` covers the
|
|
95
|
+
// content read too — the latency this fold added to the quiet-window
|
|
96
|
+
// callback, measurable in the same per-phase record as every other cost.
|
|
97
|
+
recordCascadeFindingPolicy({
|
|
98
|
+
filePath,
|
|
99
|
+
durationMs: Date.now() - policyStart,
|
|
100
|
+
suppressed,
|
|
101
|
+
total,
|
|
102
|
+
auxSuppressed,
|
|
103
|
+
});
|
|
104
|
+
// No zero-length early return here: `formatCascadeNeighborDiagnostics`
|
|
105
|
+
// renders "" for a neighbour with no diagnostics and the `!formatted` guard
|
|
106
|
+
// below already returns `undefined` for it — a second check was mutation-
|
|
107
|
+
// inert (M7: deleting it left all 7 cascade cases green).
|
|
69
108
|
const neighbors = [
|
|
70
109
|
{ filePath, reason: "references", diagnostics, lspTouched: true },
|
|
71
110
|
];
|
|
72
|
-
const
|
|
111
|
+
const rendered = formatCascadeNeighborDiagnostics(cwd, neighbors, {
|
|
73
112
|
noun: "cold neighbor",
|
|
74
113
|
});
|
|
75
|
-
if (!
|
|
114
|
+
if (!rendered)
|
|
76
115
|
return undefined;
|
|
116
|
+
// #1616 / #3102 AC 4, round 2 F2: a delivery that still has something to say
|
|
117
|
+
// states what it dropped, once per delivery — the same sentence the
|
|
118
|
+
// late-auxiliary advisory renders (`clients/runtime-turn.ts`). Policy drops
|
|
119
|
+
// only: an aux drop is the file's own suppression comment, which the
|
|
120
|
+
// per-edit dispatch path honours silently too, and it stays in the record
|
|
121
|
+
// above. A delivery with NOTHING left says nothing at all — silence on a
|
|
122
|
+
// push surface is not a claim that the neighbour is clean.
|
|
123
|
+
const formatted = suppressed > 0
|
|
124
|
+
? `${rendered}\nsuppressed by disposition: ${suppressed} finding(s) (marked false-positive or won't-fix).`
|
|
125
|
+
: rendered;
|
|
77
126
|
return {
|
|
78
127
|
filePath,
|
|
79
128
|
result: {
|
|
@@ -93,3 +142,150 @@ export function buildResolvedFoundCascadeRun(cwd, neighbor) {
|
|
|
93
142
|
diagnosticCount: diagnostics.length,
|
|
94
143
|
};
|
|
95
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* The ONE convert → retag → `applyFindingPolicy` derivation every cascade
|
|
147
|
+
* DISPLAY lane runs before its diagnostics reach the agent (#3102/#3157).
|
|
148
|
+
*
|
|
149
|
+
* Five lanes render `CascadeNeighborResult.diagnostics` through
|
|
150
|
+
* `formatCascadeNeighborDiagnostics`: the quiet-window builder above, and the
|
|
151
|
+
* four in-lane sites in `clients/dispatch/integration.ts` (passive cold
|
|
152
|
+
* snapshot, fresh touch, touch-error fallback, degraded fallback). Each one
|
|
153
|
+
* used to open-code `convertLspDiagnostics(...)` and render the result raw, so
|
|
154
|
+
* a neighbour ERROR the agent had marked `false-positive` was hidden by
|
|
155
|
+
* `mode=delta`/`mode=full`/the per-edit dispatcher/the probe lane and STILL
|
|
156
|
+
* re-reported by the cascade block on every edit that cascaded to it (#3157).
|
|
157
|
+
*
|
|
158
|
+
* TWO ROOTS, deliberately separate (the #1030 rule):
|
|
159
|
+
* - `cwd` is the LANGUAGE root, and only `retagAuxiliaryDiagnostics` sees it
|
|
160
|
+
* — `profile.allowBlocking(cwd)` looks for a local opengrep/zizmor/typos
|
|
161
|
+
* config, exactly as the per-edit runner (`runners/lsp.ts`) passes
|
|
162
|
+
* `ctx.cwd`.
|
|
163
|
+
* - `policyRoot` is the PROJECT root, and the disposition store and the
|
|
164
|
+
* `.pi-lens.json` rule map are read from it — `lens_diagnostic_mark` writes
|
|
165
|
+
* under `runtime.projectRoot` (`index.ts`), so reading from a nested
|
|
166
|
+
* language root in a monorepo opens a different
|
|
167
|
+
* `diagnostic-dispositions.json` and silently no-ops every mark. This is
|
|
168
|
+
* what `dispatcher.ts` already does with `ctx.projectRoot ?? ctx.cwd`.
|
|
169
|
+
*
|
|
170
|
+
* The auxiliary drop set (native `# nosemgrep`-style comments, `skipTestFiles`)
|
|
171
|
+
* is KEPT: every caller's diagnostics come straight off a client cache or a
|
|
172
|
+
* `touchFile` result, so nothing upstream ran `applyAuxiliarySuppressions`
|
|
173
|
+
* (only the `mode=full` workspace sweep does, `clients/lsp/index.ts`) and this
|
|
174
|
+
* is the FIRST application, not a double-apply — which is why the probe lane
|
|
175
|
+
* ignores it and these lanes do not.
|
|
176
|
+
*
|
|
177
|
+
* No `range.start.line` pre-partition here, unlike the late-auxiliary drain,
|
|
178
|
+
* whose own comment gives the alignment reason (`clients/runtime-turn.ts`,
|
|
179
|
+
* above its `anchored` filter): `convertLspDiagnostics` drops line-less
|
|
180
|
+
* entries, which would break the 1:1 index pairing `retagAuxiliaryDiagnostics`
|
|
181
|
+
* needs. That reason holds here too — it is simply already satisfied. Every
|
|
182
|
+
* caller reads the same `client.getAllDiagnostics()` / `touchFile` map, whose
|
|
183
|
+
* `mergeDiagnosticLists` (`clients/lsp/client.ts`) dereferences
|
|
184
|
+
* `diagnostic.range.start.line` unguarded, so a line-less entry throws long
|
|
185
|
+
* before any builder sees it and `converted` is always 1:1 with `errors`.
|
|
186
|
+
*
|
|
187
|
+
* Reads nothing when there is nothing to render, and never logs: the RECORD is
|
|
188
|
+
* the caller's decision, because the in-lane lane walks up to
|
|
189
|
+
* `CASCADE_NEIGHBOUR_BUDGET` neighbours per edit and must aggregate one row per
|
|
190
|
+
* run rather than one per neighbour.
|
|
191
|
+
*/
|
|
192
|
+
export function applyCascadeDisplayPolicy(
|
|
193
|
+
/** ERROR-severity raw diagnostics, already capped by the caller's own
|
|
194
|
+
* display cap (`MAX_PER_FILE` in-lane; uncapped for the single-neighbour
|
|
195
|
+
* quiet-window run). */
|
|
196
|
+
errors, options) {
|
|
197
|
+
if (errors.length === 0) {
|
|
198
|
+
return { diagnostics: [], suppressed: 0, auxSuppressed: 0, total: 0 };
|
|
199
|
+
}
|
|
200
|
+
const { cwd, policyRoot, filePath } = options;
|
|
201
|
+
// `undefined` → the fail-open empty string: inline suppression becomes a
|
|
202
|
+
// no-op and a STRICT `false-positive` anchor hashes an empty line, so a
|
|
203
|
+
// finding is never hidden by an I/O error (AGENTS.md shape 48).
|
|
204
|
+
const content = options.content ?? readNeighborContent(filePath);
|
|
205
|
+
const converted = convertLspDiagnostics(errors, filePath);
|
|
206
|
+
// #692/#3046: identity comes from the ONE shared derivation every other
|
|
207
|
+
// surface anchors a mark against — never a hardcoded `tool: "lsp"`.
|
|
208
|
+
const retained = retagAuxiliaryDiagnostics(converted, errors, content ?? "", {
|
|
209
|
+
cwd,
|
|
210
|
+
fileRole: detectFileRole(filePath, content),
|
|
211
|
+
});
|
|
212
|
+
const { kept } = applyFindingPolicy(retained, {
|
|
213
|
+
cwd: policyRoot,
|
|
214
|
+
filePath,
|
|
215
|
+
content: content ?? "",
|
|
216
|
+
policyMap: loadProjectRulePolicyMap(policyRoot),
|
|
217
|
+
identities: renderedRuleIdentities,
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
diagnostics: options.displayCap === undefined
|
|
221
|
+
? kept
|
|
222
|
+
: kept.slice(0, options.displayCap),
|
|
223
|
+
// Policy drops only, DISJOINT from `auxSuppressed` — the same split the
|
|
224
|
+
// `late_auxiliary_findings` record uses, so one operator reading both
|
|
225
|
+
// records does not have to know that one nests and the other does not.
|
|
226
|
+
suppressed: retained.length - kept.length,
|
|
227
|
+
auxSuppressed: converted.length - retained.length,
|
|
228
|
+
total: converted.length,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The ONE `cascade_finding_policy` row both cascade display lanes write.
|
|
233
|
+
*
|
|
234
|
+
* Gated on EITHER counter (#3102 round 2, F1): gating on the policy count alone
|
|
235
|
+
* made an aux-only drop — an ERROR master rendered, removed by the profile's own
|
|
236
|
+
* `# nosemgrep` / `skipTestFiles` rule — vanish with no row at all, the
|
|
237
|
+
* silent-drop shape this record exists to prevent (AGENTS.md shape 10). The
|
|
238
|
+
* `late_auxiliary_findings` twin reports both counters every drain.
|
|
239
|
+
*/
|
|
240
|
+
export function recordCascadeFindingPolicy(entry) {
|
|
241
|
+
const inputTruncated = entry.inputTruncated ?? 0;
|
|
242
|
+
if (entry.suppressed === 0 &&
|
|
243
|
+
entry.auxSuppressed === 0 &&
|
|
244
|
+
inputTruncated === 0)
|
|
245
|
+
return;
|
|
246
|
+
logLatency({
|
|
247
|
+
type: "phase",
|
|
248
|
+
toolName: "cascade",
|
|
249
|
+
filePath: entry.filePath,
|
|
250
|
+
phase: "cascade_finding_policy",
|
|
251
|
+
durationMs: entry.durationMs,
|
|
252
|
+
metadata: {
|
|
253
|
+
suppressed: entry.suppressed,
|
|
254
|
+
total: entry.total,
|
|
255
|
+
auxSuppressed: entry.auxSuppressed,
|
|
256
|
+
...(inputTruncated > 0 && { inputTruncated }),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The neighbour's current bytes, for the two content-bound halves of the
|
|
262
|
+
* policy stack (inline `pi-lens-ignore` and the STRICT `false-positive`
|
|
263
|
+
* anchor). One synchronous read, paid only once a neighbour actually has ERROR
|
|
264
|
+
* diagnostics to render — the same read `mode=full` pays per flagged file and
|
|
265
|
+
* the probe lane pays per cache replay.
|
|
266
|
+
*
|
|
267
|
+
* SYNC here, while the in-lane cascade's own neighbour read at
|
|
268
|
+
* `clients/dispatch/integration.ts` (the active touch, under its "A6: async
|
|
269
|
+
* read to avoid blocking event loop on network-mounted drives" comment) is
|
|
270
|
+
* ASYNC. The asymmetry is deliberate and measured, not an oversight: that read
|
|
271
|
+
* happens for EVERY actively-touched neighbour in a parallel fan-out, while
|
|
272
|
+
* this one is paid at most once per DELIVERING neighbour — 0.0125 ms per
|
|
273
|
+
* neighbour measured on 40 real 15.6 KiB sources (#3157 AC 3), on loops that
|
|
274
|
+
* already do synchronous fs work (`nodeFs.existsSync` in the degraded
|
|
275
|
+
* fallback, and the `boundToCurrentDisk` verify whenever the producer attached
|
|
276
|
+
* a content hash and its (path, mtime, size) memo misses —
|
|
277
|
+
* `clients/lsp/diagnostic-binding.ts`). An async read here would force the
|
|
278
|
+
* quiet-window builder's synchronous callback to become async for no
|
|
279
|
+
* measurable gain.
|
|
280
|
+
*
|
|
281
|
+
* `undefined` on any failure: the caller degrades to the content-free half
|
|
282
|
+
* rather than hiding a finding it could not identify.
|
|
283
|
+
*/
|
|
284
|
+
function readNeighborContent(filePath) {
|
|
285
|
+
try {
|
|
286
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -4,6 +4,7 @@ import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
|
|
|
4
4
|
import { displayProjectDataPath, getProjectDataDir } from "./file-utils.js";
|
|
5
5
|
import { normalizeMessage, stableFindingId } from "./finding-identity.js";
|
|
6
6
|
import { normalizeMapKey } from "./path-utils.js";
|
|
7
|
+
import { resolveLensToolName } from "./tool-config.js";
|
|
7
8
|
/**
|
|
8
9
|
* Report-budget precedence (#1777). A hint is an opinion; a warning is a
|
|
9
10
|
* finding. When the report is capped, warnings must be spent first, or an
|
|
@@ -239,7 +240,7 @@ export function appendCodeQualityWarningsHistory(cwd, report) {
|
|
|
239
240
|
* {@link formatActionableWarningsAdvisory} (#2521): this advisory carried the
|
|
240
241
|
* identical hardcoded-legacy-path defect, one cache key over.
|
|
241
242
|
*/
|
|
242
|
-
export function formatCodeQualityWarningsAdvisory(report, cwd) {
|
|
243
|
+
export function formatCodeQualityWarningsAdvisory(report, cwd, host = "pi") {
|
|
243
244
|
if (report.summary.warnings === 0)
|
|
244
245
|
return undefined;
|
|
245
246
|
const topRules = report.summary.topRules
|
|
@@ -261,6 +262,9 @@ export function formatCodeQualityWarningsAdvisory(report, cwd) {
|
|
|
261
262
|
.filter(Boolean)
|
|
262
263
|
.join(", ")
|
|
263
264
|
: "";
|
|
265
|
+
// #2535 F3: a known tool with no mapping on this host resolves to
|
|
266
|
+
// undefined — omit the instruction rather than naming a dead tool.
|
|
267
|
+
const diagnosticsTool = resolveLensToolName("lens_diagnostics", host);
|
|
264
268
|
return [
|
|
265
269
|
`Code-quality warnings introduced/touched this turn: ${report.summary.warnings} across ${report.summary.files} file(s).`,
|
|
266
270
|
tiers
|
|
@@ -269,7 +273,11 @@ export function formatCodeQualityWarningsAdvisory(report, cwd) {
|
|
|
269
273
|
topRules ? `Top rules: ${topRules}` : undefined,
|
|
270
274
|
// #2521: tool route first (`mode=delta` covers the code-quality cache
|
|
271
275
|
// as well as the actionable one), resolved path second.
|
|
272
|
-
|
|
276
|
+
// #2535 F3: omit when unmapped on this host, never name a dead tool
|
|
277
|
+
// (pinned available on both hosts; defensive only).
|
|
278
|
+
diagnosticsTool
|
|
279
|
+
? `Use ${diagnosticsTool} with mode=delta to inspect these warnings.`
|
|
280
|
+
: undefined,
|
|
273
281
|
"No action required unless you are already refactoring these areas.",
|
|
274
282
|
`Raw report (only if you need the JSON): ${displayProjectDataPath(cwd, "cache", "code-quality-warnings.json")}`,
|
|
275
283
|
]
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
+
import { calcCyclomaticComplexity } from "./dispatch/facts/function-facts.js";
|
|
16
17
|
import { firstChildOfType, withTreeSitterRoot, isComplexityLanguageId, isComplexitySupportedFile, resolveTreeSitterLanguage, walk, } from "./tree-sitter-shared.js";
|
|
17
18
|
// JS/TS/JSX share one grammar shape.
|
|
18
19
|
const JSTS = {
|
|
@@ -190,6 +191,9 @@ function isLogicalOp(node, nodes) {
|
|
|
190
191
|
}
|
|
191
192
|
/** Cyclomatic contribution of a subtree: decision points + logical operators. */
|
|
192
193
|
function subtreeCyclomatic(root, nodes) {
|
|
194
|
+
// Dispatch uses 1-based McCabe; this client exposes 0-based contributions (#2697).
|
|
195
|
+
if (nodes === JSTS)
|
|
196
|
+
return calcCyclomaticComplexity(root) - 1;
|
|
193
197
|
let cc = 0;
|
|
194
198
|
walk(root, (n) => {
|
|
195
199
|
if (nodes.decision.has(n.type))
|
|
@@ -438,6 +442,7 @@ export class ComplexityClient {
|
|
|
438
442
|
aiCommentPatterns: countAICommentPatterns(content),
|
|
439
443
|
singleUseFunctions: countSingleUseFunctions(functions),
|
|
440
444
|
tryCatchCount: countTryCatch(root, nodes),
|
|
445
|
+
functions,
|
|
441
446
|
};
|
|
442
447
|
}
|
|
443
448
|
aggregateFunctionStats(functions) {
|
|
@@ -171,6 +171,8 @@ export const CONFIG_DIAGNOSTIC_CODES = {
|
|
|
171
171
|
* suppressed" about a configuration that applied nothing at all.
|
|
172
172
|
*/
|
|
173
173
|
PILENS_CFG_0008: "config resolution failed; whole configuration ignored",
|
|
174
|
+
/** A tool key is unknown or names a required, non-disableable tool. */
|
|
175
|
+
PILENS_CFG_0009: "unknown or non-disableable tool config key ignored",
|
|
174
176
|
};
|
|
175
177
|
/** The shape every code in the namespace must match. */
|
|
176
178
|
export const CONFIG_DIAGNOSTIC_CODE_PATTERN = /^PILENS_CFG_\d{4}$/;
|
|
@@ -740,8 +740,8 @@ export function reportPiLensConfigRecords(records) {
|
|
|
740
740
|
*/
|
|
741
741
|
export function ignoredRecordCollector(configPath, tier) {
|
|
742
742
|
const noted = [];
|
|
743
|
-
const note = (reason) => {
|
|
744
|
-
|
|
743
|
+
const note = (reason, code = "PILENS_CFG_0001") => {
|
|
744
|
+
let notedRecord = {
|
|
745
745
|
code: "PILENS_CFG_0001",
|
|
746
746
|
file: configPath,
|
|
747
747
|
key: "",
|
|
@@ -752,7 +752,10 @@ export function ignoredRecordCollector(configPath, tier) {
|
|
|
752
752
|
sourceText: reason.sourceText,
|
|
753
753
|
}),
|
|
754
754
|
tier,
|
|
755
|
-
}
|
|
755
|
+
};
|
|
756
|
+
if (code !== "PILENS_CFG_0001")
|
|
757
|
+
notedRecord.code = code;
|
|
758
|
+
noted.push(notedRecord);
|
|
756
759
|
};
|
|
757
760
|
return {
|
|
758
761
|
note,
|
|
@@ -42,6 +42,7 @@ import { LEGACY_ROOT_LSP_KEYS, LSP_NAMESPACE_KEY } from "./config-locations.js";
|
|
|
42
42
|
// every config loader's import graph for one type alias.
|
|
43
43
|
import { DENY_KEY, } from "./config-core/schema.js";
|
|
44
44
|
import { flagConfigSectionKeys, GLOBAL_NON_FLAG_CONFIG_SECTIONS, LENS_FLAGS, PROJECT_FOREIGN_CONFIG_NAMESPACES, PROJECT_NON_FLAG_CONFIG_SECTIONS, } from "./lens-flag-registry.js";
|
|
45
|
+
import { LENS_TOOL_NAMES } from "./tool-config.js";
|
|
45
46
|
/** The JSON Schema dialect the published artifact declares. */
|
|
46
47
|
const CONFIG_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
47
48
|
/**
|
|
@@ -154,6 +155,22 @@ function buildConfigSchema() {
|
|
|
154
155
|
]) {
|
|
155
156
|
properties[key] = opaque("stable");
|
|
156
157
|
}
|
|
158
|
+
properties.tools = {
|
|
159
|
+
type: "object",
|
|
160
|
+
additionalProperties: true,
|
|
161
|
+
properties: Object.fromEntries(LENS_TOOL_NAMES.map((name) => [
|
|
162
|
+
name,
|
|
163
|
+
{
|
|
164
|
+
type: "object",
|
|
165
|
+
additionalProperties: true,
|
|
166
|
+
properties: {
|
|
167
|
+
enabled: { type: "boolean", [STABILITY_TIER_KEY]: "experimental" },
|
|
168
|
+
},
|
|
169
|
+
[STABILITY_TIER_KEY]: "experimental",
|
|
170
|
+
},
|
|
171
|
+
])),
|
|
172
|
+
[STABILITY_TIER_KEY]: "stable",
|
|
173
|
+
};
|
|
157
174
|
// Namespaces owned by another tool that ride in the same file (`trivy`,
|
|
158
175
|
// `helm`). Reserved so they survive validation, `experimental` because their
|
|
159
176
|
// shape belongs to those runners, not to this schema.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* extraction — the prose is not API (the code is), but silently rewording a
|
|
23
23
|
* warning inside a refactor is still not a thing this PR does.
|
|
24
24
|
*/
|
|
25
|
-
import { DEPRECATED_CONFIG_SURFACES, } from "./config-diagnostic-codes.js";
|
|
25
|
+
import { DEPRECATED_CONFIG_SURFACES, withConfigDiagnosticCode, } from "./config-diagnostic-codes.js";
|
|
26
26
|
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
27
27
|
import { errorClassName } from "./error-class.js";
|
|
28
28
|
import { logExtension } from "./extension-log.js";
|
|
@@ -378,7 +378,7 @@ export function warnIgnoredConfigOnce(options) {
|
|
|
378
378
|
logExtension({
|
|
379
379
|
subsystem,
|
|
380
380
|
level: "warn",
|
|
381
|
-
message,
|
|
381
|
+
message: withConfigDiagnosticCode(message, code),
|
|
382
382
|
metadata: { configPath: file, reason, code, ...(key ? { key } : {}) },
|
|
383
383
|
});
|
|
384
384
|
// HUMAN-audience too: a config the user wrote is being ignored. Routed
|
|
@@ -19,6 +19,7 @@ import { findLocalBinsAt, VENV_BIN_DIRS } from "./package-manager.js";
|
|
|
19
19
|
import { findNearestMarkerRoot } from "./path-utils.js";
|
|
20
20
|
import { getScratchTreeFnmatchPatterns } from "./scratch-tree-policy.js";
|
|
21
21
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
22
|
+
import { probeToolAsync } from "./tool-probe.js";
|
|
22
23
|
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
23
24
|
import { firstOutputLine, spawnFailedWithNoOutput, } from "./dispatch/runners/utils/spawn-outcome.js";
|
|
24
25
|
import { formatToolFailure } from "./dispatch/runners/utils/tool-failure.js";
|
|
@@ -229,7 +230,7 @@ export class PythonDeadCodeClient {
|
|
|
229
230
|
let probe;
|
|
230
231
|
let hostStallMs;
|
|
231
232
|
try {
|
|
232
|
-
probe = await
|
|
233
|
+
probe = await probeToolAsync(c.cmd, [...c.prefix, "--version"], {
|
|
233
234
|
timeout: 5000,
|
|
234
235
|
});
|
|
235
236
|
}
|
|
@@ -415,9 +416,13 @@ export class PythonDeadCodeClient {
|
|
|
415
416
|
durationMs,
|
|
416
417
|
};
|
|
417
418
|
}
|
|
419
|
+
// vulture's verified exit-code table (above): exit 0 with empty
|
|
420
|
+
// stdout IS the clean-run signal, so this ran over the root
|
|
421
|
+
// (#2154) — unlike knip, whose clean run always prints JSON.
|
|
418
422
|
return {
|
|
419
423
|
...emptyResult(this.language),
|
|
420
424
|
success: true,
|
|
425
|
+
analyzed: true,
|
|
421
426
|
summary: "No dead code found",
|
|
422
427
|
durationMs,
|
|
423
428
|
};
|
|
@@ -430,6 +435,8 @@ export class PythonDeadCodeClient {
|
|
|
430
435
|
return {
|
|
431
436
|
...emptyResult(this.language),
|
|
432
437
|
success: true,
|
|
438
|
+
// #2154: parsed vulture output for this root.
|
|
439
|
+
analyzed: true,
|
|
433
440
|
unusedExports,
|
|
434
441
|
summary: total === 0
|
|
435
442
|
? "No dead code found"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { logExtension } from "./extension-log.js";
|
|
3
3
|
import { DEGRADATION_ENTRIES_PER_KIND, LEDGER_FIELD_MAX, truncateForLedger, } from "./ledger-bounds.js";
|
|
4
4
|
import { logLatency } from "./latency-logger.js";
|
|
5
|
-
import { getSinkRotations, getSinkWriteFailures, resetSinkRotations, resetSinkWriteFailures, } from "./ndjson-logger.js";
|
|
5
|
+
import { getSinkRotations, getSinkOptionConflicts, getSinkWriteFailures, resetSinkRotations, resetSinkWriteFailures, } from "./ndjson-logger.js";
|
|
6
6
|
// #2146: pulled at READ time, never pushed. `process-singletons.ts` is a
|
|
7
7
|
// dependency leaf on purpose — it cannot import this module without closing a
|
|
8
8
|
// no-client-cycles cycle through instance-registry/instance-reaper — so the
|
|
@@ -233,6 +233,18 @@ export function getDegradationSummary() {
|
|
|
233
233
|
})),
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
|
+
const optionConflicts = getSinkOptionConflicts();
|
|
237
|
+
if (optionConflicts.length > 0) {
|
|
238
|
+
summary.push({
|
|
239
|
+
kind: "log-sink-option-conflict",
|
|
240
|
+
count: optionConflicts.length,
|
|
241
|
+
droppedCount: 0,
|
|
242
|
+
latestReasons: optionConflicts.map((sink) => ({
|
|
243
|
+
subject: truncateForLedger(sink.file),
|
|
244
|
+
reason: truncateForLedger("shared writer rotation options differed across module graphs; first writer retained ownership"),
|
|
245
|
+
})),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
236
248
|
// #2146, same read-time fold: process-singleton resets live in the leaf
|
|
237
249
|
// module's own bounded log. One entry per family, so this group's count is
|
|
238
250
|
// the number of families this build could not adopt, never an event tally.
|
|
@@ -310,6 +322,9 @@ const INFORMATIONAL_DEGRADATION_KINDS = new Set([
|
|
|
310
322
|
// doc comment above) and is frequent/self-healing by design — a `⚠` would
|
|
311
323
|
// cry wolf on the sampler's ordinary best-effort data loss.
|
|
312
324
|
"resource-sampler-scanner-escalated",
|
|
325
|
+
// #2874: a successful legacy-directory migration is an upgrade tally, not
|
|
326
|
+
// a call to action. The hash-only subject avoids exposing the project path.
|
|
327
|
+
"data_dir_migrated",
|
|
313
328
|
]);
|
|
314
329
|
export function renderDegradationLines(summary = getDegradationSummary()) {
|
|
315
330
|
if (!isRenderableSummary(summary))
|
|
@@ -815,7 +815,11 @@ export class DependencyChecker {
|
|
|
815
815
|
}
|
|
816
816
|
}
|
|
817
817
|
this.publishState(gen, circular, circularFiles);
|
|
818
|
-
|
|
818
|
+
// #2154: the one madge site that parsed a graph for this root. Every
|
|
819
|
+
// other return here (missing root, no top-level source file, madge
|
|
820
|
+
// unavailable, spawn error, parse throw) is the SAME empty shape and
|
|
821
|
+
// must not be read as "no cycles in this project".
|
|
822
|
+
return { circular, count: circular.length, analyzed: true };
|
|
819
823
|
}
|
|
820
824
|
catch (err) {
|
|
821
825
|
this.log(`Scan error: ${err.message}`);
|