mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(reference): project import failures through the canonical error contract
This commit is contained in:
1 parent
4345a45982
commit
ddb3987bb4
6 files changed
+121
-89
No files matched your search
@@ -404,6 +404,12 @@ output, not the amount of source examined. A shallow tree or many exclusions can
|
||||
still produce a large summary; depth 0 removes the outline but preserves the
|
||||
other facts. No fixed output-size or runtime improvement is promised.
|
||||
|
||||
Import failures use the shared error contract (`code`, `category`, `message`,
|
||||
`retryable`, and `remediation`). When reads completed before failure,
|
||||
`details.diagnostics.partial` retains the reader facts, with file bytes as
|
||||
arrays of byte values. Unresolved cleanup uses `cleanup_incomplete` and
|
||||
retains the source failure and resource paths in `details`.
|
||||
|
||||
The import records hashes and metadata for the supplied files, separately
|
||||
from observations of the current app. For a directory inside a Git checkout,
|
||||
it records the HEAD of the nearest enclosing repository; whether the working
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
projectReferenceSourceImportError,
|
||||
type ReferenceSourceImportError,
|
||||
} from "./ReferenceSourceImportTypes.js";
|
||||
import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js";
|
||||
import type { ReferenceSourceRead } from "../reference/ReferenceSourceReaderTypes.js";
|
||||
|
||||
describe("reference-source import error projection", () => {
|
||||
@@ -11,7 +12,7 @@ describe("reference-source import error projection", () => {
|
||||
const expectedCategories = {
|
||||
cancelled: "cancelled",
|
||||
"invalid-root": "invalid_input",
|
||||
unsupported: "unsupported_host",
|
||||
unsupported: "unsupported_provider",
|
||||
io: "execution_failure",
|
||||
parse: "execution_failure",
|
||||
} as const;
|
||||
@@ -21,6 +22,9 @@ describe("reference-source import error projection", () => {
|
||||
code: code as ReferenceSourceImportError["code"],
|
||||
message: "EIO while reading /selected/source/src/main.ts",
|
||||
});
|
||||
expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(projected.category).toBe(category);
|
||||
expect(projected.message).toContain(
|
||||
"EIO while reading /selected/source/src/main.ts",
|
||||
@@ -63,18 +67,22 @@ describe("reference-source import error projection", () => {
|
||||
message: expect.stringContaining(
|
||||
"Traversal cancelled after reading src/main.ts",
|
||||
),
|
||||
cleanup: {
|
||||
reason: "directory close rejected",
|
||||
code: "cleanup_incomplete",
|
||||
details: {
|
||||
cleanup: "incomplete",
|
||||
cleanup_reason: "directory close rejected",
|
||||
resources: ["/selected/source"],
|
||||
},
|
||||
partial: {
|
||||
root: "/selected/source",
|
||||
entries: [{ path: "src/main.ts", bytes }],
|
||||
diagnostics: {
|
||||
reference_source_code: "cancelled",
|
||||
partial: {
|
||||
root: "/selected/source",
|
||||
entries: [{ path: "src/main.ts", bytes: Array.from(bytes) }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(projected.message).toContain("Cleanup failed during import");
|
||||
expect(projected.message).toContain("directory close rejected");
|
||||
expect(projected.message).toContain("/selected/source");
|
||||
expect(projected.message).not.toContain("remains unconfirmed");
|
||||
expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,19 @@
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
projectAnalysisError,
|
||||
type AnalysisErrorProjection,
|
||||
} from "../domain/analysisErrorProjection.js";
|
||||
import type {
|
||||
AnalysisError,
|
||||
AnalysisErrorOptions,
|
||||
AnalysisCleanupObservation,
|
||||
} from "../domain/analysisErrorBase.js";
|
||||
import type { ReferenceSourcePolicy } from "../domain/referenceSourcePolicy.js";
|
||||
import type { AnalysisCleanupObservation } from "../domain/analysisErrorBase.js";
|
||||
import type { ReferenceSourceRead } from "../reference/ReferenceSourceReaderTypes.js";
|
||||
|
||||
/** Typed expected failure returned by historical-source imports. */
|
||||
@@ -13,56 +27,80 @@ export interface ReferenceSourceImportError {
|
||||
}
|
||||
|
||||
const IMPORT_FAILURE_GUIDANCE = {
|
||||
cancelled: {
|
||||
category: "cancelled",
|
||||
message:
|
||||
"Reference source import was cancelled. Start it again when ready.",
|
||||
},
|
||||
"invalid-root": {
|
||||
category: "invalid_input",
|
||||
message:
|
||||
"Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory.",
|
||||
},
|
||||
unsupported: {
|
||||
category: "unsupported_host",
|
||||
message:
|
||||
"Safe no-follow file opens are unavailable on this host. Import the source tree with REA on Linux (including WSL) or macOS.",
|
||||
},
|
||||
io: {
|
||||
category: "execution_failure",
|
||||
message:
|
||||
"Reference source files could not be read. Check directory permissions and try again.",
|
||||
},
|
||||
parse: {
|
||||
category: "execution_failure",
|
||||
message:
|
||||
"Reference source could not be indexed. Check that the source tree is readable, then try again.",
|
||||
},
|
||||
} satisfies Record<
|
||||
ReferenceSourceImportError["code"],
|
||||
{ readonly category: string; readonly message: string }
|
||||
>;
|
||||
cancelled:
|
||||
"Reference source import was cancelled. Start it again when ready.",
|
||||
"invalid-root":
|
||||
"Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory.",
|
||||
unsupported:
|
||||
"Safe no-follow file opens are unavailable on this host. Import the source tree with REA on Linux (including WSL) or macOS.",
|
||||
io: "Reference source files could not be read. Check directory permissions and try again.",
|
||||
parse:
|
||||
"Reference source could not be indexed. Check that the source tree is readable, then try again.",
|
||||
} satisfies Record<ReferenceSourceImportError["code"], string>;
|
||||
|
||||
/** Safe CLI projection for a historical-source import failure. */
|
||||
/** Canonical CLI/MCP projection, retaining source facts under diagnostic details. */
|
||||
export const projectReferenceSourceImportError = (
|
||||
error: ReferenceSourceImportError,
|
||||
): Readonly<{
|
||||
category: string;
|
||||
message: string;
|
||||
cleanup?: AnalysisCleanupObservation;
|
||||
partial?: ReferenceSourceRead;
|
||||
}> => {
|
||||
const primary = IMPORT_FAILURE_GUIDANCE[error.code];
|
||||
const cleanupMessage =
|
||||
error.cleanup === undefined
|
||||
? ""
|
||||
: ` Cleanup failed during import for ${error.cleanup.resources.join(", ")}: ${error.cleanup.reason}.`;
|
||||
return {
|
||||
category: primary.category,
|
||||
message: `${error.message} ${primary.message}${cleanupMessage}`,
|
||||
): AnalysisErrorProjection => {
|
||||
const guidance = IMPORT_FAILURE_GUIDANCE[error.code];
|
||||
const message = `${error.message} ${guidance}`;
|
||||
const partial = error.partial;
|
||||
const options: AnalysisErrorOptions = {
|
||||
cause: error.cause,
|
||||
...(error.cleanup === undefined ? {} : { cleanup: error.cleanup }),
|
||||
...(error.partial === undefined ? {} : { partial: error.partial }),
|
||||
diagnostics: {
|
||||
reference_source_code: error.code,
|
||||
...(partial === undefined
|
||||
? {}
|
||||
: {
|
||||
partial: {
|
||||
root: partial.root,
|
||||
...(partial.rootFailure === undefined
|
||||
? {}
|
||||
: { rootFailure: { ...partial.rootFailure } }),
|
||||
entries: partial.entries.map((entry) =>
|
||||
entry.status === "read" && entry.kind === "file"
|
||||
? { ...entry, bytes: Array.from(entry.bytes) }
|
||||
: { ...entry },
|
||||
),
|
||||
bytesRead: partial.bytesRead,
|
||||
limitations: [...partial.limitations],
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
let primary: AnalysisError;
|
||||
switch (error.code) {
|
||||
case "cancelled":
|
||||
primary = new AnalysisCancelledError("import_reference_source", options);
|
||||
break;
|
||||
case "invalid-root":
|
||||
primary = new AnalysisInputError(
|
||||
"import_reference_source",
|
||||
{ ...options, userMessage: message },
|
||||
[{ path: ["root"], reason: "invalid_value", message: error.message }],
|
||||
);
|
||||
break;
|
||||
case "unsupported":
|
||||
primary = new AnalysisCapabilityUnavailableError(
|
||||
"filesystem",
|
||||
"import_reference_source",
|
||||
error.message,
|
||||
{ ...options, userMessage: message, remediationAction: guidance },
|
||||
);
|
||||
break;
|
||||
case "io":
|
||||
case "parse":
|
||||
primary = new AnalysisOutputError(
|
||||
"import_reference_source",
|
||||
error.message,
|
||||
{ ...options, userMessage: message, remediationAction: guidance },
|
||||
);
|
||||
break;
|
||||
}
|
||||
// Cancellation uses a shared fixed message; the producing boundary's reason
|
||||
// is caller evidence and remains useful for every category.
|
||||
return { ...projectAnalysisError(primary), message };
|
||||
};
|
||||
|
||||
/** Import a caller-selected local source tree as historical reference. */
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isCliOperationFailure } from "../cliLogging.js";
|
||||
|
||||
const referenceSourceImportFailureSchema = z.strictObject({
|
||||
error: z.literal("Import failed"),
|
||||
category: z.enum([
|
||||
"cancelled",
|
||||
"invalid_input",
|
||||
"unsupported_host",
|
||||
"execution_failure",
|
||||
]),
|
||||
message: z.string().min(1),
|
||||
cleanup: z
|
||||
.strictObject({
|
||||
reason: z.string().min(1),
|
||||
resources: z.array(z.string().min(1)),
|
||||
})
|
||||
.optional(),
|
||||
partial: z.unknown().optional(),
|
||||
});
|
||||
|
||||
/** Identify the historical-source import command's failed operation outputs. */
|
||||
export const isReferenceSourceImportCliFailure = (value: unknown): boolean =>
|
||||
referenceSourceImportFailureSchema.safeParse(value).success ||
|
||||
isCliOperationFailure(value);
|
||||
@@ -19,11 +19,10 @@ import {
|
||||
nativeUiDeadlineField,
|
||||
} from "../domain/native/nativeDeadlines.js";
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { isCliOperationFailure, logCliCommand } from "../cliLogging.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { swiftSymbolsSchema } from "../contracts/native/nativeToolContracts.js";
|
||||
import type { Logger } from "pino";
|
||||
import { isReferenceSourceImportCliFailure } from "./referenceSourceImportStatus.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { registerNativeCallCommands } from "./nativeCallCommands.js";
|
||||
|
||||
@@ -339,7 +338,7 @@ const registerReferenceSourceCommand = (
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
isReferenceSourceImportCliFailure,
|
||||
isCliOperationFailure,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,15 +4,16 @@ import { join } from "node:path";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { analysisCliErrorEnvelopeSchema } from "../../../src/contracts/errorSchemas.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const unsupportedHostOutput = {
|
||||
const unsupportedHostOutput = expect.objectContaining({
|
||||
error: "Import failed",
|
||||
category: "unsupported_host",
|
||||
category: "unsupported_provider",
|
||||
message: expect.stringContaining(
|
||||
"Safe no-follow file opens are unavailable on this host.",
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const permissionChecksUnavailable =
|
||||
process.platform === "win32" || process.getuid?.() === 0;
|
||||
@@ -120,6 +121,12 @@ describe("compiled reference-source import preflight failures", () => {
|
||||
} else {
|
||||
expect(result.json).toMatchObject(invalidRootOutput);
|
||||
}
|
||||
const projection = scenario.fullOutput
|
||||
? (result.json as { error: unknown }).error
|
||||
: result.json;
|
||||
expect(
|
||||
analysisCliErrorEnvelopeSchema.safeParse(projection).success,
|
||||
).toBe(true);
|
||||
const messagePath = scenario.fullOutput ? "error.message" : "message";
|
||||
expect(result.json).toHaveProperty(
|
||||
messagePath,
|
||||
|
||||
Reference in new issue
Block a user