mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(errors): preserve actionable analysis diagnostics
This commit is contained in:
@@ -12,7 +12,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
11,src/domain/errors.ts:ArtifactOperationError,Artifact inventory or extraction fails,"Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...)",Exposes paths hashes and internal reason,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
|
||||
12,src/domain/errors.ts:EvidenceFileError,Evidence file operation fails,"Evidence bundle {operation} failed: {reason}",Internal reason token gives weak recovery,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
|
||||
13,src/domain/errors.ts:EvidenceIntegrityError,Evidence validation fails,Raw constructor message,May expose validation internals and lacks recovery,"Evidence is invalid or has changed. Recreate or re-import it, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Evidence state changed before the update completed. Refresh the current state and try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Missing IDs identify the session and retry; concurrent revision conflicts refresh current state.",implemented,focused assertions passed; full `npm run check` passed
|
||||
15,src/domain/errors.ts:ProcessCaptureError,Process capture fails,Raw constructor message,May expose process and cleanup internals,"Process capture could not complete. Check command access and limits, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"Analysis operation timed out after {timeoutMs}ms: {operation}",Exposes internal deadline and operation; weak recovery,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
|
||||
17,src/domain/errors.ts:HopperTimeoutError,Provider deadline expires,"Hopper did not respond within {timeoutMs}ms",Exposes provider and internal deadline,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
|
||||
|
||||
|
@@ -49,7 +49,7 @@ export const scanCanonicalArtifactInventory = async (
|
||||
(!integrity.approved || !integrity.enabled)
|
||||
)
|
||||
throw new ArtifactReaderFailure(
|
||||
"unavailable",
|
||||
"policy",
|
||||
"Integrity continuation requires explicit approval and operator policy",
|
||||
);
|
||||
const metadata = await lstat(path);
|
||||
|
||||
@@ -50,6 +50,7 @@ export class ArtifactReaderFailure extends Error {
|
||||
| "integrity"
|
||||
| "limit"
|
||||
| "path"
|
||||
| "policy"
|
||||
| "unavailable",
|
||||
message: string,
|
||||
options?: ErrorOptions,
|
||||
|
||||
@@ -24,6 +24,8 @@ export const analysisErrorRemediationAction = (
|
||||
): string => {
|
||||
if (error instanceof AnalysisInputError)
|
||||
return "Correct the listed arguments and retry.";
|
||||
if (error instanceof UnknownRegistryError && error.reason === "not-found")
|
||||
return "Check that the unknown_id belongs to this session, then retry.";
|
||||
if (error instanceof ReplayPlanStaleError)
|
||||
return "Review the rebuilt replay plan and explicitly approve its new digest.";
|
||||
if (error instanceof PermissionRequiredError)
|
||||
@@ -99,7 +101,7 @@ const artifactErrorCategory = (
|
||||
if (reason === "integrity") return "integrity_mismatch";
|
||||
if (reason === "limit") return "truncated";
|
||||
if (reason === "cancelled") return "cancelled";
|
||||
if (reason === "unavailable") return "unavailable";
|
||||
if (reason === "policy" || reason === "unavailable") return "unavailable";
|
||||
return "execution_failure";
|
||||
};
|
||||
|
||||
@@ -139,6 +141,8 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
|
||||
return evidenceFileMessage(error.reason);
|
||||
if (error instanceof InvestigationWorkspaceError)
|
||||
return investigationWorkspaceMessage(error.reason);
|
||||
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)
|
||||
return "Evidence state changed before the update completed. Refresh the current state and try again.";
|
||||
if (error instanceof ConfigurationError)
|
||||
@@ -203,8 +207,10 @@ const artifactMessage = (reason: ArtifactOperationError["reason"]): string => {
|
||||
return "Artifact is too large to process safely. Narrow the requested path or use a smaller artifact.";
|
||||
if (reason === "path")
|
||||
return "Artifact path is not allowed. Choose a path inside the artifact and try again.";
|
||||
if (reason === "policy")
|
||||
return "Artifact integrity continuation is disabled by policy. Configure REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED=true and retry only if continuing after mismatches is approved.";
|
||||
if (reason === "unavailable")
|
||||
return "Artifact format is not available on this system. Choose another artifact or supported environment.";
|
||||
return "Artifact processing is unavailable for the current target or policy. Check artifact support and required approvals.";
|
||||
if (reason === "format" || reason === "integrity")
|
||||
return "Artifact is invalid or has changed. Get a fresh copy and try again.";
|
||||
return "Artifact could not be read or written. Check file access and try again.";
|
||||
|
||||
@@ -220,6 +220,7 @@ const requestErrorDetails = (
|
||||
issues: error.issues.map((issue) => ({
|
||||
path: [...issue.path],
|
||||
reason: issue.reason,
|
||||
...(issue.message === undefined ? {} : { message: issue.message }),
|
||||
...(issue.expected === undefined ? {} : { expected: issue.expected }),
|
||||
...(issue.minimum === undefined ? {} : { minimum: issue.minimum }),
|
||||
...(issue.maximum === undefined ? {} : { maximum: issue.maximum }),
|
||||
|
||||
@@ -97,6 +97,8 @@ export interface AnalysisInputIssue {
|
||||
| "out_of_range"
|
||||
| "invalid_value"
|
||||
| "invalid_format";
|
||||
/** Schema-authored correction guidance for cross-field or custom checks. */
|
||||
readonly message?: string;
|
||||
readonly expected?: JsonValue;
|
||||
readonly minimum?: number;
|
||||
readonly maximum?: number;
|
||||
@@ -293,6 +295,7 @@ export class ArtifactOperationError extends AnalysisError {
|
||||
| "integrity"
|
||||
| "limit"
|
||||
| "path"
|
||||
| "policy"
|
||||
| "unavailable"
|
||||
| "io",
|
||||
readonly artifactDetails?: Readonly<{
|
||||
|
||||
@@ -46,7 +46,13 @@ const projectIssue = (
|
||||
expected: issue.values.filter(isSafeExpected),
|
||||
},
|
||||
];
|
||||
return [{ path, reason: "invalid_value" }];
|
||||
return [
|
||||
{
|
||||
path,
|
||||
reason: "invalid_value",
|
||||
...(issue.message.length > 0 ? { message: issue.message } : {}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const valueAtPath = (
|
||||
|
||||
+57
-1
@@ -242,6 +242,61 @@ describe("analysis error projection", () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("gives missing residual unknowns a lookup-specific remediation", () => {
|
||||
expect(
|
||||
projectAnalysisError(new UnknownRegistryError("not-found")),
|
||||
).toMatchObject({
|
||||
code: "execution_failure",
|
||||
message:
|
||||
"The requested residual unknown does not exist in this session. Check the unknown_id and try again.",
|
||||
remediation: {
|
||||
action:
|
||||
"Check that the unknown_id belongs to this session, then retry.",
|
||||
},
|
||||
details: { reason: "not-found" },
|
||||
});
|
||||
});
|
||||
|
||||
it("retains custom input guidance in the projected error details", () => {
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
new AnalysisInputError("find_changed_behavior", undefined, [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Supply either existing comparisons or one investigation_run",
|
||||
},
|
||||
]),
|
||||
),
|
||||
).toMatchObject({
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Supply either existing comparisons or one investigation_run",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("distinguishes disabled integrity continuation from format support", () => {
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
new ArtifactOperationError("inventory_artifact", "policy"),
|
||||
),
|
||||
).toMatchObject({
|
||||
code: "artifact_operation_failed",
|
||||
category: "unavailable",
|
||||
message: expect.stringContaining(
|
||||
"REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED=true",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
it("validates every closed error-reason variant against the generated contract", () => {
|
||||
const variants: AnalysisError[] = [
|
||||
...(
|
||||
@@ -251,6 +306,7 @@ describe("analysis error projection", () => {
|
||||
"integrity",
|
||||
"limit",
|
||||
"path",
|
||||
"policy",
|
||||
"unavailable",
|
||||
"io",
|
||||
] as const
|
||||
@@ -313,7 +369,7 @@ describe("analysis error projection", () => {
|
||||
).map((diagnostic) => new HopperRemoteError(9, "safe", diagnostic)),
|
||||
];
|
||||
|
||||
expect(variants).toHaveLength(38);
|
||||
expect(variants).toHaveLength(39);
|
||||
for (const variant of variants) {
|
||||
const projected = projectAnalysisError(variant);
|
||||
const parsed = analysisErrorProjectionSchema.safeParse(projected);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { artifactExtractionInputSchema } from "../src/contracts/artifactToolContracts.js";
|
||||
import { projectInputIssues } from "../src/domain/inputIssueProjection.js";
|
||||
|
||||
describe("input issue projection", () => {
|
||||
it("preserves schema-authored custom correction guidance", () => {
|
||||
const schema = z
|
||||
.object({ left: z.string().optional(), right: z.string().optional() })
|
||||
.superRefine((value, context) => {
|
||||
if (value.left === undefined && value.right === undefined)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: [],
|
||||
message: "Supply either left or right",
|
||||
});
|
||||
});
|
||||
const parsed = schema.safeParse({});
|
||||
if (parsed.success) throw new Error("expected invalid input");
|
||||
|
||||
expect(projectInputIssues(parsed.error.issues, {})).toEqual([
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message: "Supply either left or right",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects relative extraction destinations at the caller boundary", () => {
|
||||
expect(
|
||||
artifactExtractionInputSchema.safeParse({
|
||||
approved: true,
|
||||
output_root: "relative/output",
|
||||
occurrence_ids: [`occ_${"0".repeat(64)}`],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user