/** * Pure derivation of the `decision.made` + `signal.outcome` event-log events from the ONE assembled * action-result pass — a pure `(input) => EmittableEvent[]` mapper, so the call * site emits the returned array in one synchronous loop and there is no partial pair on throw. * * The outcome classification, decision-window selection, and field projection are NOT reimplemented: * we reuse the pure builders ({@link buildSignalEvaluated} → {@link buildDecisionMade} / * {@link buildSignalOutcomes}), which are pure and already battle-tested, then project their records * onto the event contract. Body lines are deterministic narratives; reasoning (decision.made) and any * error text reach `redact`; the raw scanner `meta` bag rides one JSON string attribute, never * interpreted — it is operator-authored content and must not carry secrets. Whether it is scrubbed * depends on where it lands: the on-disk event ring and the OTLP exporter carry it as the scanner * wrote it, while the live telemetry websocket redacts every string attribute on the way out and * drops this one whole, unmarked, while no redactor is registered. * * The only side effects are the serializers' rate-limited WARNs when a risk-gate or meta bag * cannot be emitted whole — silent loss on a trust surface is worse than a log line. The returned * events are unaffected by them; the loss itself is on the wire as `senpi.risk.gates_truncated` / * `senpi.signal.meta_truncated`. */ import { type PositionIdResolver } from "../telemetry/event-builders.js"; import { type AttrsFor, type EventName } from "../utils/event-catalog.js"; import { type Logger, type LogLevel } from "../utils/logger.js"; import type { RiskGateSnapshot } from "../telemetry/event-builders.js"; /** * A fully-built event ready to hand to `logger.event(name, level, { body, attributes, redact })`. * * Distributive over the event name so the `attributes` bag is the per-event {@link AttrsFor} shape, * not a loose scalar map: a builder that constructs an `EmittableEvent<"order.placed">` is checked * against `OrderEventAttrs` at construction, and a misnamed/wrong-typed attribute is a compile error * at the builder. The default `N = EventName` is the full union — what a heterogeneous array carries. */ export type EmittableEvent = N extends EventName ? { readonly name: N; readonly level: LogLevel; readonly body: string; readonly attributes?: AttrsFor; readonly redact?: Record; /** A thrown value when this event is also a fault — folded onto the record as `exception.*`. */ readonly error?: unknown; } : never; /** * Build then emit a single {@link EmittableEvent} through a logger, with build + emit guarded as one * unit so neither a throwing builder nor a telemetry failure can disturb the host (trading/scan/ * lifecycle) path. The builder runs INSIDE the guard (passed as a thunk, not evaluated at the call * site), so call sites need no surrounding try/catch. A `null` build result is a no-op — builders * that drop an event (no asset, no summary) return `null` and need no separate check. The event * carries its name alongside the matching typed `attributes` bag, so they flow straight into * `logger.event`'s `AttrsFor` contract with NO cast. `logger.event` is itself internally guarded. */ export declare function emitEvent(logger: Logger, build: () => EmittableEvent | null): void; /** * Build then emit an array of events in order, with the build and the whole loop guarded as one unit * (see {@link emitEvent}). The builder runs inside the guard, so call sites need no surrounding * try/catch. A throw during build or any emit aborts the rest silently — telemetry never throws into * the host path. */ export declare function emitEvents(logger: Logger, build: () => readonly EmittableEvent[]): void; /** The action-result pass, in the envelope `emitActionResultEvents` assembles. */ export interface ActionResultEventInput { readonly payload: unknown; readonly nowMs: number; readonly resolvePositionId?: PositionIdResolver; } /** * The outcome of serializing one pass's gate bag: the attribute value, plus whether what came out is * the whole bag. `truncated` is the honesty marker — without it, a bag that shed `metrics` and one * that never had any read identically, and a dropped bag reads as "this pass carried no snapshot". */ export interface SerializedRiskGates { /** The JSON array string, or `undefined` when even the bare projection would not fit. */ readonly json?: string; /** True when the emitted bag is not the full projection — fields shed, or nothing emitted at all. */ readonly truncated: boolean; } /** * The gate bag as a JSON array string, with a flag saying whether anything was lost getting there. * * The logger caps every string attribute at {@link MAX_ATTR_VALUE_CHARS}, and a cap applied to JSON * yields unparseable garbage — so the budget is spent here instead: shed `metrics`, then `reason`, * and give up rather than emit a fragment. What comes out is always valid JSON or nothing. * * An empty `gates` array serializes to `"[]"` on purpose: "the gates ran and none are configured" * is a fact the drill-down needs, distinct from the absent attribute's "there is no snapshot". * * A `metrics` bag that `JSON.stringify` refuses (a BigInt, a throwing `toJSON`) fails only its own * tier — the metrics-less tiers below it still produce a bag. That fall is a producer bug, not a * size problem, so it warns on its own flag: letting it fire the budget warning would both misname * the fault and burn the one-shot flag a genuine overflow later needs. * * `log` is injectable so the once-per-process warnings can be observed in a test; production call * sites take the default. */ export declare function serializeRiskGates(gates: readonly RiskGateSnapshot[], log?: Pick): SerializedRiskGates; /** Where a bag came from, so the drop names the scanner and asset an operator has to go fix. */ export interface SignalMetaSource { readonly scannerId?: string; readonly asset?: string; } /** A bag's JSON attribute value, plus the wire marker that says a bag arrived and was lost. */ export interface SerializedSignalMeta { /** The bag's JSON, present only when the whole bag could be carried. */ readonly json?: string; /** The bag arrived but could not be carried — surfaced as `senpi.signal.meta_truncated`. */ readonly dropped: boolean; } /** * The scanner's `meta` bag as ONE JSON string attribute, plus whether a bag arrived and was lost. * * The bag is scanner-defined and NOT interpreted here: no key is lifted, dropped, or reordered — the * JSON is emitted whole or not at all. A bag over the per-value attribute cap is therefore dropped * rather than truncated (a cut JSON string is unparseable) and rather than thinned (choosing which * scanner keys survive would be interpretation). An empty bag legitimately serializes to `"{}"`, but * that value only says a bag arrived: ingest normalizes a missing bag to `{}`, so a scanner that sent * an empty one and a scanner that sent none both land here as `{}` and cannot be told apart. A drop * is NOT silent on the wire either: `dropped` raises `senpi.signal.meta_truncated`, so an absent * attribute WITH the marker (bag lost) is distinguishable from an absent attribute without one (no * bag at all). * * The warn payload carries the bag's origin and its key count — the bag's own content is scanner * free-text and never safe as an attribute. `log` is injectable so the warns are observable. */ export declare function serializeSignalMeta(meta: Record, source?: SignalMetaSource, log?: Pick): SerializedSignalMeta; /** * Build the full event array for one action-result pass: `decision.made` first (when the pass carried * a decision), then one `signal.outcome` per evaluated signal. A failed outcome escalates the * signal.outcome level to `error` (the catalog floor is INFO); the error text rides `redact`. */ export declare function deriveActionResultEvents(input: ActionResultEventInput): EmittableEvent[]; //# sourceMappingURL=event-derivation.d.ts.map