mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
Merge pull request #1074 from morluto/fix/evidence-file-missing
fix(evidence): report missing evidence files instead of permission advice
This commit is contained in:
6 files changed
+92
-16
No files matched your search
@@ -10,10 +10,14 @@ import { err, ok, type Result } from "../domain/result.js";
|
||||
export const readJsonFile = async (
|
||||
path: string,
|
||||
): Promise<Result<unknown, EvidenceFileError>> => {
|
||||
const requestedPath = resolve(path);
|
||||
try {
|
||||
const canonicalPath = await realpath(resolve(path));
|
||||
const canonicalPath = await realpath(requestedPath);
|
||||
const stats = await lstat(canonicalPath);
|
||||
if (!stats.isFile()) return err(new EvidenceFileError("read", "not-file"));
|
||||
if (!stats.isFile())
|
||||
return err(
|
||||
new EvidenceFileError("read", "not-file", { path: requestedPath }),
|
||||
);
|
||||
const encoded = await readFile(canonicalPath);
|
||||
let decoded: unknown;
|
||||
try {
|
||||
@@ -23,11 +27,21 @@ export const readJsonFile = async (
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
return err(new EvidenceFileError("read", "invalid-json", { cause }));
|
||||
return err(
|
||||
new EvidenceFileError("read", "invalid-json", {
|
||||
cause,
|
||||
path: requestedPath,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return ok(decoded);
|
||||
} catch (cause: unknown) {
|
||||
return err(new EvidenceFileError("read", "io", { cause }));
|
||||
return err(
|
||||
new EvidenceFileError("read", missingOrIo(cause), {
|
||||
cause,
|
||||
path: requestedPath,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,8 +54,8 @@ export const writeTextFile = async (
|
||||
Result<{ readonly path: string; readonly bytes: number }, EvidenceFileError>
|
||||
> => {
|
||||
const bytes = Buffer.byteLength(encoded, "utf8");
|
||||
const requestedPath = resolve(path);
|
||||
try {
|
||||
const requestedPath = resolve(path);
|
||||
const canonicalParent = await realpath(dirname(requestedPath));
|
||||
const destination = resolve(canonicalParent, basename(requestedPath));
|
||||
const existing = await lstat(destination).catch((cause: unknown) => {
|
||||
@@ -49,9 +63,14 @@ export const writeTextFile = async (
|
||||
throw cause;
|
||||
});
|
||||
if (existing !== undefined) {
|
||||
if (!overwrite) return err(new EvidenceFileError("write", "exists"));
|
||||
if (!overwrite)
|
||||
return err(
|
||||
new EvidenceFileError("write", "exists", { path: requestedPath }),
|
||||
);
|
||||
if (!existing.isFile() || existing.isSymbolicLink())
|
||||
return err(new EvidenceFileError("write", "not-file"));
|
||||
return err(
|
||||
new EvidenceFileError("write", "not-file", { path: requestedPath }),
|
||||
);
|
||||
}
|
||||
if (overwrite)
|
||||
await writeFileAtomic(destination, encoded, {
|
||||
@@ -63,11 +82,27 @@ export const writeTextFile = async (
|
||||
return ok({ path: requestedPath, bytes });
|
||||
} catch (cause: unknown) {
|
||||
if (!overwrite && fileErrorCode(cause) === "EEXIST")
|
||||
return err(new EvidenceFileError("write", "exists", { cause }));
|
||||
return err(new EvidenceFileError("write", "io", { cause }));
|
||||
return err(
|
||||
new EvidenceFileError("write", "exists", {
|
||||
cause,
|
||||
path: requestedPath,
|
||||
}),
|
||||
);
|
||||
return err(
|
||||
new EvidenceFileError("write", missingOrIo(cause), {
|
||||
cause,
|
||||
path: requestedPath,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** A missing path or parent is a selection error, not a permission failure. */
|
||||
const missingOrIo = (cause: unknown): "missing" | "io" => {
|
||||
const code = fileErrorCode(cause);
|
||||
return code === "ENOENT" || code === "ENOTDIR" ? "missing" : "io";
|
||||
};
|
||||
|
||||
const publishNewFile = async (
|
||||
destination: string,
|
||||
encoded: string,
|
||||
|
||||
@@ -162,8 +162,7 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
|
||||
: `Evidence ${error.evidenceId} does not match the requested reference (${error.reason}). Check the expected and actual identity in the diagnostic details.`;
|
||||
if (error instanceof EvidenceIntegrityError)
|
||||
return "Evidence is invalid or has changed. Recreate or re-import it, then try again.";
|
||||
if (error instanceof EvidenceFileError)
|
||||
return evidenceFileMessage(error.reason);
|
||||
if (error instanceof EvidenceFileError) return evidenceFileMessage(error);
|
||||
if (error instanceof UnknownRegistryError && error.reason === "not-found")
|
||||
return "The requested residual unknown does not exist in this session. Check the unknown_id and try again.";
|
||||
if (error instanceof UnknownRegistryError)
|
||||
@@ -258,7 +257,14 @@ const artifactMessage = (reason: ArtifactOperationError["reason"]): string => {
|
||||
return "Artifact could not be read or written. Check file access and try again.";
|
||||
};
|
||||
|
||||
const evidenceFileMessage = (reason: EvidenceFileError["reason"]): string => {
|
||||
const evidenceFileMessage = ({
|
||||
operation,
|
||||
reason,
|
||||
}: EvidenceFileError): string => {
|
||||
if (reason === "missing")
|
||||
return operation === "read"
|
||||
? "Evidence file does not exist at the selected path. Check the path and try again."
|
||||
: "Evidence output directory does not exist. Choose an existing directory and try again.";
|
||||
if (reason === "not-file")
|
||||
return "Evidence path does not point to a regular file. Choose a file and try again.";
|
||||
if (reason === "exists")
|
||||
|
||||
@@ -259,7 +259,11 @@ const artifactStateErrorDetails = (
|
||||
};
|
||||
if (error instanceof UnknownRegistryError) return { reason: error.reason };
|
||||
if (error instanceof EvidenceFileError)
|
||||
return { operation: error.operation, reason: error.reason };
|
||||
return {
|
||||
operation: error.operation,
|
||||
reason: error.reason,
|
||||
...(error.path === undefined ? {} : { path: error.path }),
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,11 +25,15 @@ export class EvidenceReferenceError extends EvidenceIntegrityError {
|
||||
export class EvidenceFileError extends AnalysisError {
|
||||
readonly _tag = "EvidenceFileError";
|
||||
|
||||
/** Caller-selected path that failed, resolved against the working directory. */
|
||||
readonly path: string | undefined;
|
||||
|
||||
constructor(
|
||||
readonly operation: "read" | "write",
|
||||
readonly reason: "not-file" | "exists" | "invalid-json" | "io",
|
||||
options?: ErrorOptions,
|
||||
readonly reason: "not-file" | "exists" | "invalid-json" | "missing" | "io",
|
||||
options?: ErrorOptions & { readonly path?: string },
|
||||
) {
|
||||
super(`Evidence bundle ${operation} failed: ${reason}`, options);
|
||||
this.path = options?.path;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user