mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
refactor(native): parse native inventory results once at the operation port (#10880)
This commit is contained in:
1 parent
6d40eb943b
commit
66d1527a3d
4 files changed
+207
-1
No files matched your search
@@ -41,7 +41,8 @@ consumers would still need the typed value.
|
||||
four readers today. `NativeDispatchMetadataInspection`'s `.strict()` row
|
||||
check is removed.
|
||||
3. **The port parses once: `executeFact`.** The application port module
|
||||
exports `executeFact(port, contract, parameters, options)`. It runs the
|
||||
`src/application/AnalysisOperationFacts.ts` exports
|
||||
`executeFact(port, contract, parameters, options)`. It runs the
|
||||
operation through the given port's `execute` and parses the result with the
|
||||
contract once, returning
|
||||
`{ execution: AnalysisExecution; fact: Fact }`. A mismatch is an
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
listNamesFact,
|
||||
listProceduresFact,
|
||||
listStringsFact,
|
||||
searchProceduresFact,
|
||||
searchStringsFact,
|
||||
} from "../domain/native/nativeOperationFacts.js";
|
||||
import { ok } from "../domain/result.js";
|
||||
import { executeFact } from "./AnalysisOperationFacts.js";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type AnalysisOperationPort,
|
||||
} from "./AnalysisProvider.js";
|
||||
|
||||
const provider = { id: "fixture", name: "Fixture", version: "1" };
|
||||
|
||||
const portReturning = (
|
||||
result: JsonValue,
|
||||
calls: string[] = [],
|
||||
): AnalysisOperationPort => ({
|
||||
execute: async (operation) => {
|
||||
calls.push(operation);
|
||||
return ok(createAnalysisExecution(result, provider));
|
||||
},
|
||||
});
|
||||
|
||||
const nameRow = { address: "0x1000", value: "main", provider_extra: 7 };
|
||||
const stringRow = { address: "0x2000", value: "hello", provider_extra: 7 };
|
||||
|
||||
it.each([
|
||||
[listNamesFact, nameRow],
|
||||
[listProceduresFact, nameRow],
|
||||
[searchProceduresFact, nameRow],
|
||||
[listStringsFact, stringRow],
|
||||
[searchStringsFact, stringRow],
|
||||
] as const)(
|
||||
"parses %s once, dropping unknown row fields from the fact but not the execution",
|
||||
async (contract, row) => {
|
||||
const calls: string[] = [];
|
||||
const executed = await executeFact(
|
||||
portReturning([row], calls),
|
||||
contract,
|
||||
{},
|
||||
);
|
||||
expect(calls).toEqual([contract.operation]);
|
||||
if (!executed.ok) throw executed.error;
|
||||
expect(executed.value.fact).toEqual([
|
||||
{ address: row.address, value: row.value },
|
||||
]);
|
||||
// Evidence is built from the execution, which keeps the source value.
|
||||
expect(executed.value.execution.result).toEqual([row]);
|
||||
},
|
||||
);
|
||||
|
||||
it("refuses a malformed result with field diagnostics and the observed execution", async () => {
|
||||
const executed = await executeFact(
|
||||
portReturning([{ address: 4096, value: "main" }]),
|
||||
listNamesFact,
|
||||
{ limit: 1 },
|
||||
);
|
||||
expect(executed.ok).toBe(false);
|
||||
if (executed.ok) return;
|
||||
expect(executed.error).toBeInstanceOf(AnalysisOutputError);
|
||||
expect(executed.error).toMatchObject({
|
||||
operation: "list_names",
|
||||
reason: "Provider returned an invalid name inventory",
|
||||
diagnostics: {
|
||||
contract_issues: [
|
||||
{
|
||||
path: [0, "address"],
|
||||
code: "invalid_type",
|
||||
expected: "string",
|
||||
received: "number",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(executed.error.partialObservation).toMatchObject({
|
||||
operation: "list_names",
|
||||
parameters: { limit: 1 },
|
||||
normalized_result: [{ address: 4096, value: "main" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the port's own failure unchanged", async () => {
|
||||
const failure = new AnalysisOutputError("list_names", "unavailable");
|
||||
const executed = await executeFact(
|
||||
{ execute: async () => ({ ok: false, error: failure }) },
|
||||
listNamesFact,
|
||||
{},
|
||||
);
|
||||
expect(executed).toEqual({ ok: false, error: failure });
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
import { outputContractDiagnostics } from "../domain/native/nativeProviderOutput.js";
|
||||
import type { NativeOperationFactContract } from "../domain/native/nativeOperationFacts.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type {
|
||||
AnalysisExecution,
|
||||
AnalysisOperation,
|
||||
AnalysisOperationPort,
|
||||
ExecutionOptions,
|
||||
} from "./AnalysisProvider.js";
|
||||
|
||||
/** A completed execution and its result parsed once against its named contract. */
|
||||
export interface AnalysisFactExecution<Fact> {
|
||||
/** The provider's execution; its result stays the source value. */
|
||||
readonly execution: AnalysisExecution;
|
||||
readonly fact: Fact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one native operation through `port` and parse its result once against
|
||||
* the named contract. The call goes through the port's own `execute`, so a
|
||||
* wrapping port observes it as any other call. A mismatch is an
|
||||
* `AnalysisOutputError` naming the failed fields, keeping the execution as
|
||||
* partial observed Evidence.
|
||||
*/
|
||||
export const executeFact = async <Operation extends AnalysisOperation, Fact>(
|
||||
port: AnalysisOperationPort,
|
||||
contract: NativeOperationFactContract<Operation, Fact>,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<AnalysisFactExecution<Fact>, AnalysisError>> => {
|
||||
const executed = await port.execute(contract.operation, parameters, options);
|
||||
if (!executed.ok) return executed;
|
||||
const parsed = contract.schema.safeParse(executed.value.result);
|
||||
if (parsed.success)
|
||||
return ok({ execution: executed.value, fact: parsed.data });
|
||||
return err(
|
||||
new AnalysisOutputError(
|
||||
contract.operation,
|
||||
`Provider returned an invalid ${contract.subject}`,
|
||||
{
|
||||
cause: parsed.error,
|
||||
diagnostics: outputContractDiagnostics(
|
||||
parsed.error,
|
||||
executed.value.result,
|
||||
),
|
||||
},
|
||||
).retainPartialObservation(
|
||||
createAnalysisExecutionEvidence(
|
||||
contract.operation,
|
||||
parameters,
|
||||
executed.value,
|
||||
"observed",
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { z } from "zod";
|
||||
import { nativeInventoryRowsSchemas } from "./nativeInventory.js";
|
||||
|
||||
/**
|
||||
* One native operation's provider-neutral result contract with its exact fact
|
||||
* type. Readers name the contract they need; nothing selects one by catalog
|
||||
* order. Row schemas are lenient: fields they do not name are dropped from
|
||||
* the typed fact, while Evidence keeps the provider's source value.
|
||||
*/
|
||||
export interface NativeOperationFactContract<Operation extends string, Fact> {
|
||||
readonly operation: Operation;
|
||||
/** What the result holds, for failure messages. */
|
||||
readonly subject: string;
|
||||
readonly schema: z.ZodType<Fact>;
|
||||
}
|
||||
|
||||
const factContract = <Operation extends string, Fact>(
|
||||
operation: Operation,
|
||||
subject: string,
|
||||
schema: z.ZodType<Fact>,
|
||||
): NativeOperationFactContract<Operation, Fact> =>
|
||||
Object.freeze({ operation, subject, schema });
|
||||
|
||||
export const listNamesFact = factContract(
|
||||
"list_names",
|
||||
"name inventory",
|
||||
nativeInventoryRowsSchemas.names,
|
||||
);
|
||||
export const listProceduresFact = factContract(
|
||||
"list_procedures",
|
||||
"procedure inventory",
|
||||
nativeInventoryRowsSchemas.names,
|
||||
);
|
||||
export const searchProceduresFact = factContract(
|
||||
"search_procedures",
|
||||
"procedure search result",
|
||||
nativeInventoryRowsSchemas.names,
|
||||
);
|
||||
export const listStringsFact = factContract(
|
||||
"list_strings",
|
||||
"string inventory",
|
||||
nativeInventoryRowsSchemas.strings,
|
||||
);
|
||||
export const searchStringsFact = factContract(
|
||||
"search_strings",
|
||||
"string search result",
|
||||
nativeInventoryRowsSchemas.strings,
|
||||
);
|
||||
Reference in new issue
Block a user