2026-07-17 23:57:23 +08:00
|
|
|
import { builtinModules } from "node:module";
|
2026-07-17 21:30:24 +08:00
|
|
|
import { posix } from "node:path";
|
|
|
|
|
|
|
|
|
|
import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js";
|
|
|
|
|
|
2026-08-10 14:57:44 +08:00
|
|
|
type ArtifactPathResolutionContext =
|
|
|
|
|
| "package-entrypoint"
|
|
|
|
|
| "filesystem-expression"
|
|
|
|
|
| "module-specifier"
|
|
|
|
|
| "html-reference";
|
|
|
|
|
|
|
|
|
|
type UnresolvedArtifactPathStatus =
|
|
|
|
|
| "not-found"
|
|
|
|
|
| "unavailable"
|
|
|
|
|
| "external"
|
|
|
|
|
| "rejected";
|
|
|
|
|
|
|
|
|
|
interface ArtifactPathResolutionBase {
|
2026-07-17 21:30:24 +08:00
|
|
|
readonly declared_path: string;
|
2026-08-10 14:57:44 +08:00
|
|
|
readonly resolution_context: ArtifactPathResolutionContext;
|
2026-07-17 21:30:24 +08:00
|
|
|
readonly limitations: readonly string[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 14:57:44 +08:00
|
|
|
/** Explicit outcome of bounded, artifact-confined path resolution. */
|
|
|
|
|
export type ArtifactPathResolution = ArtifactPathResolutionBase &
|
|
|
|
|
(
|
|
|
|
|
| {
|
|
|
|
|
readonly resolved_path: string;
|
|
|
|
|
readonly resolution_status: "resolved";
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
readonly resolved_path: null;
|
|
|
|
|
readonly resolution_status: UnresolvedArtifactPathStatus;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-17 21:30:24 +08:00
|
|
|
/** Values needed to resolve a declaration without filesystem access. */
|
|
|
|
|
export interface ResolveArtifactPathInput {
|
|
|
|
|
readonly declaredPath: string;
|
|
|
|
|
readonly sourcePath: string;
|
2026-08-10 14:57:44 +08:00
|
|
|
readonly context: ArtifactPathResolutionContext;
|
2026-07-17 21:30:24 +08:00
|
|
|
readonly files: ReadonlyMap<string, JavaScriptArtifactFile>;
|
|
|
|
|
readonly htmlBaseHref?: string | null;
|
2026-07-17 23:57:23 +08:00
|
|
|
readonly moduleKind?: "import" | "require";
|
2026-07-17 21:30:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EXTENSIONS = [
|
|
|
|
|
".js",
|
|
|
|
|
".cjs",
|
|
|
|
|
".mjs",
|
|
|
|
|
".ts",
|
|
|
|
|
".tsx",
|
|
|
|
|
".json",
|
|
|
|
|
".html",
|
|
|
|
|
".node",
|
|
|
|
|
];
|
|
|
|
|
const MAX_PACKAGE_DEPTH = 4;
|
2026-07-17 23:57:23 +08:00
|
|
|
const NODE_BUILTINS = new Set(
|
|
|
|
|
builtinModules.map((name) => name.replace(/^node:/u, "")),
|
|
|
|
|
);
|
2026-07-17 21:30:24 +08:00
|
|
|
|
2026-08-10 14:57:44 +08:00
|
|
|
type CandidateResolution =
|
|
|
|
|
| {
|
|
|
|
|
readonly resolvedPath: string;
|
|
|
|
|
readonly status: "resolved";
|
|
|
|
|
readonly limitations: readonly string[];
|
|
|
|
|
}
|
|
|
|
|
| {
|
|
|
|
|
readonly resolvedPath: null;
|
|
|
|
|
readonly status: UnresolvedArtifactPathStatus;
|
|
|
|
|
readonly limitations: readonly string[];
|
|
|
|
|
};
|
2026-07-17 21:30:24 +08:00
|
|
|
|
|
|
|
|
/** Resolve one declaration under its exact syntax/metadata context. */
|
|
|
|
|
export const resolveArtifactPathByContext = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
): ArtifactPathResolution => resolveAtDepth(input, 0);
|
|
|
|
|
|
|
|
|
|
const resolveAtDepth = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
packageDepth: number,
|
|
|
|
|
): ArtifactPathResolution => {
|
|
|
|
|
const rejected = rejectDeclaration(input);
|
|
|
|
|
if (rejected !== null) return rejected;
|
|
|
|
|
const candidate = contextualCandidate(input);
|
|
|
|
|
if (typeof candidate !== "string") return candidate;
|
|
|
|
|
const confined = confineCandidate(input, candidate);
|
|
|
|
|
if (typeof confined !== "string") return confined;
|
|
|
|
|
const resolved = resolveCandidate(input, confined, packageDepth);
|
2026-08-10 14:57:44 +08:00
|
|
|
return outcome(input, resolved);
|
2026-07-17 21:30:24 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const rejectDeclaration = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
): ArtifactPathResolution | null => {
|
|
|
|
|
const declared = input.declaredPath;
|
|
|
|
|
if (declared.length === 0)
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "rejected", [
|
|
|
|
|
"The declared path is empty.",
|
|
|
|
|
]);
|
2026-07-17 21:30:24 +08:00
|
|
|
if (declared.length > 4_096)
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "rejected", [
|
2026-07-17 21:30:24 +08:00
|
|
|
"The declared path exceeds the 4096-character resolution bound.",
|
|
|
|
|
]);
|
|
|
|
|
if (declared.includes("\0") || declared.includes("\\"))
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "rejected", [
|
2026-07-17 21:30:24 +08:00
|
|
|
"NUL and backslash path syntax are not admitted for canonical artifact paths.",
|
|
|
|
|
]);
|
|
|
|
|
if (/%(?:2e|2f|5c)/iu.test(declared))
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "rejected", [
|
2026-07-17 21:30:24 +08:00
|
|
|
"Encoded dot or separator bytes are rejected before artifact path resolution.",
|
|
|
|
|
]);
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const contextualCandidate = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
): string | ArtifactPathResolution => {
|
|
|
|
|
const { context } = input;
|
|
|
|
|
const declared =
|
|
|
|
|
context === "module-specifier" || context === "html-reference"
|
|
|
|
|
? stripQueryAndFragment(input.declaredPath)
|
|
|
|
|
: input.declaredPath;
|
|
|
|
|
if (context === "html-reference") return htmlCandidate(input);
|
|
|
|
|
if (context === "module-specifier") {
|
|
|
|
|
const fileUrl = fileUrlPath(declared);
|
|
|
|
|
if (fileUrl !== undefined) return fileUrl;
|
|
|
|
|
if (hasScheme(declared))
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "external", [
|
2026-07-17 21:30:24 +08:00
|
|
|
"URL and Node builtin schemes are outside static artifact module resolution.",
|
|
|
|
|
]);
|
|
|
|
|
if (!declared.startsWith(".") && !declared.startsWith("/"))
|
2026-07-17 23:57:23 +08:00
|
|
|
return bareModuleCandidate(input, declared);
|
2026-07-17 21:30:24 +08:00
|
|
|
} else if (hasScheme(declared))
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "external", [
|
2026-07-17 21:30:24 +08:00
|
|
|
"URL schemes are outside this local artifact path context.",
|
|
|
|
|
]);
|
|
|
|
|
const relative = declared.startsWith("/") ? declared.slice(1) : declared;
|
|
|
|
|
return declared.startsWith("/")
|
|
|
|
|
? relative
|
|
|
|
|
: posix.join(posix.dirname(input.sourcePath), relative);
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-17 23:57:23 +08:00
|
|
|
const bareModuleCandidate = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
declared: string,
|
|
|
|
|
): string | ArtifactPathResolution => {
|
|
|
|
|
const packageName = barePackageName(declared);
|
|
|
|
|
if (
|
|
|
|
|
packageName === null ||
|
|
|
|
|
NODE_BUILTINS.has(packageName) ||
|
|
|
|
|
declared.startsWith("#")
|
|
|
|
|
)
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "external", [
|
2026-07-17 23:57:23 +08:00
|
|
|
"The bare specifier is a Node builtin, package import map, or invalid package name.",
|
|
|
|
|
]);
|
|
|
|
|
if (declared !== packageName)
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "external", [
|
2026-07-17 23:57:23 +08:00
|
|
|
"Bare package subpaths remain unresolved unless an exact package-exports subpath model is available.",
|
|
|
|
|
]);
|
|
|
|
|
const source = input.files.get(input.sourcePath);
|
|
|
|
|
let directory = posix.dirname(input.sourcePath);
|
|
|
|
|
while (true) {
|
|
|
|
|
const candidate = posix.join(directory, "node_modules", declared);
|
|
|
|
|
if (hasContainerCandidate(input.files, candidate, source?.container_sha256))
|
|
|
|
|
return candidate;
|
|
|
|
|
if (directory === "." || directory === "") break;
|
|
|
|
|
directory = posix.dirname(directory);
|
|
|
|
|
}
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "external", [
|
2026-07-17 23:57:23 +08:00
|
|
|
"No matching bare package was inventoried in an enclosing node_modules directory.",
|
|
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const barePackageName = (specifier: string): string | null => {
|
|
|
|
|
const segments = specifier.split("/");
|
|
|
|
|
if (specifier.startsWith("@"))
|
|
|
|
|
return segments.length >= 2 && segments[0] !== "" && segments[1] !== ""
|
|
|
|
|
? `${segments[0]}/${segments[1]}`
|
|
|
|
|
: null;
|
|
|
|
|
return segments[0] === "" ? null : (segments[0] ?? null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hasContainerCandidate = (
|
|
|
|
|
files: ReadonlyMap<string, JavaScriptArtifactFile>,
|
|
|
|
|
candidate: string,
|
|
|
|
|
containerSha256: string | undefined,
|
|
|
|
|
): boolean =>
|
|
|
|
|
[...directCandidates(candidate), posix.join(candidate, "package.json")].some(
|
|
|
|
|
(path) => {
|
|
|
|
|
const file = files.get(path);
|
|
|
|
|
return (
|
|
|
|
|
file !== undefined &&
|
|
|
|
|
(containerSha256 === undefined ||
|
|
|
|
|
file.container_sha256 === containerSha256)
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-17 21:30:24 +08:00
|
|
|
const htmlCandidate = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
): string | ArtifactPathResolution => {
|
|
|
|
|
const declared = stripQueryAndFragment(input.declaredPath);
|
|
|
|
|
if (hasScheme(declared))
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "external", [
|
2026-07-17 21:30:24 +08:00
|
|
|
"External HTML references are not mapped to local artifact assets.",
|
|
|
|
|
]);
|
|
|
|
|
if (declared.startsWith("/")) return declared.slice(1);
|
|
|
|
|
const base = input.htmlBaseHref;
|
|
|
|
|
if (base === undefined || base === null || base === "")
|
|
|
|
|
return posix.join(posix.dirname(input.sourcePath), declared);
|
|
|
|
|
if (hasScheme(base))
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "external", [
|
2026-07-17 21:30:24 +08:00
|
|
|
"The document base href is external, so its script reference is not a local artifact path.",
|
|
|
|
|
]);
|
|
|
|
|
const basePath = base.startsWith("/")
|
|
|
|
|
? base.slice(1)
|
|
|
|
|
: posix.join(posix.dirname(input.sourcePath), base);
|
|
|
|
|
const baseDirectory = base.endsWith("/") ? basePath : posix.dirname(basePath);
|
|
|
|
|
return posix.join(baseDirectory, declared);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const confineCandidate = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
candidate: string,
|
|
|
|
|
): string | ArtifactPathResolution => {
|
|
|
|
|
const normalized = posix.normalize(candidate);
|
|
|
|
|
if (
|
|
|
|
|
normalized === ".." ||
|
|
|
|
|
normalized.startsWith("../") ||
|
|
|
|
|
normalized.startsWith("/")
|
|
|
|
|
)
|
2026-08-10 14:57:44 +08:00
|
|
|
return unresolvedOutcome(input, "rejected", [
|
2026-07-17 21:30:24 +08:00
|
|
|
"The resolved candidate escapes the canonical artifact root.",
|
|
|
|
|
]);
|
|
|
|
|
return normalized === "." ? "" : normalized;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resolveCandidate = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
candidate: string,
|
|
|
|
|
packageDepth: number,
|
|
|
|
|
): CandidateResolution => {
|
|
|
|
|
const source = input.files.get(input.sourcePath);
|
|
|
|
|
for (const path of directCandidates(candidate)) {
|
|
|
|
|
const target = input.files.get(path);
|
|
|
|
|
if (
|
|
|
|
|
target !== undefined &&
|
|
|
|
|
(source === undefined ||
|
|
|
|
|
target.container_sha256 === source.container_sha256)
|
|
|
|
|
)
|
|
|
|
|
return { resolvedPath: path, status: "resolved", limitations: [] };
|
|
|
|
|
}
|
|
|
|
|
if (packageDepth >= MAX_PACKAGE_DEPTH)
|
|
|
|
|
return {
|
|
|
|
|
resolvedPath: null,
|
|
|
|
|
status: "unavailable",
|
|
|
|
|
limitations: [
|
|
|
|
|
`Directory package resolution exceeded its ${String(MAX_PACKAGE_DEPTH)}-package recursion bound.`,
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
const packagePath = posix.join(candidate, "package.json");
|
|
|
|
|
const packageFile = input.files.get(packagePath);
|
|
|
|
|
if (
|
|
|
|
|
packageFile === undefined ||
|
|
|
|
|
(source !== undefined &&
|
|
|
|
|
packageFile.container_sha256 !== source.container_sha256)
|
|
|
|
|
)
|
|
|
|
|
return notFoundCandidate();
|
|
|
|
|
if (!packageFile.text.included)
|
|
|
|
|
return {
|
|
|
|
|
resolvedPath: null,
|
|
|
|
|
status: "unavailable",
|
|
|
|
|
limitations: [
|
|
|
|
|
`Directory package metadata ${packagePath} was inventoried but its text is unavailable: ${packageFile.text.reason}.`,
|
|
|
|
|
],
|
|
|
|
|
};
|
2026-07-17 23:57:23 +08:00
|
|
|
const main = packageEntry(packageFile.text.value, input.moduleKind);
|
2026-07-17 21:30:24 +08:00
|
|
|
if (main.status === "invalid")
|
|
|
|
|
return {
|
|
|
|
|
resolvedPath: null,
|
|
|
|
|
status: "unavailable",
|
|
|
|
|
limitations: [
|
|
|
|
|
`Directory package metadata ${packagePath} is not valid bounded package JSON.`,
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
if (main.status === "missing") return notFoundCandidate();
|
|
|
|
|
const nested = resolveAtDepth(
|
|
|
|
|
{
|
|
|
|
|
declaredPath: main.value,
|
|
|
|
|
sourcePath: packagePath,
|
|
|
|
|
context: "package-entrypoint",
|
|
|
|
|
files: input.files,
|
|
|
|
|
},
|
|
|
|
|
packageDepth + 1,
|
|
|
|
|
);
|
2026-08-10 14:57:44 +08:00
|
|
|
return nested.resolution_status === "resolved"
|
|
|
|
|
? {
|
|
|
|
|
resolvedPath: nested.resolved_path,
|
|
|
|
|
status: nested.resolution_status,
|
|
|
|
|
limitations: nested.limitations,
|
|
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
resolvedPath: null,
|
|
|
|
|
status: nested.resolution_status,
|
|
|
|
|
limitations: nested.limitations,
|
|
|
|
|
};
|
2026-07-17 21:30:24 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const directCandidates = (candidate: string): readonly string[] => [
|
|
|
|
|
candidate,
|
|
|
|
|
...EXTENSIONS.map((extension) => `${candidate}${extension}`),
|
|
|
|
|
...EXTENSIONS.map((extension) => posix.join(candidate, `index${extension}`)),
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-17 23:57:23 +08:00
|
|
|
const packageEntry = (
|
2026-07-17 21:30:24 +08:00
|
|
|
text: string,
|
2026-07-17 23:57:23 +08:00
|
|
|
moduleKind: ResolveArtifactPathInput["moduleKind"],
|
2026-07-17 21:30:24 +08:00
|
|
|
):
|
|
|
|
|
| { readonly status: "value"; readonly value: string }
|
|
|
|
|
| { readonly status: "missing" }
|
|
|
|
|
| { readonly status: "invalid" } => {
|
|
|
|
|
try {
|
|
|
|
|
const value: unknown = JSON.parse(text);
|
|
|
|
|
if (typeof value !== "object" || value === null)
|
|
|
|
|
return { status: "invalid" };
|
2026-07-17 23:57:23 +08:00
|
|
|
const rawExports = Reflect.get(value, "exports");
|
|
|
|
|
if (rawExports !== undefined) return packageExport(rawExports, moduleKind);
|
|
|
|
|
const preferred =
|
|
|
|
|
moduleKind === "import"
|
|
|
|
|
? [Reflect.get(value, "module"), Reflect.get(value, "main")]
|
|
|
|
|
: [Reflect.get(value, "main"), Reflect.get(value, "module")];
|
|
|
|
|
const entry = preferred.find((candidate) => candidate !== undefined);
|
|
|
|
|
if (entry === undefined) return { status: "missing" };
|
|
|
|
|
return boundedPackagePath(entry);
|
2026-07-17 21:30:24 +08:00
|
|
|
} catch {
|
|
|
|
|
return { status: "invalid" };
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-17 23:57:23 +08:00
|
|
|
const packageExport = (
|
|
|
|
|
value: unknown,
|
|
|
|
|
moduleKind: ResolveArtifactPathInput["moduleKind"],
|
|
|
|
|
):
|
|
|
|
|
| { readonly status: "value"; readonly value: string }
|
|
|
|
|
| { readonly status: "invalid" } => {
|
|
|
|
|
if (typeof value === "string") return boundedPackagePath(value);
|
|
|
|
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
|
|
|
return { status: "invalid" };
|
|
|
|
|
const root = Reflect.get(value, ".") ?? value;
|
|
|
|
|
if (typeof root === "string") return boundedPackagePath(root);
|
|
|
|
|
if (typeof root !== "object" || root === null || Array.isArray(root))
|
|
|
|
|
return { status: "invalid" };
|
|
|
|
|
const condition =
|
|
|
|
|
Reflect.get(root, moduleKind ?? "default") ?? Reflect.get(root, "default");
|
|
|
|
|
return boundedPackagePath(condition);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const boundedPackagePath = (
|
|
|
|
|
value: unknown,
|
|
|
|
|
):
|
|
|
|
|
| { readonly status: "value"; readonly value: string }
|
|
|
|
|
| { readonly status: "invalid" } =>
|
|
|
|
|
typeof value === "string" && value.length > 0 && value.length <= 4_096
|
|
|
|
|
? { status: "value", value }
|
|
|
|
|
: { status: "invalid" };
|
|
|
|
|
|
2026-07-17 21:30:24 +08:00
|
|
|
const notFoundCandidate = (): CandidateResolution => ({
|
|
|
|
|
resolvedPath: null,
|
|
|
|
|
status: "not-found",
|
|
|
|
|
limitations: [
|
|
|
|
|
"No bounded extension, directory package, or index candidate exists in the inventoried artifact container.",
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const hasScheme = (value: string): boolean =>
|
|
|
|
|
/^[A-Za-z][A-Za-z+.-]*:/u.test(value);
|
|
|
|
|
|
|
|
|
|
const fileUrlPath = (value: string): string | undefined => {
|
|
|
|
|
if (!value.startsWith("file://")) return undefined;
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(value);
|
|
|
|
|
if (url.protocol !== "file:" || url.hostname !== "") return undefined;
|
|
|
|
|
return decodeURIComponent(url.pathname).replace(/^\/+/, "");
|
|
|
|
|
} catch {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const stripQueryAndFragment = (value: string): string =>
|
|
|
|
|
value.split("#", 1)[0]?.split("?", 1)[0] ?? "";
|
|
|
|
|
|
|
|
|
|
const outcome = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
2026-08-10 14:57:44 +08:00
|
|
|
resolution: CandidateResolution,
|
|
|
|
|
): ArtifactPathResolution =>
|
|
|
|
|
resolution.status === "resolved"
|
|
|
|
|
? {
|
|
|
|
|
declared_path: input.declaredPath,
|
|
|
|
|
resolution_context: input.context,
|
|
|
|
|
resolved_path: resolution.resolvedPath,
|
|
|
|
|
resolution_status: resolution.status,
|
|
|
|
|
limitations: resolution.limitations,
|
|
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
declared_path: input.declaredPath,
|
|
|
|
|
resolution_context: input.context,
|
|
|
|
|
resolved_path: null,
|
|
|
|
|
resolution_status: resolution.status,
|
|
|
|
|
limitations: resolution.limitations,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const unresolvedOutcome = (
|
|
|
|
|
input: ResolveArtifactPathInput,
|
|
|
|
|
status: UnresolvedArtifactPathStatus,
|
2026-07-17 21:30:24 +08:00
|
|
|
limitations: readonly string[],
|
2026-08-10 14:57:44 +08:00
|
|
|
): ArtifactPathResolution =>
|
|
|
|
|
outcome(input, { resolvedPath: null, status, limitations });
|