mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(mobile): retain inventory sources at the projection boundary
This commit is contained in:
1 parent
fafc1881b5
commit
c1a0561bf3
16 files changed
+124
-61
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
type EvidenceSubjectTarget,
|
||||
} from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import type { MobileApplicationProjectionWithSources } from "../domain/mobileApplicationGraphProjection.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { requestInputError, workflowInputError } from "./workflowInputError.js";
|
||||
|
||||
@@ -23,13 +24,20 @@ interface InventoryProjectionResult {
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
||||
export interface InventoryProjectionEvidenceResult {
|
||||
readonly evidence: Evidence;
|
||||
readonly sources: readonly Evidence[];
|
||||
}
|
||||
|
||||
interface InventoryProjectionOptions<
|
||||
Input extends InventoryProjectionInput,
|
||||
Output extends InventoryProjectionResult,
|
||||
> {
|
||||
readonly rawInput: unknown;
|
||||
readonly schema: z.ZodType<Input>;
|
||||
readonly project: (input: Input) => Output;
|
||||
readonly project: (
|
||||
input: Input,
|
||||
) => MobileApplicationProjectionWithSources<Output>;
|
||||
readonly operation: string;
|
||||
readonly predicateType: string;
|
||||
readonly provider: EvidenceProvider;
|
||||
@@ -43,17 +51,18 @@ export const projectInventoryEvidence = <
|
||||
Output extends InventoryProjectionResult,
|
||||
>(
|
||||
options: InventoryProjectionOptions<Input, Output>,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
): Result<InventoryProjectionEvidenceResult, AnalysisError> => {
|
||||
const parsed = options.schema.safeParse(options.rawInput);
|
||||
if (!parsed.success)
|
||||
return err(
|
||||
requestInputError(options.operation, parsed.error, options.rawInput),
|
||||
);
|
||||
try {
|
||||
const result = options.project(parsed.data);
|
||||
const first = parsed.data.inventory_evidence[0];
|
||||
return ok(
|
||||
createEvidence(
|
||||
const projected = options.project(parsed.data);
|
||||
const result = projected.projection;
|
||||
const first = projected.sources[0];
|
||||
return ok({
|
||||
evidence: createEvidence(
|
||||
first?.subject === null || first?.subject === undefined
|
||||
? undefined
|
||||
: {
|
||||
@@ -80,7 +89,8 @@ export const projectInventoryEvidence = <
|
||||
evidenceLinks: result.source_evidence_ids,
|
||||
},
|
||||
),
|
||||
);
|
||||
sources: projected.sources,
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
cause instanceof TypeError || cause instanceof z.ZodError
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
androidApplicationProjectionInputSchema,
|
||||
projectAndroidApplication,
|
||||
} from "../../domain/android/androidApplication.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
import { ANDROID_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
|
||||
import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
|
||||
import {
|
||||
projectInventoryEvidence,
|
||||
type InventoryProjectionEvidenceResult,
|
||||
} from "../InventoryProjectionEvidence.js";
|
||||
|
||||
const OPERATION = "project_android_application_graph" as const;
|
||||
|
||||
/** Project authenticated APK inventory Evidence into Android application Evidence. */
|
||||
export const projectAndroidApplicationEvidence = (
|
||||
rawInput: unknown,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
): Result<InventoryProjectionEvidenceResult, AnalysisError> => {
|
||||
return projectInventoryEvidence({
|
||||
rawInput,
|
||||
schema: androidApplicationProjectionInputSchema,
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
appleApplicationProjectionInputSchema,
|
||||
projectAppleApplication,
|
||||
} from "../../domain/apple/appleApplication.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
import { APPLE_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
|
||||
import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
|
||||
import {
|
||||
projectInventoryEvidence,
|
||||
type InventoryProjectionEvidenceResult,
|
||||
} from "../InventoryProjectionEvidence.js";
|
||||
|
||||
const OPERATION = "project_apple_application_graph" as const;
|
||||
|
||||
/** Project authenticated IPA or macOS app inventory Evidence into Apple application evidence. */
|
||||
export const projectAppleApplicationEvidence = (
|
||||
rawInput: unknown,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
): Result<InventoryProjectionEvidenceResult, AnalysisError> => {
|
||||
return projectInventoryEvidence({
|
||||
rawInput,
|
||||
schema: appleApplicationProjectionInputSchema,
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
harmonyApplicationProjectionInputSchema,
|
||||
projectHarmonyApplication,
|
||||
} from "../../domain/harmony/harmonyApplication.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
import { HARMONY_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
|
||||
import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
|
||||
import {
|
||||
projectInventoryEvidence,
|
||||
type InventoryProjectionEvidenceResult,
|
||||
} from "../InventoryProjectionEvidence.js";
|
||||
|
||||
const OPERATION = "project_harmony_application_graph" as const;
|
||||
|
||||
/** Project authenticated HarmonyOS package inventory Evidence into application evidence. */
|
||||
export const projectHarmonyApplicationEvidence = (
|
||||
rawInput: unknown,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
): Result<InventoryProjectionEvidenceResult, AnalysisError> => {
|
||||
return projectInventoryEvidence({
|
||||
rawInput,
|
||||
schema: harmonyApplicationProjectionInputSchema,
|
||||
|
||||
@@ -133,7 +133,7 @@ describe("content identity and occurrence facts", () => {
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
}).projection;
|
||||
expect(
|
||||
projection.components.javascript.map((component) => component.path),
|
||||
).toEqual(["Demo.app/Contents/Resources/a.js"]);
|
||||
|
||||
@@ -93,7 +93,7 @@ export const registerApplicationCommands = (
|
||||
await import("../application/android/AndroidApplicationService.js");
|
||||
const result = projectAndroidApplicationEvidence(input);
|
||||
return result.ok
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value) }
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value.evidence) }
|
||||
: result;
|
||||
},
|
||||
});
|
||||
@@ -109,7 +109,7 @@ export const registerApplicationCommands = (
|
||||
await import("../application/apple/AppleApplicationService.js");
|
||||
const result = projectAppleApplicationEvidence(input);
|
||||
return result.ok
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value) }
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value.evidence) }
|
||||
: result;
|
||||
},
|
||||
});
|
||||
@@ -125,7 +125,7 @@ export const registerApplicationCommands = (
|
||||
await import("../application/harmony/HarmonyApplicationService.js");
|
||||
const result = projectHarmonyApplicationEvidence(input);
|
||||
return result.ok
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value) }
|
||||
? { ok: true, value: jsonValueSchema.parse(result.value.evidence) }
|
||||
: result;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { prefixedDigestSchema } from "../digests.js";
|
||||
import { nativeRuntimeConvention } from "../nativeConvention.js";
|
||||
import {
|
||||
applicationInventoryProjectionInputSchema,
|
||||
type MobileApplicationProjectionWithSources,
|
||||
bridgeCandidateCoverageSchema,
|
||||
projectCartesianCandidates,
|
||||
projectedBridgeCandidateSchema,
|
||||
@@ -85,7 +86,7 @@ type Component = z.infer<typeof componentSchema>;
|
||||
/** Project exact APK paths and hashes without decoding or executing target code. */
|
||||
export const projectAndroidApplication = (
|
||||
input: AndroidApplicationProjectionInput,
|
||||
): AndroidApplicationProjectionResult => {
|
||||
): MobileApplicationProjectionWithSources<AndroidApplicationProjectionResult> => {
|
||||
const parsed = androidApplicationProjectionInputSchema.parse(input);
|
||||
const { evidence, inventory } = parseArtifactInventoryEvidence(
|
||||
parsed.inventory_evidence,
|
||||
@@ -149,10 +150,13 @@ export const projectAndroidApplication = (
|
||||
),
|
||||
limitations,
|
||||
};
|
||||
return androidApplicationProjectionResultSchema.parse({
|
||||
...withoutId,
|
||||
projection_id: `adp_${projectionDigest(withoutId, "Android application projection")}`,
|
||||
});
|
||||
return {
|
||||
projection: androidApplicationProjectionResultSchema.parse({
|
||||
...withoutId,
|
||||
projection_id: `adp_${projectionDigest(withoutId, "Android application projection")}`,
|
||||
}),
|
||||
sources: evidence,
|
||||
};
|
||||
};
|
||||
|
||||
const classify = (all: readonly Component[]) => ({
|
||||
|
||||
@@ -161,7 +161,7 @@ const inventoryEvidence = (
|
||||
};
|
||||
|
||||
const project = (evidence: Evidence) =>
|
||||
projectAppleApplication({ inventory_evidence: [evidence] });
|
||||
projectAppleApplication({ inventory_evidence: [evidence] }).projection;
|
||||
|
||||
const macho = (path: string): FixtureEntry => ({ path, format: "mach-o" });
|
||||
|
||||
@@ -849,7 +849,7 @@ it.each(["file", "slice"] as const)(
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
}).projection;
|
||||
expect(projection.unavailable_components).toEqual([
|
||||
{
|
||||
path: "Payload/Fixture.app/libentry.dylib",
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
bridgeCandidateCoverageSchema,
|
||||
projectCartesianCandidates,
|
||||
} from "../bridgeCandidateProjection.js";
|
||||
import type { MobileApplicationProjectionWithSources } from "../mobileApplicationGraphProjection.js";
|
||||
import {
|
||||
projectedComponentSchema,
|
||||
projectedInventoryEntries,
|
||||
@@ -113,7 +114,7 @@ type RootFormat = (typeof APPLE_ROOT_FORMATS)[number];
|
||||
/** Project exact Apple application paths and hashes without parsing or executing target code. */
|
||||
export const projectAppleApplication = (
|
||||
input: AppleApplicationProjectionInput,
|
||||
): AppleApplicationProjectionResult => {
|
||||
): MobileApplicationProjectionWithSources<AppleApplicationProjectionResult> => {
|
||||
const parsed = appleApplicationProjectionInputSchema.parse(input);
|
||||
const { evidence, inventory } = parseArtifactInventoryEvidence(
|
||||
parsed.inventory_evidence,
|
||||
@@ -200,10 +201,13 @@ export const projectAppleApplication = (
|
||||
},
|
||||
limitations,
|
||||
};
|
||||
return appleApplicationProjectionResultSchema.parse({
|
||||
...withoutId,
|
||||
projection_id: appleProjectionId(withoutId),
|
||||
});
|
||||
return {
|
||||
projection: appleApplicationProjectionResultSchema.parse({
|
||||
...withoutId,
|
||||
projection_id: appleProjectionId(withoutId),
|
||||
}),
|
||||
sources: evidence,
|
||||
};
|
||||
};
|
||||
|
||||
/** Identify the complete projection, including source Evidence provenance. */
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
applicationInventoryProjectionInputSchema,
|
||||
bridgeCandidateCoverageSchema,
|
||||
projectCartesianCandidates,
|
||||
type MobileApplicationProjectionWithSources,
|
||||
projectedBridgeCandidateSchema,
|
||||
projectedComponentSchema,
|
||||
projectedInventoryEntries,
|
||||
@@ -78,7 +79,7 @@ type Component = z.infer<typeof componentSchema>;
|
||||
/** Project exact HarmonyOS package paths and hashes without decoding or executing target code. */
|
||||
export const projectHarmonyApplication = (
|
||||
input: HarmonyApplicationProjectionInput,
|
||||
): HarmonyApplicationProjectionResult => {
|
||||
): MobileApplicationProjectionWithSources<HarmonyApplicationProjectionResult> => {
|
||||
const parsed = harmonyApplicationProjectionInputSchema.parse(input);
|
||||
const { evidence, inventory } = parseArtifactInventoryEvidence(
|
||||
parsed.inventory_evidence,
|
||||
@@ -145,10 +146,13 @@ export const projectHarmonyApplication = (
|
||||
),
|
||||
limitations,
|
||||
};
|
||||
return harmonyApplicationProjectionResultSchema.parse({
|
||||
...withoutId,
|
||||
projection_id: `hmp_${projectionDigest(withoutId, "HarmonyOS application projection")}`,
|
||||
});
|
||||
return {
|
||||
projection: harmonyApplicationProjectionResultSchema.parse({
|
||||
...withoutId,
|
||||
projection_id: `hmp_${projectionDigest(withoutId, "HarmonyOS application projection")}`,
|
||||
}),
|
||||
sources: evidence,
|
||||
};
|
||||
};
|
||||
|
||||
const harmonyRootFormat = (format: string): RootFormat => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
hasCompleteInventoryCoverage,
|
||||
type ParsedInventorySet,
|
||||
} from "./artifactInventoryEvidence.js";
|
||||
import { evidenceSchema } from "./evidence.js";
|
||||
import { evidenceSchema, type Evidence } from "./evidence.js";
|
||||
import { digestSchema } from "./digests.js";
|
||||
import { prefixedDigestSchema } from "./digests.js";
|
||||
import {
|
||||
@@ -44,6 +44,12 @@ export const unavailableComponentSchema = z.strictObject({
|
||||
|
||||
export type ProjectedComponent = z.infer<typeof projectedComponentSchema>;
|
||||
|
||||
/** A projection and the authenticated source Evidence used to derive it. */
|
||||
export interface MobileApplicationProjectionWithSources<Projection> {
|
||||
readonly projection: Projection;
|
||||
readonly sources: readonly Evidence[];
|
||||
}
|
||||
|
||||
/** Path-pair hypotheses connecting managed content to a native library. */
|
||||
export const projectedBridgeCandidateSchema = <
|
||||
Basis extends readonly [string, ...string[]],
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
applicationToolContract,
|
||||
} from "../../contracts/applicationToolContracts.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
import type { InventoryProjectionEvidenceResult } from "../../application/InventoryProjectionEvidence.js";
|
||||
import { recordSessionEvidenceSources } from "../sessionEvidence.js";
|
||||
import { logToolExecution } from "../toolLogging.js";
|
||||
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
|
||||
@@ -47,7 +47,9 @@ const registerProjection = (context: {
|
||||
readonly server: EvidenceMcpServer;
|
||||
readonly options: ApplicationToolRegistration;
|
||||
readonly contract: (typeof APPLICATION_TOOL_CONTRACTS)[number];
|
||||
readonly project: (input: unknown) => Result<Evidence, AnalysisError>;
|
||||
readonly project: (
|
||||
input: unknown,
|
||||
) => Result<InventoryProjectionEvidenceResult, AnalysisError>;
|
||||
}): void => {
|
||||
const { server, options, contract, project } = context;
|
||||
server.registerTool(
|
||||
@@ -69,27 +71,16 @@ const registerProjection = (context: {
|
||||
return server.delivery.toCallToolResult(result, contract);
|
||||
const recordedSources = recordSessionEvidenceSources(
|
||||
options.recordEvidence,
|
||||
inventoryEvidenceSources(input),
|
||||
result.value.sources,
|
||||
);
|
||||
if (!recordedSources.ok)
|
||||
return server.delivery.toCallToolResult(recordedSources, contract);
|
||||
return recordResult(
|
||||
{ ...options, delivery: server.delivery },
|
||||
contract,
|
||||
result.value,
|
||||
result.value.evidence,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const inventoryEvidenceSources = (input: unknown): readonly Evidence[] => {
|
||||
if (
|
||||
typeof input !== "object" ||
|
||||
input === null ||
|
||||
!("inventory_evidence" in input) ||
|
||||
!Array.isArray(input.inventory_evidence)
|
||||
)
|
||||
return [];
|
||||
return input.inventory_evidence as readonly Evidence[];
|
||||
};
|
||||
@@ -313,7 +313,7 @@ describe("Android bridge candidate coverage", () => {
|
||||
|
||||
if (!result.ok) throw result.error;
|
||||
const projection = androidApplicationProjectionResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
result.value.evidence.normalized_result,
|
||||
);
|
||||
expect(projection.components.dex).toHaveLength(1_001);
|
||||
expect(projection.bridge_candidates).toHaveLength(1_001);
|
||||
@@ -346,7 +346,7 @@ describe("Android bridge candidate coverage", () => {
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const projection = androidApplicationProjectionResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
result.value.evidence.normalized_result,
|
||||
);
|
||||
|
||||
expect(projection.components.dex).toHaveLength(129);
|
||||
|
||||
@@ -66,8 +66,11 @@ async function createCompleteAppleProjection() {
|
||||
});
|
||||
|
||||
if (!result.ok) throw new TypeError("Could not project fixture inventory");
|
||||
expect(result.value.sources.map(({ evidence_id }) => evidence_id)).toEqual(
|
||||
fragments.map(({ evidence_id }) => evidence_id),
|
||||
);
|
||||
const projection = appleApplicationProjectionResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
result.value.evidence.normalized_result,
|
||||
);
|
||||
expect([...projection.source_evidence_ids].sort()).toEqual(
|
||||
fragments.map(({ evidence_id }) => evidence_id).sort(),
|
||||
@@ -481,7 +484,7 @@ describe("Apple application projection completeness", () => {
|
||||
if (!result.ok) throw result.error;
|
||||
expect(
|
||||
appleApplicationProjectionResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
result.value.evidence.normalized_result,
|
||||
),
|
||||
).toMatchObject({
|
||||
application_roots: [applicationRoot],
|
||||
|
||||
@@ -12,6 +12,7 @@ import { afterEach, expect, it } from "vitest";
|
||||
|
||||
import { createDirectAnalysis } from "../../../src/composition/directAnalysis.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { fragmentInventoryEvidence } from "../../fixtures/artifactEvidence.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
@@ -84,9 +85,10 @@ it("advertises and executes Android and Apple inventory projections", async () =
|
||||
const apkInventory = parseEvidence(
|
||||
await runProviderAnalysis(await writeApk(root), "inventory_artifact", {}),
|
||||
);
|
||||
const apkSources = fragmentInventoryEvidence(apkInventory, 2).toReversed();
|
||||
const android = await client.callTool({
|
||||
name: "project_android_application_graph",
|
||||
arguments: { inventory_evidence: [apkInventory] },
|
||||
arguments: { inventory_evidence: apkSources },
|
||||
});
|
||||
if (android.isError === true)
|
||||
throw new Error(JSON.stringify(android, null, 2));
|
||||
@@ -94,6 +96,38 @@ it("advertises and executes Android and Apple inventory projections", async () =
|
||||
operation: "project_android_application_graph",
|
||||
provider: { id: "rea-android-application" },
|
||||
});
|
||||
const androidEvidence = parseEvidence(android.structuredContent);
|
||||
const androidBundle = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
expect(androidBundle.structuredContent).toMatchObject({
|
||||
result: {
|
||||
records: expect.arrayContaining([...apkSources, androidEvidence]),
|
||||
},
|
||||
});
|
||||
|
||||
const tamperedSource = {
|
||||
...apkSources[0]!,
|
||||
evidence_id: apkSources[1]!.evidence_id,
|
||||
};
|
||||
const rejected = await client.callTool({
|
||||
name: "project_android_application_graph",
|
||||
arguments: { inventory_evidence: [tamperedSource] },
|
||||
});
|
||||
expect(rejected.isError).toBe(true);
|
||||
const bundleAfterRejectedSource = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
expect(bundleAfterRejectedSource.structuredContent).toMatchObject({
|
||||
result: {
|
||||
records: expect.arrayContaining([...apkSources, androidEvidence]),
|
||||
},
|
||||
});
|
||||
expect(bundleAfterRejectedSource.structuredContent).toEqual(
|
||||
androidBundle.structuredContent,
|
||||
);
|
||||
|
||||
const ipaInventory = parseEvidence(
|
||||
await runProviderAnalysis(await writeIpa(root), "inventory_artifact", {}),
|
||||
|
||||
@@ -13,16 +13,17 @@ import { createDirectAnalysis } from "../../src/composition/directAnalysis.js";
|
||||
import type { AnalysisError } from "../../src/domain/analysisErrorBase.js";
|
||||
import { type Evidence, parseEvidence } from "../../src/domain/evidence.js";
|
||||
import type { Result } from "../../src/domain/result.js";
|
||||
import type { InventoryProjectionEvidenceResult } from "../../src/application/InventoryProjectionEvidence.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
|
||||
const { runProviderAnalysis } = createDirectAnalysis({});
|
||||
|
||||
/** Assert a projection succeeded and return its Evidence for schema parsing. */
|
||||
export const requireSuccessfulProjection = (
|
||||
result: Result<Evidence, AnalysisError>,
|
||||
result: Result<InventoryProjectionEvidenceResult, AnalysisError>,
|
||||
): Evidence => {
|
||||
if (!result.ok) throw result.error;
|
||||
return result.value;
|
||||
return result.value.evidence;
|
||||
};
|
||||
|
||||
/** Write one synthetic ZIP application package from literal entries. */
|
||||
@@ -61,7 +62,7 @@ export const inventoryApplicationPackage = async (
|
||||
export const requireDeterministicProjection = <Schema extends z.ZodType>(
|
||||
project: (input: {
|
||||
inventory_evidence: readonly Evidence[];
|
||||
}) => Result<Evidence, AnalysisError>,
|
||||
}) => Result<InventoryProjectionEvidenceResult, AnalysisError>,
|
||||
inventory: Evidence,
|
||||
schema: Schema,
|
||||
): z.output<Schema> => {
|
||||
|
||||
Reference in new issue
Block a user