fix(javascript): classify application input selection failures by cause

analyze_javascript_application reported every input-selection problem as
an artifact fault:

- a missing input path: "Artifact could not be read or written", with no
  path or reason;
- an unreadable selected directory: the same generic io failure;
- a regular file that is neither a directory nor an .asar, and a
  --artifact-format that contradicts the selection: "Artifact is invalid
  or has changed. Get a fresh copy and try again."

Report a missing path as an invalid input_path, an unreadable selection
as access_denied, a non-directory non-.asar file as unsupported_target
with the accepted inputs as remediation, and a contradicting format as an
invalid format with the observed kind as expected value. Failures inside
a selected artifact keep their existing classification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 committed 2026-10-09 09:13:17 +00:00
1 parent f4c99ab8a5
commit 0a14bbbf05
7 files changed
+186 -18

No files matched your search

@@ -1,6 +1,12 @@
import { lstat, realpath } from "node:fs/promises";
import { constants } from "node:fs";
import { access, lstat, realpath } from "node:fs/promises";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import {
AnalysisAccessDeniedError,
AnalysisInputError,
AnalysisUnsupportedTargetError,
} from "../../domain/analysisErrorCore.js";
import { createJavaScriptArtifactReader as createReader } from "../../artifacts/javascript/JavaScriptArtifactReader.js";
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js";
@@ -58,7 +64,7 @@ export const reconstructJavaScriptArtifact = async (
};
const input = javascriptArtifactReconstructionInputSchema.parse(rawInput);
abortIfNeeded(signal);
const path = await realpath(input.input_path);
const path = await resolveSelectedInput(input.input_path);
const format = await resolveFormat(path, input);
const snapshot = await scanCanonicalArtifactInventory(path, { signal });
if (snapshot.manifest.root_format !== format)
@@ -159,6 +165,37 @@ export const reconstructJavaScriptArtifact = async (
}
};
const OPERATION = "analyze_javascript_application";
/**
* Resolve the caller-selected input. A missing or unreadable selection is a
* caller or host-permission failure, not a damaged artifact.
*/
const resolveSelectedInput = async (inputPath: string): Promise<string> => {
try {
const path = await realpath(inputPath);
await access(path, constants.R_OK);
return path;
} catch (cause: unknown) {
const code =
cause instanceof Error && "code" in cause ? String(cause.code) : "";
if (code === "EACCES" || code === "EPERM")
throw new AnalysisAccessDeniedError(OPERATION, inputPath, code, {
cause,
});
if (code === "ENOENT" || code === "ENOTDIR")
throw new AnalysisInputError(OPERATION, { cause }, [
{
path: ["input_path"],
reason: "invalid_value",
message: `No file or directory exists at the selected input path (${code}): ${inputPath}`,
},
]);
throw cause;
}
};
/** The selected input's kind, checked against the caller's requested format. */
const resolveFormat = async (
path: string,
input: JavaScriptArtifactReconstructionInput,
@@ -170,15 +207,24 @@ const resolveFormat = async (
? "asar"
: undefined;
if (observed === undefined)
throw new ArtifactReaderFailure(
"format",
`JavaScript artifact reconstruction accepts only a directory or .asar file: ${path}`,
throw new AnalysisUnsupportedTargetError(
OPERATION,
input.input_path,
"JavaScript application analysis accepts only a directory or an .asar file",
{
remediationAction:
"Select an extracted application directory, an app bundle, or an .asar file such as Contents/Resources/app.asar.",
},
);
if (input.format !== "auto" && input.format !== observed)
throw new ArtifactReaderFailure(
"format",
`Requested ${input.format} input but observed ${observed}: ${path}`,
);
throw new AnalysisInputError(OPERATION, undefined, [
{
path: ["format"],
reason: "invalid_value",
message: `Requested ${input.format} input, but the selected input is ${observed === "asar" ? "an .asar file" : "a directory"}: ${input.input_path}`,
expected: ["auto", observed],
},
]);
return observed;
};