export type FlowActionSucceededObservationActionKind = | "runtime-action" | "navigation-action"; /** * Core-owned facts retained with one action invocation for a later * host-owned terminal-success observer. Invocation and definition identity * stay on the enclosing execution request and are not duplicated here. */ export type FlowActionSucceededObservationSeed = Readonly<{ flowId: string; flowVersionId: string; flowVersionNumber: number; flowRunId: string; actionPlacementId: string; actionKind: FlowActionSucceededObservationActionKind; }>; type FlowActionSucceededObservationSeedInput = { readonly flowId: string; readonly flowVersionId: string; readonly flowVersionNumber: number; readonly flowRunId: string; readonly actionPlacementId: string; readonly actionKind: FlowActionSucceededObservationActionKind; }; const seedKeys = new Set([ "flowId", "flowVersionId", "flowVersionNumber", "flowRunId", "actionPlacementId", "actionKind", ]); const isNonBlankString = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0; const isPositiveSafeInteger = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value > 0; /** Copies trusted scalar provenance into an immutable value boundary. */ export const buildFlowActionSucceededObservationSeed = ( input: FlowActionSucceededObservationSeedInput, ): FlowActionSucceededObservationSeed => Object.freeze({ flowId: input.flowId, flowVersionId: input.flowVersionId, flowVersionNumber: input.flowVersionNumber, flowRunId: input.flowRunId, actionPlacementId: input.actionPlacementId, actionKind: input.actionKind, }); /** * Validates and copies optional analytics provenance for the expected executor. * A null result drops only analytics metadata; callers continue the action. */ export const toFlowActionSucceededObservationSeed = ( value: unknown, expectedActionKind: FlowActionSucceededObservationActionKind, ): FlowActionSucceededObservationSeed | null => { if (typeof value !== "object" || value === null) return null; const valueRecord = value as Record; const keys = Object.keys(valueRecord); if ( keys.length !== seedKeys.size || keys.some((key) => !seedKeys.has(key)) || !isNonBlankString(valueRecord.flowId) || !isNonBlankString(valueRecord.flowVersionId) || !isPositiveSafeInteger(valueRecord.flowVersionNumber) || !isNonBlankString(valueRecord.flowRunId) || !isNonBlankString(valueRecord.actionPlacementId) || valueRecord.actionKind !== expectedActionKind ) { return null; } return buildFlowActionSucceededObservationSeed({ flowId: valueRecord.flowId, flowVersionId: valueRecord.flowVersionId, flowVersionNumber: valueRecord.flowVersionNumber, flowRunId: valueRecord.flowRunId, actionPlacementId: valueRecord.actionPlacementId, actionKind: expectedActionKind, }); };