fix(javascript): preserve analysis applicability after interruption (#4207)

Preserve applicable artifact processing phases and entered/unvisited status after interruption. Centralize JavaScript semantic applicability for full and incremental builders; retain native byte identity, actual JavaScript unknowns and structured-file provenance.

Fixes #3948. Part of #4027.

Validation:78 distinct focused cases across10files, full static/docs, and real Joplin timeout/default public CLI/MCP controls passed. Source pins, every attempt/outlier and compact resource/token projections are retained. No target/native provider execution or speedup claim.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
YsopwnableandKaoru0822 authored and GitHub committed 2026-10-16 03:46:00 +08:00
1 parent 663d07f02c
commit 85eac7b92b
18 files changed
+3717 -93

No files matched your search

@@ -22,6 +22,8 @@ import type {
} from "../../domain/javascript/javascriptArtifactFiles.js";
import type {
AnalyzedJavaScriptArtifactFile,
JavaScriptArtifactAnalysisFailure,
JavaScriptArtifactAnalysisScope,
JavaScriptArtifactAnalysis,
JavaScriptModuleArtifactAnalysis,
JavaScriptModuleSemanticIr,
@@ -32,6 +34,7 @@ import type {
JavaScriptSourceMapObservation,
JavaScriptSourceMapOriginal,
} from "./JavaScriptArtifactAnalysisTypes.js";
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js";
import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js";
import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactReferences.js";
@@ -141,6 +144,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
fileSet.files.length - (fileSet.source_selection?.unselected_files ?? 0);
let selectedFileIndex = 0;
for (const file of fileSet.files) {
let analysisStarted = false;
try {
await setImmediate();
if (classifyOutsideAnalysisScope(file, state, projection)) {
@@ -155,6 +159,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
if (signal?.aborted === true)
throw new AnalysisCancelledError("analyze_javascript_application");
await beforeFile(file, selectedFileIndex++, selectedFileCount);
analysisStarted = true;
if (file.kind !== "javascript" || !file.text.included) {
const steps = analyzeArtifactFileSteps(file, {
state,
@@ -182,10 +187,12 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
: error instanceof AnalysisTimeoutError
? { worker_timeout_ms: error.timeoutMs }
: null;
const failure =
const failure: JavaScriptArtifactAnalysisFailure | undefined =
error === null
? undefined
: {
scope: javascriptArtifactAnalysisScopes.javascript,
status: "interrupted",
reason: `${file.path}: ${error.message}`,
error: projectAnalysisError(error),
limits,
@@ -245,15 +252,22 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
? cause
: null;
if (interruption !== null) {
const scope = javascriptArtifactAnalysisScopes[file.kind];
state.files.push({
file,
javascript: null,
semantic: null,
analysis_failure: {
reason: `${file.path}: ${interruption.message}`,
error: projectAnalysisError(interruption),
limits: null,
},
...(scope === null
? {}
: {
analysis_failure: {
scope,
status: analysisStarted ? "interrupted" : "not-started",
reason: `${file.path}: ${interruption.message}`,
error: projectAnalysisError(interruption),
limits: null,
},
}),
});
projection.recordUnavailableFile(
file,
@@ -277,25 +291,36 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
? { worker_timeout_ms: interruption.timeoutMs }
: null,
};
// This interruption has one cause. Share its immutable failure per phase
// rather than copying it for every unvisited source in a large bundle.
const failures = new Map<
JavaScriptArtifactAnalysisScope,
JavaScriptArtifactAnalysisFailure
>();
for (const file of fileSet.files) {
if (analyzedPaths.has(file.path)) continue;
if (classifyOutsideAnalysisScope(file, state, projection)) continue;
const scope = javascriptArtifactAnalysisScopes[file.kind];
let unavailable = scope === null ? undefined : failures.get(scope);
if (scope !== null && unavailable === undefined) {
unavailable = { ...failure, scope, status: "not-started" };
failures.set(scope, unavailable);
}
state.files.push({
file,
javascript: null,
semantic: null,
analysis_failure: failure,
...(unavailable === undefined ? {} : { analysis_failure: unavailable }),
});
if (file.kind === "javascript")
projection.recordUnavailableFile(
file,
failure.reason,
failure.limits,
interruption instanceof AnalysisResourceConstraintError ||
interruption instanceof AnalysisTimeoutError
? "resource-limit"
: "incomplete-module",
);
projection.recordUnavailableFile(
file,
failure.reason,
failure.limits,
interruption instanceof AnalysisResourceConstraintError ||
interruption instanceof AnalysisTimeoutError
? "resource-limit"
: "incomplete-module",
);
}
}
return { analysis: finalizeArtifactAnalysis(state), interruption };
@@ -423,13 +448,12 @@ const classifyOutsideAnalysisScope = <
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",
);
projection?.recordUnavailableFile(
file,
"Source was not analyzed because it is outside the caller-selected source_paths.",
null,
"incomplete-module",
);
return true;
};