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;
};
@@ -5,7 +5,10 @@ import type {
} from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import type { JavaScriptModuleSemanticIr } from "../../domain/javascript/javascriptModuleSemanticIr.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import type {
JavaScriptArtifactFile,
JavaScriptArtifactFileKind,
} from "../../domain/javascript/javascriptArtifactFiles.js";
import type { JsonValue } from "../../domain/jsonValue.js";
interface JavaScriptStructuredObservation {
@@ -107,6 +110,47 @@ export type JavaScriptJsonModuleObservation = JavaScriptStructuredObservation &
/** Module facts needed after file-local semantic projection has finished. */
export type { JavaScriptModuleSemanticIr } from "../../domain/javascript/javascriptModuleSemanticIr.js";
/** Applicable processing of inventoried files; native bytes remain metadata-only. */
export const javascriptArtifactAnalysisScopes = {
javascript: {
operation: "analyze-javascript-source",
authority: "ast-static-analysis",
},
"package-json": {
operation: "parse-package-json",
authority: "artifact-bytes",
},
json: { operation: "parse-json-module", authority: "artifact-bytes" },
html: {
operation: "parse-html-script-references",
authority: "artifact-bytes",
},
"source-map": {
operation: "parse-local-source-map",
authority: "artifact-bytes",
},
"native-addon": null,
} as const satisfies Record<
JavaScriptArtifactFileKind,
{
readonly operation: string;
readonly authority: "artifact-bytes" | "ast-static-analysis";
} | null
>;
export type JavaScriptArtifactAnalysisScope = NonNullable<
(typeof javascriptArtifactAnalysisScopes)[JavaScriptArtifactFileKind]
>;
/** A processing gap preserves applicability and whether the phase was entered. */
export interface JavaScriptArtifactAnalysisFailure {
readonly scope: JavaScriptArtifactAnalysisScope;
readonly status: "interrupted" | "not-started";
readonly reason: string;
readonly error: JsonValue;
readonly limits: Readonly<Record<string, JsonValue>> | null;
}
/** One relevant file plus optional AST-only JavaScript facts. */
export interface AnalyzedJavaScriptArtifactFile<
SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr,
@@ -117,11 +161,7 @@ export interface AnalyzedJavaScriptArtifactFile<
readonly ir: SemanticIr;
} | null;
/** Operational incompleteness, distinct from a malformed source file. */
readonly analysis_failure?: {
readonly reason: string;
readonly error: JsonValue;
readonly limits: Readonly<Record<string, JsonValue>> | null;
};
readonly analysis_failure?: JavaScriptArtifactAnalysisFailure;
/** Facts retained inline when their expanded graph exceeds available memory. */
readonly application_projection_failure?: {
readonly reason: string;
@@ -12,6 +12,7 @@ import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptSt
import type {
JavaScriptHtmlInlineScriptsObservation,
JavaScriptModuleArtifactAnalysis,
JavaScriptArtifactAnalysisScope,
} from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptUnanalyzedArtifactLookup } from "../../domain/javascript/javascriptArtifactFiles.js";
import { HTML_INLINE_SCRIPT_LOCATION_LIMIT } from "../../domain/javascript/htmlArtifactReferences.js";
@@ -25,7 +26,7 @@ import {
artifactObservationEvidence,
astObservationEvidence,
staticInferenceEvidence,
unavailableAstEvidence,
unavailableStaticEvidence,
} from "./JavaScriptArtifactGraphEvidence.js";
import {
artifactPathResolutionProperties,
@@ -106,11 +107,12 @@ export interface RoleAssetLinkInput {
readonly coverage?: JavaScriptArtifactGraphCoverage;
}
/** Explicit package, JavaScript, or source-map parse gap. */
/** An explicit processing/selection gap with the applicable phase's authority. */
export interface UnavailableParseInput {
readonly file: JavaScriptArtifactFile;
readonly asset: ApplicationNode;
readonly operation: string;
readonly authority: JavaScriptArtifactAnalysisScope["authority"];
readonly limitation: string;
readonly failure?: JsonValue;
readonly limits?: JavaScriptArtifactGraphCoverage["limits"];
@@ -230,7 +232,8 @@ export const addUnavailableStaticParseScope = (
? {}
: { analysis_failure: input.failure }),
},
evidence: unavailableAstEvidence({
evidence: unavailableStaticEvidence({
authority: input.authority,
sha256: input.file.sha256,
path: input.file.path,
operation: input.operation,
@@ -1,5 +1,6 @@
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactAnalysisScope } from "./JavaScriptArtifactAnalysisTypes.js";
const EXTRACTOR = {
name: "rea-javascript-artifact-reconstruction",
@@ -93,11 +94,14 @@ export const staticInferenceEvidence = (
evidence_ids: [],
});
/** Unavailable AST fact that preserves a parse, approval, or limit gap. */
export const unavailableAstEvidence = (
input: EvidenceInput & { readonly limitation: string },
/** Unavailable static fact with the authority of its applicable processing phase. */
export const unavailableStaticEvidence = (
input: EvidenceInput & {
readonly authority: JavaScriptArtifactAnalysisScope["authority"];
readonly limitation: string;
},
): ApplicationGraphEvidence => ({
authority: "ast-static-analysis",
authority: input.authority,
state: "unavailable",
confidence: "unknown",
artifact: artifactReference(input.sha256),
@@ -6,6 +6,7 @@ import {
partialApplicationCoverage,
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { freezeJsonSnapshot } from "../../domain/immutableJson.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
@@ -211,11 +212,17 @@ export const addJavaScriptArtifactFiles = (
}),
});
}
if (!file.text.included && file.text.reason === "outside-analysis-scope")
const analysisScope = javascriptArtifactAnalysisScopes[file.kind];
if (
!file.text.included &&
file.text.reason === "outside-analysis-scope" &&
analysisScope !== null
)
addUnavailableStaticParseScope(context, {
file,
asset: target,
operation: "select-javascript-sources",
authority: analysisScope.authority,
limitation:
"Source is outside the caller-selected source_paths; it was inventoried but not analyzed.",
});
@@ -223,12 +230,13 @@ export const addJavaScriptArtifactFiles = (
addUnavailableStaticParseScope(context, {
file,
asset: target,
operation: "analyze-javascript-source",
...analyzed.analysis_failure.scope,
limitation: analyzed.analysis_failure.reason,
failure: analyzed.analysis_failure.error,
limits: javaScriptAnalysisCoverageLimits(
analyzed.analysis_failure.limits,
),
details: { analysis_status: analyzed.analysis_failure.status },
});
else if (
file.kind === "javascript" &&
@@ -239,6 +247,7 @@ export const addJavaScriptArtifactFiles = (
file,
asset: target,
operation: "parse-javascript",
authority: javascriptArtifactAnalysisScopes.javascript.authority,
limitation: file.text.included
? "JavaScript syntax could not be parsed."
: `JavaScript text was unavailable: ${file.text.reason}.`,
@@ -248,6 +257,7 @@ export const addJavaScriptArtifactFiles = (
file,
asset: target,
operation: "project-javascript-application",
authority: javascriptArtifactAnalysisScopes.javascript.authority,
limitation: analyzed.application_projection_failure.reason,
failure: analyzed.application_projection_failure.error,
limits: javaScriptAnalysisCoverageLimits(
@@ -259,14 +269,14 @@ export const addJavaScriptArtifactFiles = (
addUnavailableStaticParseScope(context, {
file,
asset: target,
operation: "parse-package-json",
...javascriptArtifactAnalysisScopes["package-json"],
limitation: packageValue.limitation,
});
if (jsonValue !== undefined && jsonValue.status !== "included")
addUnavailableStaticParseScope(context, {
file,
asset: target,
operation: "parse-json-module",
...javascriptArtifactAnalysisScopes.json,
limitation: jsonValue.limitation,
});
const sourceMap = sourceMapsByPath.get(file.path);
@@ -274,7 +284,7 @@ export const addJavaScriptArtifactFiles = (
addUnavailableStaticParseScope(context, {
file,
asset: target,
operation: "parse-local-source-map",
...javascriptArtifactAnalysisScopes["source-map"],
limitation: sourceMap.limitation,
});
const inlineScripts = inlineScriptsByPath.get(file.path);
@@ -283,6 +293,7 @@ export const addJavaScriptArtifactFiles = (
file,
asset: target,
operation: "parse-html-inline-scripts",
authority: javascriptArtifactAnalysisScopes.javascript.authority,
limitation: htmlInlineScriptLimitation(inlineScripts),
limits:
inlineScripts.script_count > inlineScripts.scripts.length
@@ -20,6 +20,7 @@ import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySn
import type { ArtifactInventoryPartialObservation } from "../../domain/artifactPartialObservation.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { analyzeAndAdoptJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
import { createJavaScriptSourceAnalysis } from "../../artifacts/javascript/JavaScriptSourceAnalysis.js";
import { JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS } from "../../domain/javascript/javascriptAnalysisResourceControls.js";
import { createOwnedJavaScriptApplicationEvidenceCooperatively } from "./JavaScriptApplicationEvidence.js";
@@ -137,7 +138,7 @@ export const reconstructJavaScriptArtifact = async (
);
await reportPhase(
"parse_javascript_sources",
`Parsing and projecting ${String(files.files.length - (files.source_selection?.unselected_files ?? 0))} selected application source files`,
`Processing ${String(files.files.length - (files.source_selection?.unselected_files ?? 0))} selected application files`,
);
const semanticProjection = createJavaScriptSemanticGraphProjection({
...(input.max_semantic_nodes === undefined
@@ -168,11 +169,16 @@ export const reconstructJavaScriptArtifact = async (
semanticProjection,
async (file, completed, total) => {
abortIfNeeded(signal);
const ast =
javascriptArtifactAnalysisScopes[file.kind]?.authority ===
"ast-static-analysis";
await progress?.report({
phase: "parse_javascript_source",
phase: ast
? "parse_javascript_source"
: "process_javascript_artifact_metadata",
completed: 0,
total: 1,
message: `Parsing and projecting ${file.path} (${String(completed + 1)}/${String(total)})`,
message: `${ast ? "Parsing and projecting" : "Processing metadata for"} ${file.path} (${String(completed + 1)}/${String(total)})`,
});
abortIfNeeded(signal);
},
@@ -163,7 +163,7 @@ export interface JavaScriptSemanticGraphProjection {
readonly adoptFileProjection: (
projection: JavaScriptSemanticFileProjection,
) => void;
/** Preserve source-bound unknowns for an execution or result-retention failure. */
/** Preserve JavaScript-source unknowns; other file kinds have no semantic module. */
readonly recordUnavailableFile: (
file: JavaScriptArtifactFile,
reason: string,
@@ -346,6 +346,9 @@ export const createJavaScriptSemanticGraphProjection = (
if (projection.truncated) truncatedFiles += 1;
},
recordUnavailableFile: (file, reason, limits, unknownReason) => {
// Both full and incremental construction enter here. Metadata/native
// processing gaps belong to the application graph, never JavaScript IR.
if (file.kind !== "javascript") return;
const retainedLimits = javaScriptAnalysisCoverageLimits(limits);
for (const limit of retainedLimits) {
executionLimits.set(