fix(errors): preserve actionable analysis diagnostics

This commit is contained in:
morluto
2026-07-29 07:18:29 +00:00
parent 969519c00f
commit 0f96bf365c
9 changed files with 119 additions and 6 deletions
+1 -1
View File
@@ -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
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
12 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
13 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
14 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
15 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. Missing IDs identify the session and retry; concurrent revision conflicts refresh current state. implemented focused assertions passed; full `npm run check` passed
16 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
17 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
18 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);
+1
View File
@@ -50,6 +50,7 @@ export class ArtifactReaderFailure extends Error {
| "integrity"
| "limit"
| "path"
| "policy"
| "unavailable",
message: string,
options?: ErrorOptions,
+8 -2
View File
@@ -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.";
+1
View File
@@ -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 }),
+3
View File
@@ -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<{
+7 -1
View File
@@ -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
View File
@@ -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);
+40
View File
@@ -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);
});
});