/**
 * DeliveryObservation — host-domain trace record of how a sink
 * handled an emitted effect under a given externalization policy.
 *
 * This is host-domain infrastructure, NOT causal substrate truth.
 * Delivery observations live in the pipeline's in-memory trace or
 * in chunk sink files — not in the graph.
 *
 * @module DeliveryObservation
 * @see docs/design/layer-boundary.md
 */
import WarpError from "../errors/WarpError.js";
import { sortedReplacer } from "../utils/canonicalStringify.js";
import { requireNonEmptyString, validateTimestamp } from "../utils/scalarValidation.js";
import { validateOutcome, DELIVERY_MODES } from "./ExternalizationPolicy.js";
const modeSet = new Set(DELIVERY_MODES);
// ============================================================================
// Types
// ============================================================================
/**
 * DeliveryObservation — trace record of how a sink handled an emitted effect.
 */
export class DeliveryObservation {
    /** Links to the EffectEmission */
    emissionId;
    /** Which sink/adapter handled it */
    sinkId;
    outcome;
    /** Why (e.g., "replay mode"). Omitted (not null) when absent. */
    reason;
    /** Wall-clock milliseconds */
    timestamp;
    /** Execution context at delivery time */
    lens;
    /**
     * Creates an immutable DeliveryObservation.
     */
    constructor({ emissionId, sinkId, outcome, reason, timestamp, lens }) {
        requireNonEmptyString(emissionId, 'emissionId');
        requireNonEmptyString(sinkId, 'sinkId');
        const validatedOutcome = validateOutcome(outcome);
        validateTimestamp(timestamp);
        validateLens(lens);
        this.emissionId = emissionId;
        this.sinkId = sinkId;
        this.outcome = validatedOutcome;
        this.timestamp = timestamp;
        this.lens = freezeLens(lens);
        if (reason !== undefined) {
            this.reason = reason;
        }
        Object.freeze(this);
    }
}
// ============================================================================
// Validation
// ============================================================================
/**
 * Asserts that a lens is a non-null object.
 */
function requireLensObject(lens) {
    if (lens === null || lens === undefined || typeof lens !== 'object') {
        throw new WarpError('lens must be an object', 'E_VALIDATION');
    }
}
/**
 * Validates a lens has a recognized mode and boolean suppressExternal.
 */
function validateLens(lens) {
    requireLensObject(lens);
    if (!modeSet.has(lens.mode)) {
        throw new WarpError(`lens.mode must be one of: ${DELIVERY_MODES.join(', ')}`, 'E_VALIDATION');
    }
    if (typeof lens.suppressExternal !== 'boolean') {
        throw new WarpError('lens.suppressExternal must be a boolean', 'E_VALIDATION');
    }
}
// ============================================================================
// Factory
// ============================================================================
/**
 * Freezes a lens into an immutable ExternalizationPolicy snapshot.
 */
function freezeLens(lens) {
    return Object.freeze({
        mode: lens.mode,
        suppressExternal: lens.suppressExternal,
    });
}
/**
 * Creates an immutable DeliveryObservation from validated parameters.
 */
export function createDeliveryObservation({ emissionId, sinkId, outcome, reason, timestamp, lens }) {
    return new DeliveryObservation({
        emissionId, sinkId, outcome, timestamp, lens,
        ...(reason !== undefined ? { reason } : {}),
    });
}
// ============================================================================
// Canonical JSON
// ============================================================================
/**
 * Produces a deterministic JSON string for a DeliveryObservation.
 */
export function canonicalObservationJson(observation) {
    return JSON.stringify(observation, sortedReplacer); // nosemgrep: ts-no-json-stringify-in-core -- 0025B
}
