mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
195 lines
6.6 KiB
TypeScript
195 lines
6.6 KiB
TypeScript
import { z } from "zod";
|
|
import type { ExecutionOptions } from "./AnalysisProvider.js";
|
|
import type { WebRuntimePort } from "./WebRuntimePort.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisOutputError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
|
import { jsonObjectSchema } from "../domain/jsonValue.js";
|
|
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import {
|
|
observeWebExecutionInputSchema,
|
|
webExecutionSchema,
|
|
webExecutionObservationSchema,
|
|
} from "../domain/webExecution.js";
|
|
import {
|
|
inspectWebEventListenersInputSchema,
|
|
webEventListenersSchema,
|
|
webEventListenersObservationSchema,
|
|
} from "../domain/webEventListeners.js";
|
|
import {
|
|
completeProviderEvidence,
|
|
keepIncompleteObservation,
|
|
} from "./ProviderOperationExecution.js";
|
|
|
|
/** Shared public workflows keep instrumentation and listener inspection independently selectable. */
|
|
export class WebRuntimeService {
|
|
constructor(readonly provider: WebRuntimePort) {}
|
|
|
|
/** Observe externally triggered execution; selection authorizes the declared instrumentation. */
|
|
async observe(
|
|
rawInput: unknown,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<Evidence, AnalysisError>> {
|
|
const operation = "observe_web_execution";
|
|
const parsed = parseInput(
|
|
observeWebExecutionInputSchema,
|
|
rawInput,
|
|
operation,
|
|
options,
|
|
);
|
|
if (!parsed.ok) return parsed;
|
|
const result = await this.provider.observeExecution(parsed.value, options);
|
|
if (!result.ok)
|
|
return this.retainRuntimePartial(
|
|
result.error,
|
|
operation,
|
|
"Provider retained a non-JSON incomplete execution observation.",
|
|
(partial) => {
|
|
if (!("instrumentation" in partial) || !("window" in partial))
|
|
return undefined;
|
|
const verified = webExecutionObservationSchema.safeParse(partial);
|
|
if (!verified.success) return undefined;
|
|
return this.evidence(operation, parsed.value, verified.data);
|
|
},
|
|
);
|
|
const verified = webExecutionSchema.safeParse(result.value);
|
|
if (!verified.success)
|
|
return err(new AnalysisOutputError(operation, verified.error.message));
|
|
return this.evidence(operation, parsed.value, verified.data, options);
|
|
}
|
|
|
|
/** Inspect one selected node's listener declarations without dispatching an event. */
|
|
async inspect(
|
|
rawInput: unknown,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<Evidence, AnalysisError>> {
|
|
const operation = "inspect_web_event_listeners";
|
|
const parsed = parseInput(
|
|
inspectWebEventListenersInputSchema,
|
|
rawInput,
|
|
operation,
|
|
options,
|
|
);
|
|
if (!parsed.ok) return parsed;
|
|
const result = await this.provider.inspectEventListeners(
|
|
parsed.value,
|
|
options,
|
|
);
|
|
if (!result.ok)
|
|
return this.retainRuntimePartial(
|
|
result.error,
|
|
operation,
|
|
"Provider retained a non-JSON incomplete event-listener observation.",
|
|
(partial) => {
|
|
if (!("selected_node" in partial) || !("listeners" in partial))
|
|
return undefined;
|
|
const verified =
|
|
webEventListenersObservationSchema.safeParse(partial);
|
|
if (!verified.success) return undefined;
|
|
return this.evidence(operation, parsed.value, verified.data);
|
|
},
|
|
);
|
|
const verified = webEventListenersSchema.safeParse(result.value);
|
|
if (!verified.success)
|
|
return err(new AnalysisOutputError(operation, verified.error.message));
|
|
return this.evidence(operation, parsed.value, verified.data, options);
|
|
}
|
|
|
|
/** Promote a schema-valid partial, or keep a JSON snapshot of an incomplete one. */
|
|
private retainRuntimePartial(
|
|
error: AnalysisError,
|
|
operation: string,
|
|
nonJsonReason: string,
|
|
promote: (
|
|
partial: NonNullable<AnalysisError["partialObservation"]>,
|
|
) => Result<Evidence, AnalysisError> | undefined,
|
|
): Result<Evidence, AnalysisError> {
|
|
const partial = error.partialObservation;
|
|
if (partial !== undefined && !("evidence_id" in partial)) {
|
|
const evidence = promote(partial);
|
|
if (evidence !== undefined) {
|
|
if (evidence.ok) {
|
|
error.partialObservation = evidence.value;
|
|
return err(error);
|
|
}
|
|
}
|
|
}
|
|
return err(keepIncompleteObservation(error, operation, nonJsonReason));
|
|
}
|
|
|
|
private evidence(
|
|
operation: string,
|
|
input:
|
|
| z.infer<typeof observeWebExecutionInputSchema>
|
|
| z.infer<typeof inspectWebEventListenersInputSchema>,
|
|
result:
|
|
| z.infer<typeof webExecutionObservationSchema>
|
|
| z.infer<typeof webEventListenersObservationSchema>,
|
|
options?: ExecutionOptions,
|
|
): Result<Evidence, AnalysisError> {
|
|
if (
|
|
result.target.target_id !== input.target_id ||
|
|
("observation_ms" in input &&
|
|
(!("window" in result) ||
|
|
result.window.requested_ms !== input.observation_ms)) ||
|
|
("selector" in input &&
|
|
(!("selected_node" in result) ||
|
|
result.selected_node.selector !== input.selector))
|
|
)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
operation,
|
|
"observation_ms" in input
|
|
? "Provider changed the selected target or requested window."
|
|
: "Provider changed the selected target or node selector.",
|
|
),
|
|
);
|
|
if (
|
|
input.allowed_origins.length > 0 &&
|
|
!input.allowed_origins.includes(result.target.origin)
|
|
)
|
|
return err(
|
|
new AnalysisOutputError(
|
|
operation,
|
|
"Provider returned a document outside the selected origin scope.",
|
|
),
|
|
);
|
|
return completeProviderEvidence(
|
|
createEvidence(undefined, this.provider.identity(), {
|
|
predicateType: `rea.${operation.replaceAll("_", "-")}`,
|
|
operation,
|
|
parameters: jsonObjectSchema.parse(input),
|
|
result,
|
|
confidence: "observed",
|
|
authority: "external-service",
|
|
limitations: result.limitations,
|
|
}),
|
|
options?.signal,
|
|
);
|
|
}
|
|
}
|
|
|
|
const parseInput = <T>(
|
|
schema: z.ZodType<T>,
|
|
rawInput: unknown,
|
|
operation: string,
|
|
options: ExecutionOptions | undefined,
|
|
): Result<T, AnalysisError> => {
|
|
if (options?.signal?.aborted)
|
|
return err(
|
|
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
|
|
);
|
|
const parsed = schema.safeParse(rawInput);
|
|
return parsed.success
|
|
? ok(parsed.data)
|
|
: err(
|
|
analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput, {
|
|
cause: parsed.error,
|
|
}),
|
|
);
|
|
};
|