Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 8a8efb8226 fix(javascript): keep unselected sources out of cancellation failures
With source_paths, the analysis loop checked cancellation before it
recognized an outside-analysis-scope file. Cancellation arriving after the
last selected source therefore recorded the next unselected file as a
cancelled analysis failure, and the completion loop skipped it as already
analyzed.

Give out-of-selection classification one owner, used by the loop, the
interruption handler and the completion loop. The cancellation is still
reported as the interruption.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:36:10 +00:00
2 changed files with 80 additions and 29 deletions
@@ -143,25 +143,27 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
const selectedFileCount =
fileSet.files.length - (fileSet.source_selection?.unselected_files ?? 0);
let selectedFileIndex = 0;
// Caller-excluded sources are never analyzed, so neither cancellation nor
// another interruption describes them.
const recordOutsideSelection = (file: JavaScriptArtifactFile): boolean => {
if (file.text.included || file.text.reason !== "outside-analysis-scope")
return false;
state.files.push({ file, javascript: null, semantic: null });
if (file.kind === "javascript")
projection.recordUnavailableFile(
file,
"Source was not analyzed because it is outside the caller-selected source_paths.",
null,
"incomplete-module",
);
return true;
};
for (const file of fileSet.files) {
try {
await setImmediate();
if (signal?.aborted === true)
throw new AnalysisCancelledError("analyze_javascript_application");
if (
!file.text.included &&
file.text.reason === "outside-analysis-scope"
) {
state.files.push({ file, javascript: null, semantic: null });
if (file.kind === "javascript")
projection.recordUnavailableFile(
file,
"Source was not analyzed because it is outside the caller-selected source_paths.",
null,
"incomplete-module",
);
continue;
}
if (recordOutsideSelection(file)) continue;
await beforeFile(file, selectedFileIndex++, selectedFileCount);
if (file.kind !== "javascript" || !file.text.included) {
const steps = analyzeArtifactFileSteps(file, {
@@ -252,7 +254,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
: cause instanceof AnalysisError
? cause
: null;
if (interruption !== null) {
if (interruption !== null && !recordOutsideSelection(file)) {
state.files.push({
file,
javascript: null,
@@ -286,21 +288,8 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
: null,
};
for (const file of fileSet.files) {
if (analyzedPaths.has(file.path)) continue;
if (
!file.text.included &&
file.text.reason === "outside-analysis-scope"
) {
state.files.push({ file, javascript: null, semantic: null });
if (file.kind === "javascript")
projection.recordUnavailableFile(
file,
"Source was not analyzed because it is outside the caller-selected source_paths.",
null,
"incomplete-module",
);
if (analyzedPaths.has(file.path) || recordOutsideSelection(file))
continue;
}
state.files.push({
file,
javascript: null,
@@ -0,0 +1,62 @@
import { expect, it } from "vitest";
import { analyzeAndAdoptJavaScriptArtifactFiles } from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js";
import { createJavaScriptSemanticGraphProjection } from "../../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js";
import { readJavaScriptArtifactFiles } from "../../../../src/artifacts/javascript/JavaScriptArtifactFiles.js";
import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javascript/JavaScriptArtifactReader.js";
import { createJavaScriptSourceAnalysis } from "../../../../src/artifacts/javascript/JavaScriptSourceAnalysis.js";
import { JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS } from "../../../../src/domain/javascript/javascriptAnalysisResourceControls.js";
import {
scanCanonicalArtifactInventory,
withArtifactResourceScope,
} from "../../../fixtures/artifactInventory.js";
import { writeJavaScriptSourceSelectionFixture } from "../../../fixtures/javascriptSourceSelection.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
it("keeps unselected sources outside the selection when cancellation follows the last selected source", async () => {
const root = await createTestTempDirectory("rea-js-selected-late-cancel-");
await writeJavaScriptSourceSelectionFixture(root);
const snapshot = await scanCanonicalArtifactInventory(root, {});
const reader = createJavaScriptArtifactReader(root, "directory");
const worker = createJavaScriptSourceAnalysis(
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
1,
);
try {
const files = await withArtifactResourceScope((scope) =>
readJavaScriptArtifactFiles(reader, snapshot, scope, undefined, [
"main.mjs",
]),
);
const controller = new AbortController();
// Cancel after the only selected source completes, before the loop
// reaches the first unselected source.
const { analysis, interruption } =
await analyzeAndAdoptJavaScriptArtifactFiles(
files,
{
analyze: async (file, budget, signal) => {
const result = await worker.analyze(file, budget, signal);
controller.abort();
return result;
},
},
createJavaScriptSemanticGraphProjection(),
async () => {},
controller.signal,
);
expect(interruption?._tag).toBe("AnalysisCancelledError");
const unselected = analysis.files.filter(
({ file }) =>
!file.text.included && file.text.reason === "outside-analysis-scope",
);
expect(unselected).toHaveLength(5);
expect(
unselected.filter((entry) => entry.analysis_failure !== undefined),
).toEqual([]);
} finally {
await worker.close();
await reader.close();
}
});