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
|
@@ -87,7 +87,7 @@ import { applyDispositionsMultiFile } from "../diagnostic-dispositions.js";
|
|
|
87
87
|
import { getKnipIgnorePatterns } from "../file-utils.js";
|
|
88
88
|
import { isAtOrAboveHomeDir, realpathOrResolve } from "../path-utils.js";
|
|
89
89
|
import { isSameOrWithin } from "../lsp/server.js";
|
|
90
|
-
import { incrementDegradationCount } from "../degradation-ledger.js";
|
|
90
|
+
import { incrementDegradationCount, recordDegradationOnce, } from "../degradation-ledger.js";
|
|
91
91
|
import { GitleaksClient } from "../gitleaks-client.js";
|
|
92
92
|
import { GovulncheckClient } from "../govulncheck-client.js";
|
|
93
93
|
import { isToolInstallAllowedByTrust, projectTrustDenialReason, } from "../project-trust.js";
|
|
@@ -281,8 +281,15 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
|
|
|
281
281
|
function record(id, adapted, elapsedMs, analysedRoot, analysis) {
|
|
282
282
|
if (analysedRoot) {
|
|
283
283
|
pushUnique(analyzed, id);
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
// #2962: the entry is pushed on the DECLARATION, not on its size. A
|
|
285
|
+
// coverage producer that analysed the root and proved zero files is a
|
|
286
|
+
// third state, distinct from both "covered, file absent from the set"
|
|
287
|
+
// and "this runner declares no coverage" — and it is the only one of
|
|
288
|
+
// the three that used to fall through to the whole-root id-only arm in
|
|
289
|
+
// `runnerRetirementDecision`, retiring findings for files nothing
|
|
290
|
+
// scanned. The eight non-producer runners pass no `analyzedFiles` at
|
|
291
|
+
// all and are untouched by this.
|
|
292
|
+
if (analysis?.analyzedFiles !== undefined) {
|
|
286
293
|
const root = realpathOrResolve(analysisRoot);
|
|
287
294
|
authoritativeCoverage.push({
|
|
288
295
|
runnerId: id,
|
|
@@ -291,6 +298,17 @@ export async function fetchFreshProjectDiagnostics(cacheManager, cwd, clients, s
|
|
|
291
298
|
return realpathOrResolve(file);
|
|
292
299
|
})),
|
|
293
300
|
});
|
|
301
|
+
if (analysis.analyzedFiles.length === 0) {
|
|
302
|
+
// Bounded at one row per (producer, root) per session: the kept
|
|
303
|
+
// finding is otherwise indistinguishable from a healthy run, and
|
|
304
|
+
// "the only coverage producer proved nothing" is the fact an
|
|
305
|
+
// operator needs when mode=full keeps showing a stale finding.
|
|
306
|
+
recordDegradationOnce({
|
|
307
|
+
kind: "runner-coverage-empty",
|
|
308
|
+
subject: `${id}:${root}`,
|
|
309
|
+
reason: `${id} analysed this root but declared zero scanned files, so retained ${id} findings are kept rather than retired`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
294
312
|
}
|
|
295
313
|
}
|
|
296
314
|
timings[id] = (timings[id] ?? 0) + elapsedMs;
|
|
@@ -64,11 +64,11 @@ import * as path from "node:path";
|
|
|
64
64
|
import { toPositiveFinite } from "./env-utils.js";
|
|
65
65
|
import { FRESHNESS_CADENCE_MS } from "./freshness-cadence.js";
|
|
66
66
|
import { assignFlagConfigSection, flagConfigSectionKeys, GLOBAL_NON_FLAG_CONFIG_SECTIONS, GLOBAL_ONLY_NON_FLAG_KEYS, hasFlagConfigPath, LENS_FLAGS, PROJECT_FOREIGN_CONFIG_NAMESPACES, PROJECT_FOREIGN_NAMESPACE_HONORED_KEYS, PROJECT_NON_FLAG_CONFIG_SECTIONS, PROJECT_SCOPED_LENS_FLAGS, readFlagConfigValue, } from "./lens-flag-registry.js";
|
|
67
|
-
import { configSearchDirs, getPiLensGlobalConfigPath, PROJECT_CONFIG_BASENAMES, PROJECT_CONFIG_LOCATIONS, } from "./config-locations.js";
|
|
67
|
+
import { configSearchDirs, getPiLensGlobalConfigPath, isResolvedGlobalConfigPath, PROJECT_CONFIG_BASENAMES, PROJECT_CONFIG_LOCATIONS, } from "./config-locations.js";
|
|
68
68
|
import { deprecationRecords, ignoredRecordCollector, projectLocationFor, readConfigDocument, reportConfigReadFailure, reportPiLensConfigRecords, resolveOnePiLensConfigDocument, } from "./config-resolve.js";
|
|
69
69
|
import { finalizeRecords, } from "./config-core/records.js";
|
|
70
70
|
import { homeRelativePath, isAtOrAboveHomeDir, walkUpDirs, } from "./path-utils.js";
|
|
71
|
-
import { findPiLensConfigMarkerInDir } from "./workspace-topology.js";
|
|
71
|
+
import { dirMtimesStillFresh, findPiLensConfigMarkerInDir, } from "./workspace-topology.js";
|
|
72
72
|
import { readToolConfig } from "./tool-config.js";
|
|
73
73
|
/**
|
|
74
74
|
* Project config basenames, in DESCENDING precedence (first match wins), for
|
|
@@ -289,7 +289,7 @@ function safeDirMtimeMs(dir) {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
function discoveryCacheStillFresh(entry) {
|
|
292
|
-
if (!entry.dirMtimes
|
|
292
|
+
if (!dirMtimesStillFresh(entry.dirMtimes)) {
|
|
293
293
|
return false;
|
|
294
294
|
}
|
|
295
295
|
// #2483 round 2. When no config was found anywhere, `bearingDirMtimes`'s
|
|
@@ -344,6 +344,12 @@ function discoverPiLensProjectConfig(startDir) {
|
|
|
344
344
|
if (!info) {
|
|
345
345
|
for (const name of PROJECT_CONFIG_BASENAMES) {
|
|
346
346
|
const candidate = path.join(dir, name);
|
|
347
|
+
// The global tier reads its file by absolute path; a candidate that
|
|
348
|
+
// IS that file is refused here rather than adopted as a project
|
|
349
|
+
// config, which would double-read and double-validate it
|
|
350
|
+
// (global-config-location PR, refs #2457).
|
|
351
|
+
if (isResolvedGlobalConfigPath(candidate))
|
|
352
|
+
continue;
|
|
347
353
|
const stat = safeFileStat(candidate);
|
|
348
354
|
if (stat?.isFile()) {
|
|
349
355
|
info = {
|
|
@@ -358,6 +364,8 @@ function discoverPiLensProjectConfig(startDir) {
|
|
|
358
364
|
}
|
|
359
365
|
for (const location of LEGACY_PROJECT_LOCATIONS) {
|
|
360
366
|
const file = path.join(dir, location.relativePath);
|
|
367
|
+
if (isResolvedGlobalConfigPath(file))
|
|
368
|
+
continue;
|
|
361
369
|
const stat = safeFileStat(file);
|
|
362
370
|
if (!stat?.isFile())
|
|
363
371
|
continue;
|
|
@@ -30,7 +30,7 @@ import { getSharedTreeSitterClient } from "../tree-sitter-shared.js";
|
|
|
30
30
|
import { symbolExtractionGrammar, TreeSitterSymbolExtractor, } from "../tree-sitter-symbol-extractor.js";
|
|
31
31
|
import { withTreeSitterRoot } from "../tree-sitter-shared.js";
|
|
32
32
|
import { incrementDegradationCount } from "../degradation-ledger.js";
|
|
33
|
-
import { resolveGitIdentity } from "./git-identity.js";
|
|
33
|
+
import { createGitIdentityResolver, resolveGitIdentity, } from "./git-identity.js";
|
|
34
34
|
import { formatReviewGraphRevisionDriftNote, } from "./revision-drift.js";
|
|
35
35
|
import { buildSymbolId } from "./symbol-id.js";
|
|
36
36
|
import { clearReviewGraphFileIr, getFreshReviewGraphFileIr, reviewGraphIrContentHash, } from "./shared-extraction-ir.js";
|
|
@@ -670,18 +670,18 @@ export function getReviewGraphCacheIdentity(cwd, graph) {
|
|
|
670
670
|
// path, that assumption silently breaks. This doesn't hard-fail (the issue
|
|
671
671
|
// is explicit: log-once observability is enough) — it just makes the
|
|
672
672
|
// assumption visible. The Set records every cwd whose check has RUN (not just
|
|
673
|
-
// mismatches), so
|
|
674
|
-
//
|
|
673
|
+
// mismatches), so a build-local resolver keeps repeated checks in one build
|
|
674
|
+
// bounded without retaining repository presence across builds.
|
|
675
675
|
const _cwdWorktreeCheckedCwds = new Set();
|
|
676
676
|
export function _resetCwdWorktreeMismatchLogForTests() {
|
|
677
677
|
_cwdWorktreeCheckedCwds.clear();
|
|
678
678
|
}
|
|
679
|
-
function logCwdWorktreeMismatchOnce(cwd) {
|
|
679
|
+
function logCwdWorktreeMismatchOnce(cwd, resolveIdentity = resolveGitIdentity) {
|
|
680
680
|
const key = normalizeMapKey(cwd);
|
|
681
681
|
if (_cwdWorktreeCheckedCwds.has(key))
|
|
682
682
|
return;
|
|
683
683
|
_cwdWorktreeCheckedCwds.add(key);
|
|
684
|
-
const identity =
|
|
684
|
+
const identity = resolveIdentity(cwd);
|
|
685
685
|
if (!identity)
|
|
686
686
|
return; // not a git repo — nothing to compare against
|
|
687
687
|
if (identity.worktreeRoot === normalizeFilePath(path.resolve(cwd)))
|
|
@@ -1345,7 +1345,7 @@ function logSnapshotReadVerdict(cwd, phase, reason) {
|
|
|
1345
1345
|
return;
|
|
1346
1346
|
logReviewGraph({ cwd, phase, reason });
|
|
1347
1347
|
}
|
|
1348
|
-
function loadPersistedGraph(cwd, opts) {
|
|
1348
|
+
function loadPersistedGraph(cwd, opts, resolveIdentity = resolveGitIdentity) {
|
|
1349
1349
|
const cacheDir = path.join(getProjectDataDir(cwd), "cache");
|
|
1350
1350
|
const cachePath = path.join(cacheDir, GRAPH_CACHE_FILENAME);
|
|
1351
1351
|
const legacyPath = path.join(cacheDir, LEGACY_GRAPH_CACHE_FILENAME);
|
|
@@ -1382,7 +1382,7 @@ function loadPersistedGraph(cwd, opts) {
|
|
|
1382
1382
|
// "can't verify," not a mismatch, so it does not drop the snapshot.
|
|
1383
1383
|
let stampedHead;
|
|
1384
1384
|
if (opts?.verifyWorktreeIdentity && data.gitStamp) {
|
|
1385
|
-
const current =
|
|
1385
|
+
const current = resolveIdentity(cwd);
|
|
1386
1386
|
if (current && current.worktreeRoot !== data.gitStamp.worktreeRoot) {
|
|
1387
1387
|
logSnapshotReadVerdict(cwd, "snapshot_read_dropped", "worktree_mismatch");
|
|
1388
1388
|
return null;
|
|
@@ -2198,11 +2198,11 @@ function persistGraph(cwd, signature, fileSignatures, fileHashes, graph, options
|
|
|
2198
2198
|
const cacheDir = path.join(getProjectDataDir(cwd), "cache");
|
|
2199
2199
|
const cachePath = path.join(cacheDir, GRAPH_CACHE_FILENAME);
|
|
2200
2200
|
sweepStaleStageFiles(cacheDir);
|
|
2201
|
-
// #300: resolve the git stamp
|
|
2202
|
-
//
|
|
2203
|
-
//
|
|
2201
|
+
// #300: resolve the git stamp at persist time. The build-local resolver
|
|
2202
|
+
// deduplicates repeated checks within this build, while a later build gets a
|
|
2203
|
+
// fresh lifecycle view. undefined for
|
|
2204
2204
|
// non-git cwds, which serializes as `gitStamp: undefined` → omitted key.
|
|
2205
|
-
const gitStamp = resolveGitIdentity(cwd);
|
|
2205
|
+
const gitStamp = (options.resolveIdentity ?? resolveGitIdentity)(cwd);
|
|
2206
2206
|
// Retain the immutable snapshot inputs and build their O(graph) serializable
|
|
2207
2207
|
// arrays only after the quiet window. Replacing a pending entry during an edit
|
|
2208
2208
|
// burst now avoids both serialization and the pre-serialization full copies.
|
|
@@ -2353,7 +2353,7 @@ function hashIgnoredIds(ignoredIds) {
|
|
|
2353
2353
|
}
|
|
2354
2354
|
/** Assemble the checkpoint DTO from the current PRE-resolution graph. Isolated
|
|
2355
2355
|
* so both the offloaded and synchronous writers serialize identical bytes. */
|
|
2356
|
-
function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds) {
|
|
2356
|
+
function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity = resolveGitIdentity) {
|
|
2357
2357
|
return {
|
|
2358
2358
|
version: REVIEW_GRAPH_VERSION,
|
|
2359
2359
|
builtAt: new Date().toISOString(),
|
|
@@ -2363,7 +2363,7 @@ function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileC
|
|
|
2363
2363
|
ignoredIdsHash: hashIgnoredIds(ignoredIds),
|
|
2364
2364
|
nodes: Array.from(graph.nodes.entries()),
|
|
2365
2365
|
edges: graph.edges,
|
|
2366
|
-
gitStamp:
|
|
2366
|
+
gitStamp: resolveIdentity(cwd),
|
|
2367
2367
|
};
|
|
2368
2368
|
}
|
|
2369
2369
|
/**
|
|
@@ -2375,12 +2375,12 @@ function buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileC
|
|
|
2375
2375
|
* resurrect one over a completed build; it falls back to a synchronous write
|
|
2376
2376
|
* when the worker is unavailable. A lost checkpoint only costs a cold rebuild.
|
|
2377
2377
|
*/
|
|
2378
|
-
function writeReviewGraphCheckpoint(cwd, graph, processedHashes, targetFileCount, ignoredIds) {
|
|
2378
|
+
function writeReviewGraphCheckpoint(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity = resolveGitIdentity) {
|
|
2379
2379
|
const generation = (_checkpointGenerations.get(cwd) ?? 0) + 1;
|
|
2380
2380
|
_checkpointGenerations.set(cwd, generation);
|
|
2381
2381
|
let data;
|
|
2382
2382
|
try {
|
|
2383
|
-
data = buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds);
|
|
2383
|
+
data = buildReviewGraphCheckpointData(cwd, graph, processedHashes, targetFileCount, ignoredIds, resolveIdentity);
|
|
2384
2384
|
}
|
|
2385
2385
|
catch {
|
|
2386
2386
|
return; // best-effort — building the DTO failed, skip this stride
|
|
@@ -2488,7 +2488,7 @@ function deleteReviewGraphCheckpoint(cwd) {
|
|
|
2488
2488
|
* The returned graph carries `persistCoverage.inProgress` so it can never be
|
|
2489
2489
|
* mistaken for a complete graph if it escapes the resume path.
|
|
2490
2490
|
*/
|
|
2491
|
-
function loadReviewGraphCheckpoint(cwd) {
|
|
2491
|
+
function loadReviewGraphCheckpoint(cwd, resolveIdentity = resolveGitIdentity) {
|
|
2492
2492
|
const checkpointPath = reviewGraphCheckpointPath(cwd);
|
|
2493
2493
|
let data;
|
|
2494
2494
|
try {
|
|
@@ -2515,7 +2515,7 @@ function loadReviewGraphCheckpoint(cwd) {
|
|
|
2515
2515
|
return null;
|
|
2516
2516
|
}
|
|
2517
2517
|
if (data.gitStamp) {
|
|
2518
|
-
const current =
|
|
2518
|
+
const current = resolveIdentity(cwd);
|
|
2519
2519
|
if (current && current.worktreeRoot !== data.gitStamp.worktreeRoot) {
|
|
2520
2520
|
logReviewGraph({
|
|
2521
2521
|
cwd,
|
|
@@ -2598,8 +2598,8 @@ function pruneOrphanNonFileNodes(graph) {
|
|
|
2598
2598
|
* - The ignored-id fingerprint must match (import-edge resolution depends on
|
|
2599
2599
|
* it), else fail open.
|
|
2600
2600
|
*/
|
|
2601
|
-
async function tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds) {
|
|
2602
|
-
const loaded = loadReviewGraphCheckpoint(cwd);
|
|
2601
|
+
async function tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds, resolveIdentity = resolveGitIdentity) {
|
|
2602
|
+
const loaded = loadReviewGraphCheckpoint(cwd, resolveIdentity);
|
|
2603
2603
|
if (!loaded)
|
|
2604
2604
|
return null;
|
|
2605
2605
|
if (loaded.ignoredIdsHash !== hashIgnoredIds(ignoredIds)) {
|
|
@@ -4153,6 +4153,7 @@ async function tryIncrementalFromCache(cached, ctx) {
|
|
|
4153
4153
|
projectSeq: ctx.seqAtBuildStart,
|
|
4154
4154
|
seqHint: ctx.seqHint,
|
|
4155
4155
|
mode: ctx.mode,
|
|
4156
|
+
resolveIdentity: ctx.resolveIdentity,
|
|
4156
4157
|
});
|
|
4157
4158
|
setGraphBuildInfo(graph, {
|
|
4158
4159
|
reused: true,
|
|
@@ -4179,7 +4180,7 @@ function hasGraphKindExtension(file) {
|
|
|
4179
4180
|
* `_lastGraphBuildInfo` (the caller's full sweep sets the mode and stamps the
|
|
4180
4181
|
* fallback reason). Correctness bar is HIGH: any doubt ⇒ fall back.
|
|
4181
4182
|
*/
|
|
4182
|
-
async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, ignoredIds, cacheEpoch) {
|
|
4183
|
+
async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, ignoredIds, cacheEpoch, resolveIdentity) {
|
|
4183
4184
|
const cached = _workspaceGraphCache.get(normalizedCwd);
|
|
4184
4185
|
// Condition 2: need an in-process entry that recorded a build seq and whose
|
|
4185
4186
|
// SOURCE WALK was complete. An entry-budget-truncated or checkpoint graph has
|
|
@@ -4326,6 +4327,7 @@ async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, fa
|
|
|
4326
4327
|
projectSeq: seqAtBuildStart,
|
|
4327
4328
|
seqHint: true,
|
|
4328
4329
|
mode: "seq-fastpath",
|
|
4330
|
+
resolveIdentity,
|
|
4329
4331
|
});
|
|
4330
4332
|
// #459: filesToUpdate was non-empty — this fastpath re-extracted real files,
|
|
4331
4333
|
// so (unlike the no-op branch above) the graph object did change.
|
|
@@ -4343,6 +4345,7 @@ async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, fa
|
|
|
4343
4345
|
return { graph };
|
|
4344
4346
|
}
|
|
4345
4347
|
async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
4348
|
+
const resolveIdentity = createGitIdentityResolver();
|
|
4346
4349
|
const normalizedCwd = normalizeMapKey(cwd);
|
|
4347
4350
|
const cacheEpoch = workspaceCacheEpoch(normalizedCwd);
|
|
4348
4351
|
// `await undefined` still yields a microtask, which reorders overlapping
|
|
@@ -4352,7 +4355,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4352
4355
|
await _reviewGraphBuildGateForTests();
|
|
4353
4356
|
const normalizedChanged = changedFiles.map((file) => normalizeMapKey(file));
|
|
4354
4357
|
const normalizedChangedSet = new Set(normalizedChanged);
|
|
4355
|
-
logCwdWorktreeMismatchOnce(cwd);
|
|
4358
|
+
logCwdWorktreeMismatchOnce(cwd, resolveIdentity);
|
|
4356
4359
|
// #622: reject a cwd that IS (or is an ancestor of) $HOME before any walk is
|
|
4357
4360
|
// attempted. The 3 real per-edit callers (dispatch/integration.ts's
|
|
4358
4361
|
// computeCascadeForFile, mcp/analyze.ts, tree-sitter.ts's
|
|
@@ -4405,7 +4408,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4405
4408
|
// cascade.log can watch the fast-path hit/miss rate.
|
|
4406
4409
|
let seqFastpathFallback;
|
|
4407
4410
|
if (seqHint && seqAtBuildStart !== undefined && seqFastpathEnabled()) {
|
|
4408
|
-
const fast = await trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, await ignoredIdsPromise, cacheEpoch);
|
|
4411
|
+
const fast = await trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, await ignoredIdsPromise, cacheEpoch, resolveIdentity);
|
|
4409
4412
|
if ("graph" in fast)
|
|
4410
4413
|
return fast.graph;
|
|
4411
4414
|
seqFastpathFallback = fast.fallback;
|
|
@@ -4550,6 +4553,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4550
4553
|
seqAtBuildStart,
|
|
4551
4554
|
ignoredIds,
|
|
4552
4555
|
cacheEpoch,
|
|
4556
|
+
resolveIdentity,
|
|
4553
4557
|
});
|
|
4554
4558
|
if (incremental) {
|
|
4555
4559
|
updateGraphBuildInfo(incremental, { seqFastpathFallback });
|
|
@@ -4561,7 +4565,9 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4561
4565
|
// #202 content-hash confirm below already content-verify the load, and
|
|
4562
4566
|
// dropping on every HEAD move would force a full whole-repo rebuild after
|
|
4563
4567
|
// each plain `git commit` (HEAD moves, files unchanged).
|
|
4564
|
-
const diskCached = sourceFilesTruncated
|
|
4568
|
+
const diskCached = sourceFilesTruncated
|
|
4569
|
+
? null
|
|
4570
|
+
: loadPersistedGraph(cwd, undefined, resolveIdentity);
|
|
4565
4571
|
if (diskCached?.signature === signature) {
|
|
4566
4572
|
const graph = cloneGraph(diskCached.graph);
|
|
4567
4573
|
rebuildIndexes(graph);
|
|
@@ -4613,6 +4619,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4613
4619
|
facts,
|
|
4614
4620
|
seqAtBuildStart,
|
|
4615
4621
|
ignoredIds,
|
|
4622
|
+
resolveIdentity,
|
|
4616
4623
|
});
|
|
4617
4624
|
if (incremental) {
|
|
4618
4625
|
updateGraphBuildInfo(incremental, { seqFastpathFallback });
|
|
@@ -4623,7 +4630,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4623
4630
|
// present and still current (#936 limit 2), else cold from an empty graph.
|
|
4624
4631
|
const resumed = sourceFilesTruncated
|
|
4625
4632
|
? null
|
|
4626
|
-
: await tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds);
|
|
4633
|
+
: await tryResumeFromCheckpoint(cwd, filesToBuild, ignoredIds, resolveIdentity);
|
|
4627
4634
|
const graph = resumed?.graph ?? createEmptyGraph();
|
|
4628
4635
|
const filesToExtract = resumed?.remaining ?? filesToBuild;
|
|
4629
4636
|
const treeSitterClient = getSharedTreeSitterClient();
|
|
@@ -4674,7 +4681,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4674
4681
|
if (Number.isFinite(testStopAfter) &&
|
|
4675
4682
|
testStopAfter > 0 &&
|
|
4676
4683
|
extractedCount >= testStopAfter) {
|
|
4677
|
-
writeReviewGraphCheckpointSync(cwd, buildReviewGraphCheckpointData(cwd, graph, fileHashes, filesToBuild.length, ignoredIds), {
|
|
4684
|
+
writeReviewGraphCheckpointSync(cwd, buildReviewGraphCheckpointData(cwd, graph, fileHashes, filesToBuild.length, ignoredIds, resolveIdentity), {
|
|
4678
4685
|
nodes: graph.nodes.size,
|
|
4679
4686
|
edges: graph.edges.length,
|
|
4680
4687
|
processed: fileHashes.size,
|
|
@@ -4686,7 +4693,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4686
4693
|
if (filesSinceCheckpoint >= checkpointEvery &&
|
|
4687
4694
|
remainingAfter >= checkpointEvery &&
|
|
4688
4695
|
Date.now() - lastCheckpointMs >= checkpointMinIntervalMs) {
|
|
4689
|
-
writeReviewGraphCheckpoint(cwd, graph, fileHashes, filesToBuild.length, ignoredIds);
|
|
4696
|
+
writeReviewGraphCheckpoint(cwd, graph, fileHashes, filesToBuild.length, ignoredIds, resolveIdentity);
|
|
4690
4697
|
filesSinceCheckpoint = 0;
|
|
4691
4698
|
lastCheckpointMs = Date.now();
|
|
4692
4699
|
}
|
|
@@ -4776,6 +4783,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4776
4783
|
mode: "full",
|
|
4777
4784
|
sourceFileCount,
|
|
4778
4785
|
sourceFilesTruncated,
|
|
4786
|
+
resolveIdentity,
|
|
4779
4787
|
}); // fire-and-forget
|
|
4780
4788
|
setGraphBuildInfo(graph, {
|
|
4781
4789
|
reused: false,
|
|
@@ -14,14 +14,24 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
-
import { BoundedLruCache } from "../bounded-cache.js";
|
|
18
17
|
import { normalizeFilePath, walkUpDirs } from "../path-utils.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
/** A build-local memo: it is discarded when the build settles. */
|
|
19
|
+
export function createGitIdentityResolver() {
|
|
20
|
+
const memo = new Map();
|
|
21
|
+
return (cwd) => {
|
|
22
|
+
if (memo.has(cwd))
|
|
23
|
+
return memo.get(cwd);
|
|
24
|
+
const identity = resolveGitIdentity(cwd);
|
|
25
|
+
memo.set(cwd, identity);
|
|
26
|
+
return identity;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Kept as a compatibility no-op for tests that reset the former memo. Git
|
|
30
|
+
// repository creation/removal is a process-lifecycle change, so the lookup
|
|
31
|
+
// below must walk the filesystem on every call rather than cache presence or
|
|
32
|
+
// absence by cwd.
|
|
23
33
|
export function _resetGitIdentityCacheForTests() {
|
|
24
|
-
|
|
34
|
+
return;
|
|
25
35
|
}
|
|
26
36
|
function readCommonDir(gitDir) {
|
|
27
37
|
try {
|
|
@@ -84,9 +94,6 @@ function resolveGitDir(cwd) {
|
|
|
84
94
|
return null;
|
|
85
95
|
}
|
|
86
96
|
function getResolvedGitDir(cwd) {
|
|
87
|
-
const key = normalizeFilePath(path.resolve(cwd));
|
|
88
|
-
if (_gitDirCache.has(key))
|
|
89
|
-
return _gitDirCache.get(key) ?? null;
|
|
90
97
|
let resolved;
|
|
91
98
|
try {
|
|
92
99
|
resolved = resolveGitDir(cwd);
|
|
@@ -94,7 +101,6 @@ function getResolvedGitDir(cwd) {
|
|
|
94
101
|
catch {
|
|
95
102
|
resolved = null;
|
|
96
103
|
}
|
|
97
|
-
_gitDirCache.set(key, resolved);
|
|
98
104
|
return resolved;
|
|
99
105
|
}
|
|
100
106
|
function resolveRefToSha(commonDir, ref) {
|
|
@@ -12,6 +12,7 @@ import * as fs from "node:fs";
|
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
14
14
|
import { isFileKind } from "./file-kinds.js";
|
|
15
|
+
import { pathsEqual } from "./path-utils.js";
|
|
15
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
16
17
|
import { resolveToolCwd } from "./tool-cwd.js";
|
|
17
18
|
import { ruffConfigArgs } from "./tool-policy.js";
|
|
@@ -95,7 +96,7 @@ export class RuffClient {
|
|
|
95
96
|
absolutePath,
|
|
96
97
|
], { timeout: 10000, cwd: ruffCwd });
|
|
97
98
|
const beforeDiags = pre.stdout?.trim()
|
|
98
|
-
? this.parseOutput(pre.stdout, absolutePath)
|
|
99
|
+
? this.parseOutput(pre.stdout, ruffCwd, absolutePath)
|
|
99
100
|
: [];
|
|
100
101
|
const fixableCount = beforeDiags.filter((d) => d.fixable).length;
|
|
101
102
|
const fix = await safeSpawnAsync(this.ruffCommand, ["check", "--fix", ...configArgs, absolutePath], { timeout: 15000, cwd: ruffCwd });
|
|
@@ -119,15 +120,41 @@ export class RuffClient {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
// --- Internal ---
|
|
122
|
-
|
|
123
|
+
/**
|
|
124
|
+
* `cwd` is the directory ruff RAN in, and it is REQUIRED: a default would
|
|
125
|
+
* hide the wrong base at a call site that forgot it (#3284's D11).
|
|
126
|
+
*
|
|
127
|
+
* ruff's JSON `filename` is the `SourceFile`'s name verbatim — the JSON
|
|
128
|
+
* renderer reads `span.file().path(resolver)` (ruff 0.16.8
|
|
129
|
+
* `crates/ruff_db/src/diagnostic/render/json.rs:58,120`), which for a ruff
|
|
130
|
+
* (not `ty`) diagnostic is `file.name()` (`crates/ruff_db/src/diagnostic/
|
|
131
|
+
* mod.rs:1187-1193`), NOT the cwd-relative `relative_path` the text
|
|
132
|
+
* renderers use (`mod.rs:1195-1204`). That name is
|
|
133
|
+
* `SourceFileBuilder::new(path.to_string_lossy(), …)`
|
|
134
|
+
* (`crates/ruff_linter/src/linter.rs:1049`) over a path the resolver has
|
|
135
|
+
* already absolutized ("Normalize every path (e.g., convert from relative to
|
|
136
|
+
* absolute)", `crates/ruff_workspace/src/resolver.rs:484`). So ruff echoes
|
|
137
|
+
* the absolute path we hand it as argv: the BASE was already right here and
|
|
138
|
+
* the fold deletes a `path.resolve` with no base — which silently meant the
|
|
139
|
+
* EXTENSION's `process.cwd()`, not ruff's — rather than leaving it for a
|
|
140
|
+
* future relative invocation to re-enable.
|
|
141
|
+
*/
|
|
142
|
+
parseOutput(output, cwd, filterFile) {
|
|
123
143
|
if (!output.trim())
|
|
124
144
|
return [];
|
|
125
145
|
try {
|
|
126
146
|
const items = JSON.parse(output);
|
|
127
147
|
const diagnostics = [];
|
|
128
148
|
for (const item of items) {
|
|
129
|
-
// Filter to single file if requested
|
|
130
|
-
|
|
149
|
+
// Filter to single file if requested. #3278/#3286: one seam answers
|
|
150
|
+
// "is this reported diagnostic about the file I ran for?" — resolve
|
|
151
|
+
// the tool's spelling against the cwd the tool RAN in and compare
|
|
152
|
+
// through `pathsEqual`, the repo's on-disk identity predicate. A bare
|
|
153
|
+
// `!==` treats a spelling that differs only in case (the SAME file on
|
|
154
|
+
// Windows and on a case-folding POSIX mount) as a different file and
|
|
155
|
+
// drops every finding for the edited file (#209, #3277).
|
|
156
|
+
if (filterFile &&
|
|
157
|
+
!pathsEqual(path.resolve(cwd, item.filename), filterFile))
|
|
131
158
|
continue;
|
|
132
159
|
diagnostics.push({
|
|
133
160
|
line: item.location.row - 1, // ruff is 1-indexed
|
|
@@ -184,12 +184,45 @@ export class RuntimeCoordinator {
|
|
|
184
184
|
this._cascadeSessionStats.diagnosticsSurfaced += diagnosticsSurfaced;
|
|
185
185
|
this._cascadeSessionStats.coldSnapshotTouches += coldSnapshotTouches;
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Record the turn-end policy verdict on every live inline-blocker record
|
|
189
|
+
* (#3248): `true` for a file whose every blocker the policy suppressed,
|
|
190
|
+
* `false` for every other record in the map.
|
|
191
|
+
*
|
|
192
|
+
* ONE writer, rewriting the WHOLE axis each turn end, so the verdict can
|
|
193
|
+
* never outlive the pass that made it: a record demoted since, or one whose
|
|
194
|
+
* survivors came back because the file's bytes moved under a content-bound
|
|
195
|
+
* anchor, is cleared by the same call that sets its sibling. A fresh
|
|
196
|
+
* dispatch replaces the record wholesale (`recordInlineBlockers`), so a NEW
|
|
197
|
+
* blocker is never born pre-suppressed (#1198 ordering).
|
|
198
|
+
*
|
|
199
|
+
* @returns how many records this call CHANGED — 0 means the gate's view of
|
|
200
|
+
* the map is already what the policy just decided.
|
|
201
|
+
*/
|
|
202
|
+
applyInlineBlockerPolicyVerdicts(suppressedFilePaths) {
|
|
203
|
+
const suppressed = new Set(suppressedFilePaths.map((filePath) => path.resolve(filePath)));
|
|
204
|
+
let changed = 0;
|
|
205
|
+
for (const [key, entry] of this._pendingInlineBlockers.entries()) {
|
|
206
|
+
const policySuppressed = suppressed.has(path.resolve(entry.filePath));
|
|
207
|
+
if (!!entry.policySuppressed === policySuppressed)
|
|
208
|
+
continue;
|
|
209
|
+
this._pendingInlineBlockers.set(key, { ...entry, policySuppressed });
|
|
210
|
+
changed += 1;
|
|
211
|
+
}
|
|
212
|
+
return changed;
|
|
213
|
+
}
|
|
187
214
|
updateGitGuardStatus(hasBlockers, output) {
|
|
188
215
|
// The status is an aggregate over the current per-file map. A clean B
|
|
189
216
|
// result must not erase an unresolved A result; the pipeline records/clears
|
|
190
217
|
// the edited file immediately before this method runs.
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
// #3248: every live record except one whose whole blocker set the
|
|
219
|
+
// turn-end disposition policy suppressed. The record itself stays in the
|
|
220
|
+
// map — the policy is content-bound and re-derived from the record's
|
|
221
|
+
// diagnostics at every turn end, so dropping it there would be silencing
|
|
222
|
+
// rather than filtering (AGENTS.md shape 10); it just stops counting for
|
|
223
|
+
// the gate the agent was shown nothing for.
|
|
224
|
+
const blocking = this.getInlineBlockersSnapshot().filter((entry) => !entry.policySuppressed);
|
|
225
|
+
this._gitGuardHasBlockers = hasBlockers || blocking.length > 0;
|
|
193
226
|
if (!this._gitGuardHasBlockers) {
|
|
194
227
|
this._gitGuardSummary = "";
|
|
195
228
|
return;
|
|
@@ -198,7 +231,7 @@ export class RuntimeCoordinator {
|
|
|
198
231
|
.split("\n")
|
|
199
232
|
.map((line) => line.trim())
|
|
200
233
|
.find((line) => line.length > 0);
|
|
201
|
-
const summaries =
|
|
234
|
+
const summaries = blocking
|
|
202
235
|
.map((entry) => entry.summary.trim())
|
|
203
236
|
.filter(Boolean);
|
|
204
237
|
this._gitGuardSummary = (summaries[0] ??
|
|
@@ -665,7 +698,7 @@ export class RuntimeCoordinator {
|
|
|
665
698
|
// that actually settles it delivers it.
|
|
666
699
|
return this._cascadeRuns.length > 0;
|
|
667
700
|
}
|
|
668
|
-
recordInlineBlockers(filePath, summary, writeIndex, sources, lines, contentBaseline) {
|
|
701
|
+
recordInlineBlockers(filePath, summary, writeIndex, sources, lines, contentBaseline, diagnostics) {
|
|
669
702
|
const recordedAtMs = Date.now();
|
|
670
703
|
this._pendingInlineBlockers.set(path.resolve(filePath), {
|
|
671
704
|
filePath,
|
|
@@ -673,6 +706,7 @@ export class RuntimeCoordinator {
|
|
|
673
706
|
writeIndex,
|
|
674
707
|
sources,
|
|
675
708
|
lines,
|
|
709
|
+
diagnostics,
|
|
676
710
|
recordedAtMs,
|
|
677
711
|
stale: false,
|
|
678
712
|
...(contentBaseline
|
|
@@ -8,7 +8,7 @@ import { evaluateGitGuard, isGitCommitOrPushAttempt } from "./git-guard.js";
|
|
|
8
8
|
import { dropHashlineAnchorMemo } from "./hashline-anchor.js";
|
|
9
9
|
import { evaluateSharedCheckoutGuard } from "./shared-checkout-guard.js";
|
|
10
10
|
import { logLatency } from "./latency-logger.js";
|
|
11
|
-
import { normalizeMapKey } from "./path-utils.js";
|
|
11
|
+
import { normalizeMapKey, toPosix } from "./path-utils.js";
|
|
12
12
|
import { captureFileStats, getOpaqueBaselineStore, isGitWorktree, } from "./opaque-mutation-scan.js";
|
|
13
13
|
import { normalizeForGuardMatch } from "./host-edit-normalize.js";
|
|
14
14
|
import { retargetReplacementIndentation } from "./indent-retarget.js";
|
|
@@ -133,7 +133,12 @@ function isLspCapableFile(filePath) {
|
|
|
133
133
|
return LANGUAGE_POLICY[kind]?.lspCapable !== false;
|
|
134
134
|
}
|
|
135
135
|
function shouldSkipLspAutoTouch(filePath, projectRoot) {
|
|
136
|
-
|
|
136
|
+
// #1193 P3: the separator fold is `toPosix`, not a fourth inline copy of
|
|
137
|
+
// `.replace(/\\/g, "/")`. The `toLowerCase` stays and is NOT a path-key case
|
|
138
|
+
// fold: `normalized` is never a map key, only the haystack for the
|
|
139
|
+
// lowercase marker substrings below, which must match a mis-cased spelling
|
|
140
|
+
// of the same marker directory on a case-insensitive filesystem.
|
|
141
|
+
const normalized = toPosix(path.resolve(filePath)).toLowerCase();
|
|
137
142
|
const base = path.basename(filePath).toLowerCase();
|
|
138
143
|
if (normalized.includes("/.pi-lens/"))
|
|
139
144
|
return true;
|
|
@@ -792,9 +797,15 @@ async function handleToolCallImpl(deps) {
|
|
|
792
797
|
// budgeted families rather than leaving this site to spell its
|
|
793
798
|
// own axis value (#2557 review F7).
|
|
794
799
|
hook: "tool_call",
|
|
795
|
-
// The ambient slot is populated by tool_result,
|
|
796
|
-
// already run
|
|
797
|
-
// this await (#2523 AC4).
|
|
800
|
+
// The ambient slot is populated by tool_result, AFTER this hook has
|
|
801
|
+
// already run, so the live `tool_call` signal is the only one that
|
|
802
|
+
// can release this await when the user presses Escape (#2523 AC4).
|
|
803
|
+
// #2939 round 2 restored this after round 2's own measurement: with
|
|
804
|
+
// the signal absent, an aborted caller waits the demand's whole
|
|
805
|
+
// `BOOTSTRAP_LOAD_TIMEOUT_MS` out and the cancel then surfaces on the
|
|
806
|
+
// ledger as a `timeout` degradation — the exact inversion
|
|
807
|
+
// `requestBootstrapClients`'s `unavailableReason !== "aborted"` guard
|
|
808
|
+
// exists to prevent.
|
|
798
809
|
signal: deps.ctx.signal,
|
|
799
810
|
}))?.complexityClient;
|
|
800
811
|
const baseline = await complexityClient?.analyzeFile(filePath);
|