refactor(native): parse native inventory results once at the operation port (#10880)

This commit is contained in:
morluto authored and GitHub committed 2026-11-15 16:59:39 +08:00
1 parent 6d40eb943b
commit 66d1527a3d
4 files changed
+207 -1

No files matched your search

+2 -1
View File
@@ -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 });
});
+60
View File
@@ -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",
),
),
);
};
+48
View File
@@ -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,
);