/** * Event contract types + pure builders: bus-event payloads → telemetry records. * * Each record is one atomic "work unit": plain, immutable, serializable data with no methods or * builder dance; variants are modelled with a discriminator field plus optionals. The types are * transport-agnostic — nothing here knows about spans, attributes, or any wire format. * * The builders read a loosely-typed payload defensively (the `asObject`/`str`/`num`/`bool`/ * `toEpochMs` helpers below) and return a fully-populated record (or `null` when the event cannot * yield one). Redaction is NOT done here — records are populated faithfully; a downstream mapper is * the single redaction seam. * * Identity (service, recipe, runtime, strategy address, boot id) is stamped by the identity layer * and is never on a record — see {@link IdentityEnvelope}. Timestamps are epoch milliseconds; ISO * strings (and, for a scan result, an epoch-ms number) from the bus are converted once here, so * consumers never parse dates. */ import type { RiskFailureKind, RiskGateStatus } from "../health/types.js"; /** A plain object, or `undefined` for anything else (null, array, primitive). */ export declare function asObject(value: unknown): Record | undefined; /** A non-empty string, or `undefined`. */ export declare function str(value: unknown): string | undefined; /** A finite number, coercing numeric strings; `undefined` otherwise. */ export declare function num(value: unknown): number | undefined; /** A boolean, or `undefined` (so absence stays distinct from `false`). */ export declare function bool(value: unknown): boolean | undefined; /** * ISO string or epoch-ms number → epoch ms. Falls back to `fallbackMs` when the value is missing * or unparseable, so a record always carries a usable timestamp instead of `NaN`. */ export declare function toEpochMs(value: unknown, fallbackMs: number): number; /** * Trade direction as the bus reports it. The alias itself is non-null; sites that admit a * non-directional signal widen it to `TradeDirection | null` (e.g. {@link SignalSummary}). */ export type TradeDirection = "LONG" | "SHORT"; /** * Lightweight projection of a scanner signal as it appears on the bus, including the explainable * `factors`/`meta` bags so the audit trail keeps the "why". `meta` is scanner-defined and is neither * interpreted nor scrubbed on its way out: the event mapper emits it verbatim as the * `senpi.signal.meta` attribute, so whatever a scanner puts in it is what reaches the wire. */ export interface SignalSummary { readonly signalType: string; readonly asset: string; readonly direction: TradeDirection | null; readonly score: number; /** Signal time, epoch ms. */ readonly timestamp: number; readonly factors?: Record; readonly meta?: Record; } /** Coarse terminal classification of a scanner execution. */ export type ScannerRunOutcome = "complete" | "skipped" | "error"; /** * One scanner execution attempt (the work unit). Folds the bus's `scanner:run:start` + * terminal `scanner:run:{complete,skip,error}` events into a single atomic record so a * duration is always derivable (`endedAt - startedAt`). Skip/error early-exit paths can * terminate without a paired `scanner:run:start` event; in that case `startedAt` is filled * with the run's attempt timestamp (which equals `endedAt`), so duration is 0 rather than absent. * The skipped/disabled distinction lives on `outcome`/`statusDetail`, not on a missing start. */ export interface ScannerRunRecord { readonly type: "scanner_run"; readonly scannerId: string; /** Telemetry-owned per-scan-run id; threads scan → pass into one trace. */ readonly runId?: string; readonly outcome: ScannerRunOutcome; /** Raw engine status: `ok`|`heartbeat`|`skipped`|`halted`|`disabled`|`error`. */ readonly statusDetail?: string; /** * Run start, epoch ms (from `scanner:run:start`). Required: on a no-start early-exit path it is * set to the attempt timestamp (= `endedAt`), so a `Duration` is always derivable. */ readonly startedAt: number; /** Run completion, epoch ms (terminal event timestamp). */ readonly endedAt: number; readonly scannedCount?: number; readonly actionableCount?: number; readonly signalCount?: number; readonly signals?: readonly SignalSummary[]; /** Per-scanner aggregated run metrics/details (`ScanResult.summary`). */ readonly summary?: Record; /** Explanation for a non-OK status (skip/error). */ readonly reason?: string; /** Error message when `outcome === "error"`. */ readonly error?: string; /** Error class name (`Error.name`) when `outcome === "error"` — the error-grouping key. */ readonly errorName?: string; } /** How the decision was produced. */ export type DecisionMode = "llm" | "script" | "agent" | "rule" | "none"; /** Decision produced by the decision layer for an evaluation pass. */ export interface SignalDecision { readonly execute: boolean; /** Action type, e.g. `"OPEN_POSITION"`. */ readonly actionType: string; /** Concrete action instance name. */ readonly actionName?: string; /** Distinguishes a vestigial rule "decision" from a real LLM judgment. */ readonly mode?: DecisionMode; readonly confidence: number; /** Bar `confidence` was compared against (`min_confidence`); makes the below_confidence boundary reconstructable. */ readonly confidenceThreshold?: number; readonly reasoning: string; /** LLM-path auditing (from `ActionDecision.decisionTelemetry`). */ readonly model?: string; readonly tokensUsed?: number | null; readonly durationMs?: number; } /** Coarse result of acting on an evaluated signal. */ export type SignalOutcomeResult = "executed" | "rejected" | "skipped" | "below_confidence" | "error"; /** * Outcome of acting on a **single** signal within an evaluation pass. Outcomes are per-signal: the * action loops over the batch producing one result each and decrements margin as it goes, so a * later signal's outcome only makes sense alongside the earlier ones. * * `result` is the coarse classification. `reasonCode` is an open string for the granular per-signal * reasons (e.g. `risk_gate_COOLDOWN`, `insufficient_margin`, `below_min_notional`, * `position_open_failed`, `no_slots`, `invalid_direction`); keeping it an open string means new * reasons can be carried without changing this type. */ export interface SignalOutcome { readonly result: SignalOutcomeResult; /** * Cross-flow position.id when this signal relates to a live (open or just-submitted) * position — read from the PositionIdRegistry at record time, regardless of the outcome * classification (a rejected/skipped signal on a *held* asset still carries that * position's id; only a signal on an asset with no position resolves to nothing). * Optional on every record that carries it: the id is a best-effort realtime grouping * hint; authoritative post-hoc correlation anchors on `orderId` against the venue * fills ledger. */ readonly positionId?: string; readonly summary?: string; readonly reasonCode?: string; readonly reason?: string; readonly error?: string; /** Error class name (`Error.name`, e.g. `"ZodError"`) — the natural error-grouping key. */ readonly errorName?: string; readonly leverage?: number; readonly marginAmount?: number; /** Which precedence rule sized the margin (the {@link MarginSource} breadcrumb). */ readonly marginSource?: string; /** Applied percent of withdrawable — present only for the percent-based margin sources. */ readonly marginPct?: number; readonly notionalValue?: number; readonly orderId?: string; readonly entryPrice?: number; readonly size?: number; /** Order-fill success flag from `CreatePositionOrderResult.filled` (boolean, not a quantity). */ readonly filled?: boolean | null; /** Maker vs taker fill (`CreatePositionOrderResult.executionAsMaker`); `null` when unfilled. */ readonly executionAsMaker?: boolean | null; readonly requiredMarginAmount?: number; readonly availableMarginAmount?: number; } /** * One evaluated signal paired with its outcome. Pairing (rather than two index-aligned arrays) * keeps signal and outcome structurally bound, so no filter/sort/map can silently misalign them. */ export interface SignalEvaluation { readonly signal: SignalSummary; readonly outcome: SignalOutcome; /** Per-signal work window, epoch ms — stamped by the action loop (absent on older payloads). */ readonly startedAt?: number; readonly endedAt?: number; } /** * A risk gate's verdict. Aliased to the runtime's own enum rather than re-spelled: a re-spelling * would drift silently, and a verdict this layer does not recognize is a verdict it drops. */ export type RiskGateSnapshotStatus = RiskGateStatus; /** * Why a gate's evaluation failed. Aliased to the runtime's own enum for the same reason as * {@link RiskGateSnapshotStatus}: the healthy sentinel is `"none"`, and the one consumer that tests * for it ({@link "../actions/event-derivation.js"}) must break at compile time if the enum renames * or drops that member rather than start reporting every healthy gate as a failure. */ export type RiskGateSnapshotFailureKind = RiskFailureKind; /** * One risk gate's evaluation, as it rides the action-result payload under `_riskSnapshot`. * Structurally the runtime's `RiskGateEvaluation`; re-declared here because the telemetry layer * parses it defensively out of an unknown payload rather than importing the runtime type. * * `evaluationOk: false` / `fallbackApplied: true` mark a fail-closed fallback — the gate could not * be evaluated, so it reads CLOSED. That is a different statement from a real breach, and the two * are indistinguishable without these flags. */ export interface RiskGateSnapshot { readonly gateId: string; readonly gateName?: string; readonly status: RiskGateSnapshotStatus; readonly reason?: string; readonly evaluationOk?: boolean; readonly fallbackApplied?: boolean; /** Why the evaluation failed (`transport`, `empty_data`, …); `none` when it did not. */ readonly failureKind?: RiskGateSnapshotFailureKind; readonly metrics?: Record; } /** * One signal-evaluation work unit: the runtime hands a scan pass's batch to the action, which * decides once, then acts on each signal serially. The whole pass is one record — a 3-signal batch * is ONE record with three `evaluations`, not three records. Nesting the decision and per-signal * outcomes keeps them bound together without a separate join key. */ export interface SignalEvaluatedRecord { readonly type: "signal_evaluated"; readonly scannerId?: string; /** * The `ActionType` the pass's action stamped on its `ActionResult`. Read at the pass level rather * than off {@link SignalDecision}, which is absent on a decision-less pass — every per-signal * outcome still needs to say which action produced it. */ readonly actionType?: string; /** Telemetry-owned per-scan-run id; threads scan → pass into one trace. */ readonly runId?: string; /** Id of the scan that produced the signals this pass evaluated; absent when it carried none. */ readonly tickId?: string; /** Id of the intake acceptance that admitted the signal; absent for an internally produced one. */ readonly correlationId?: string; /** Evaluation start, epoch ms. Required (see {@link ScannerRunRecord.startedAt}) so a `Duration` is always derivable. */ readonly startedAt: number; /** Evaluation end, epoch ms. */ readonly endedAt: number; /** * Pass-level decision (the single `ActionDecision` the engine/LLM produces for this pass). * Absent when the pass was filtered before a decision was produced. */ readonly decision?: SignalDecision; /** One entry per signal in the evaluated batch; outcomes are per-signal. */ readonly evaluations: readonly SignalEvaluation[]; /** * The pass-level risk-gate snapshot, one entry per configured gate. Empty when the strategy has * no guard rails; absent when the pass carried no snapshot at all (a close pass, or an open pass * that returned before the snapshot resolved). */ readonly riskGates?: readonly RiskGateSnapshot[]; } export type PositionLifecycleEvent = "opened" | "closed" | "flipped" | "increased" | "decreased" | "open_order_submitted"; /** * A position lifecycle domain event observed on the bus. These are first-class events — they * fire both as the outcome of acting on a signal *and* when the position-tracker scanner * detects on-chain changes opened manually or by other tools — so they are recorded * independently of {@link SignalEvaluatedRecord}. * * Variant-specific fields are optional; `event` discriminates which apply. */ export interface PositionLifecycleRecord { readonly type: "position_lifecycle"; readonly event: PositionLifecycleEvent; readonly occurredAt: number; /** Cross-flow position.id (`__positionId` on the bus payload). See {@link SignalOutcome.positionId}. */ readonly positionId?: string; readonly asset: string; /** Empty string / undefined for the main book. */ readonly dex?: string; readonly direction?: TradeDirection | null; readonly entryPrice?: number; readonly size?: number; readonly leverage?: number; readonly leverageType?: string; readonly margin?: number; readonly unrealizedPnl?: number; readonly roe?: number; /** `null` when the venue reports no liquidation price. */ readonly liquidationPrice?: number | null; readonly reason?: string; readonly closeReason?: string; readonly closedPrice?: number; readonly closedSize?: number; /** Maker vs taker on the close (`null` when unfilled). */ readonly executionAsMaker?: boolean | null; readonly previousEntryPrice?: number; readonly previousSize?: number; readonly sizeDelta?: number; readonly newAsset?: string; readonly orderId?: string; readonly orderType?: string; readonly marginAmount?: number; /** Order-fill success flag from `CreatePositionOrderResult.filled` (boolean, not a quantity). */ readonly filled?: boolean | null; readonly reasoning?: string; /** * Externally-detected vs runtime-opened discriminator (`"runtime_opened"` | * `"externally_detected"`), from the PositionIdRegistry's intent-vs-fresh mint observation. * Populated on `opened` only (the registry surfaces it solely on confirm-open); absent on every * other lifecycle event. */ readonly reconciliationSource?: string; /** Id of the tick that observed this change (`__tickId` on the bus payload); absent when it carried none. */ readonly tickId?: string; /** Id of the intake acceptance behind that tick (`__correlationId`); absent for an internally produced one. */ readonly correlationId?: string; } export type DslTransitionKind = "created" | "phase_changed" | "tier_advanced" | "sl_updated" | "closed" | "close_pending" | "settings_updated" | "deleted"; /** A tier in the exit ladder, as carried on `dsl.created`. */ export interface DslTierSummary { readonly triggerPct: number; readonly lockHwPct: number; } /** * A DSL exit-engine transition. One record per transition; `transition` discriminates which of the * optional fields are populated. Periodic `dsl.heartbeat` is intentionally not modelled — it is a * notification cadence, not a state transition. */ export interface DslTransitionRecord { readonly type: "dsl_transition"; readonly transition: DslTransitionKind; readonly occurredAt: number; /** Cross-flow position.id (`__positionId` on the bus payload). See {@link SignalOutcome.positionId}. */ readonly positionId?: string; readonly asset: string; readonly dex?: string; /** Lifecycle event that produced the DSL row (`position_opened`|`..._increased`|`flipped`). */ readonly triggerReason?: string; readonly preset?: string; /** Exit ladder the position was created with (`dsl.created`). */ readonly tiers?: readonly DslTierSummary[]; readonly direction?: TradeDirection; readonly entryPrice?: number; readonly leverage?: number; readonly margin?: number; readonly size?: number; readonly floorPrice?: number; readonly newFloorPrice?: number; readonly phase?: 1 | 2; /** * Current tier index. Normalizes the three bus spellings into one field — `tierIndex` * (`dsl.phase_changed`), `tier` (`dsl.tier_advanced`), `currentTierIndex` (`dsl.closed`) — so * downstream never has to know which event produced the record. `null` when no tier is active. */ readonly tierIndex?: number | null; readonly lockHwPct?: number; readonly triggerPct?: number; readonly lockedProfitPct?: number; readonly newSLPrice?: number; readonly slOrderId?: number; readonly reason?: string; readonly closeReason?: string; /** Position open time, epoch ms (on `closed`). */ readonly createdAt?: number; readonly currentROE?: number; readonly highWaterRoe?: number; /** High-water *price* the trailing floor is relative to (`DslState.highWaterPrice`). */ readonly highWaterPrice?: number; readonly peakROE?: number; readonly lastPrice?: number; readonly attempt?: number; readonly updated?: readonly string[]; /** * Senpi/MCP-side position id (`DslState.backendDslPositionId`). Captured opportunistically for * exchange-side correlation — but **DSL-only**: the runtime must not treat this as a position * identity, since DSL need not be active for a position to exist. `null` when not yet assigned. */ readonly backendDslPositionId?: string | null; /** Id of the tick that emitted this transition (`__tickId` on the bus payload); absent when it carried none. */ readonly tickId?: string; /** * Who acted (`__dslSource` on the bus payload): `runtime` for this runtime's own tick, `backend` * for a relayed backend transition. Absent when the payload carried none — never defaulted. */ readonly source?: string; /** The backend's own stamp of when it acted (`__backendEventAt`); only on a backend-sourced transition. */ readonly backendEventAt?: string; } /** * Which work unit of a monitor tick a {@link DslTickRecord} carries: the tick itself, one * position's processing window, or one venue call (close attempt / SL update) within it. */ export type DslTickUnit = "tick" | "position" | "close_attempt" | "sl_update"; /** * One work unit of the DSL monitor tick — the runtime's third work loop alongside the scanner run * and the signal-evaluation pass. Unlike {@link DslTransitionRecord} (the point-in-time * state-machine record, which stays a log), these are *measured durations*: the tick batches price * fetches and processes every active position, and each position's processing may submit a close * or sync the exchange stop-loss — the venue-latency story. * * One record per bus event, no pairing: the shared `tickId` (emitted as the `tick.id` attribute on * every span and, scoped by the emitter's bootId, used as the deterministic trace-id seed) threads * the units into one trace, and `positionKey` seeds the per-position span id so close/SL units * nest under their position. * `unit` discriminates which optional fields apply. `position.id` joins the family to the * position's lifecycle and `dsl_transition` logs across flows. */ export interface DslTickRecord { readonly type: "dsl_tick"; readonly unit: DslTickUnit; /** The monitor's per-tick id — the deterministic TraceId seed (scoped by bootId) and the `tick.id` join attribute. */ readonly tickId: string; /** Work-unit start, epoch ms — observed at the emit site, never fabricated. */ readonly startedAt: number; /** Work-unit end, epoch ms (clamped so `endedAt >= startedAt`). */ readonly endedAt: number; readonly strategyCount?: number; readonly activePositionCount?: number; readonly positionsProcessed?: number; /** Batched price-fetch window within the tick (both or neither — a half window is fabricated extent). */ readonly pricesStartedAt?: number; readonly pricesEndedAt?: number; /** The DSL state key — the deterministic span-id seed component (`bootId:tickId:positionKey`). */ readonly positionKey?: string; /** Cross-flow position.id (`DslState.positionId`, falling back to `correlationId` for legacy state). */ readonly positionId?: string; readonly asset?: string; readonly dex?: string; readonly outcome?: "ok" | "error"; readonly error?: string; readonly closeReason?: string; /** Consecutive close-attempt number (1-based), from `consecutiveCloseFailures`. */ readonly attempt?: number; readonly newSLPrice?: number; /** Synced exchange SL order id (success only). */ readonly slOrderId?: number; /** Why the sync ran (e.g. `slOrderMissing,tierChanged`), as the monitor computes it. */ readonly syncReason?: string; } /** * A bucket-2 error that does not belong to a specific domain record (e.g. the bus's * `signal_processing_error`). Errors that *do* have a home — scanner failures, action * failures, DSL close failures — are carried on {@link ScannerRunRecord}, {@link SignalOutcome} * and {@link DslTransitionRecord} respectively. */ export interface RuntimeErrorRecord { readonly type: "runtime_error"; readonly occurredAt: number; /** Where the error originated, e.g. `"signal_processing"`. */ readonly phase: string; readonly message: string; /** Error class name (`Error.name`, e.g. `"ZodError"`) — the natural error-grouping key. */ readonly errorName?: string; readonly asset?: string; readonly scannerId?: string; /** Telemetry-owned per-scan-run id when the error belongs to a scan family (e.g. signal processing). */ readonly runId?: string; readonly stack?: string; } export type RuntimeLifecyclePhase = "started" | "stopped"; /** * Runtime session boundary. Marks process start/stop and carries session shape; `strategyAddress` * is omitted because it lives on the identity envelope. */ export interface RuntimeLifecycleRecord { readonly type: "runtime_lifecycle"; readonly phase: RuntimeLifecyclePhase; readonly occurredAt: number; readonly runtimeVersion?: string; readonly hasDsl?: boolean; readonly scannerCount?: number; readonly actionCount?: number; /** * The **unresolved** runtime config at boot (raw YAML/config, `${VAR}` placeholders intact) — the * retrospection snapshot of what this session ran. Deliberately NOT the resolved config: resolving * `${VAR}` inlines secrets (e.g. `SENPI_API_KEY`), which `env-resolve.ts` forbids exporting. Keeping * it unresolved needs no redaction and is the stabler artifact — it is also the input `configHash` * is computed over, so blob and hash agree. See {@link IdentityEnvelope.configHash}. */ readonly config?: Record; readonly uptimeMs?: number; } export type RuntimeStatePhase = "paused" | "resumed"; /** * A runtime trading-state transition — the runtime pausing or resuming its *own* trading on a * cap/halt. Sibling to {@link RuntimeLifecycleRecord} on a different axis: lifecycle records the * process (started/stopped); this records whether trading is active or self-paused. * * `paused` corresponds to the bus's `on_strategy_self_paused` / `on_daily_limit_hit` / * `on_drawdown_cap_hit`, which fire on a `running → paused` edge carrying the breached `metrics`. * `resumed` is the symmetric `paused → running` re-arm; modelling it here lets the emit be added * later without changing this type. `strategyAddress` is omitted — it lives on the identity envelope. * * This is the "why did the runtime stop trading" signal — for observability and for an agent * reasoning about self-imposed halts (raise a cap, wait for daily reset, investigate drawdown). */ export interface RuntimeStateRecord { readonly type: "runtime_state"; readonly phase: RuntimeStatePhase; readonly occurredAt: number; /** Stable gate identifier (e.g. the cap/halt rule id). */ readonly gateId: string; readonly gateName?: string; /** Which hook produced the pause (`self_paused`|`daily_limit`|`drawdown_cap`). */ readonly source?: string; /** Human-readable explanation with live numbers (the gate's `reason`). */ readonly reason?: string; /** Breached-gate metrics bag (`pnlDelta`, `dailyLossLimitUsd`, `drawdownFromPeakPct`, …). */ readonly metrics?: Record | null; } /** * Coarse action intent of a {@link DecisionMadeRecord} — the `open`/`close`/`none` axis the * read-side groups decisions by, derived from the decision's action type. */ export type DecisionActionIntent = "open" | "close" | "none"; /** * The judgment over one evaluation pass, as a discrete log event (`decision.made`). Derived from * the same action-result payload that builds {@link SignalEvaluatedRecord}. The trace view keeps the * decision on the `llm.decision` span; this is its logs-only twin so * a logs-only consumer (the self-learning agent) can read "what the strategy decided and why" * without walking spans. * * `reasoning` is the Body (uncapped free-text). Market-context fields (`fundingRegime`/ * `marketRegime`/`markPrice`) are reserved keys: they are NOT on the action-result * payload today, so they are left undefined (absent = NULL). */ export interface DecisionMadeRecord { readonly type: "decision_made"; readonly occurredAt: number; readonly mode?: DecisionMode; readonly byLlm: boolean; readonly model?: string; readonly tokensUsed?: number | null; readonly confidence?: number; readonly confidenceThreshold?: number; readonly durationMs?: number; readonly actionIntent: DecisionActionIntent; readonly fundingRegime?: string; readonly marketRegime?: string; readonly markPrice?: number; /** Full LLM reasoning prose — the Body content (uncapped, routed through Body redaction). */ readonly reasoning?: string; } /** Disposition of one considered signal: did it become an order, get rejected, get blocked, or error. */ export type SignalOutcomeDisposition = "accepted" | "rejected" | "blocked" | "error"; /** * One signal's disposition, as a discrete log event (`signal.outcome`). One per evaluation in the * pass, derived from the same action-result payload that builds {@link SignalEvaluatedRecord}. * The four-value `result` collapses the granular per-signal * {@link SignalOutcomeResult}; the open `reasonCode` is kept verbatim for the fine-grained reason. * * `rawSignal` is the variable-shaped scanner evidence. The mapper reads its `meta` bag off it and * emits that verbatim as one JSON string attribute — unredacted, and never lifted into the Body. The * scalars below are the queryable attribute surface. */ export interface SignalOutcomeRecord { readonly type: "signal_outcome"; readonly occurredAt: number; readonly scannerId?: string; /** * The pass's {@link SignalEvaluatedRecord.actionType} — which action produced this outcome. An * open that placed an order and a close that placed one share the `accepted` disposition, so this * is the only field on the record that separates them. */ readonly actionType?: string; readonly signalType?: string; readonly asset: string; readonly direction?: TradeDirection | null; readonly score?: number; /** Signal time, epoch ms. */ readonly signalTs?: number; readonly factors?: Record; readonly result: SignalOutcomeDisposition; /** The granular per-signal reason (open string, e.g. `risk_gate_COOLDOWN`, `below_confidence`). */ readonly reasonCode?: string; readonly positionId?: string; readonly marginAmount?: number; readonly leverage?: number; readonly notionalValue?: number; readonly size?: number; /** Which precedence rule sized the margin (the {@link MarginSource} breadcrumb). */ readonly marginSource?: string; /** Applied percent of withdrawable — present only for the percent-based margin sources. */ readonly marginPct?: number; readonly orderId?: string; readonly errorName?: string; /** The raw scanner signal payload; the mapper reads its `meta` bag off here to emit verbatim. */ readonly rawSignal?: Record; /** * The pass-level risk-gate evaluations, copied onto every outcome in the pass — they are what the * gates said when this pass ran, not a per-signal re-check. Serialized by the mapper. */ readonly riskGates?: readonly RiskGateSnapshot[]; } /** * One DSL exit attempt as a discrete log event (`dsl.close_attempt`) — exit attempts / venue * failures / retries, span-only today. Derived from the DSL close-attempt payload * (already consumed to build the {@link DslTickRecord} trace) — a second consumer for the log. * * No `errorName`: only `String(e)` is available at the producer emit site, so the error class name * is omitted. */ export interface DslCloseAttemptRecord { readonly type: "dsl_close_attempt"; readonly occurredAt: number; readonly positionId?: string; readonly asset?: string; readonly dex?: string; /** The close reason (`closeReason` on the payload). */ readonly reason?: string; readonly attempt?: number; readonly result: "ok" | "error"; readonly error?: string; } /** * One scan's funnel summary as a discrete log event (`scanner.summary`) — the anti-spam roll-up of * what a scan considered and dropped. Derived from the SAME scanner-run payload that * builds {@link ScannerRunRecord} (its `summary.funnel`). One row per scan: the * dropped candidates are ONE Body array, never one row per drop. * * `passedAssets` is a small linkage list (the survivors, each detailed by its own `signal.outcome`); * `droppedCandidates` is the variable-shaped Body array — never a filter target, reachable only after * the row is selected. The count attributes (`scannedCount`/`passedCount`/`droppedCount`/ * `droppedByReason`) carry the aggregate query surface. */ export interface ScannerSummaryRecord { readonly type: "scanner_summary"; readonly occurredAt: number; readonly scannerId: string; readonly scannedCount?: number; readonly passedCount?: number; readonly droppedCount?: number; /** Reason → count of candidates dropped for it (bounded reason vocabulary → queryable attrs). */ readonly droppedByReason?: Record; /** The survivors' assets — a small linkage list to the per-signal `signal.outcome` events. */ readonly passedAssets?: readonly string[]; /** The variable Body array: `[{asset, score?, reason, factors?}, …]` — never a filter target. */ readonly droppedCandidates?: readonly Record[]; } /** Which execution fact an {@link OrderEventRecord} carries. */ export type OrderEventKind = "placed" | "filled" | "failed"; /** * One order-execution event as a discrete log event (`order.{placed,filled,failed}`) — the venue * execution granularity (placement vs fill, maker/taker, venue rejects) that the position-level * `position_lifecycle` event can't carry. Emitted additively by the open/close actions at the * submit/success/failure points. `event` discriminates which * optional fields apply. * * Overlap with `position_lifecycle.opened`/`.closed` is intentional: the lifecycle event is the * settled position-level fact; `order.*` is the execution-level fact. Both are emitted; no dedupe. * * `limitPrice`/`fees`/`partial` are reserved-but-unpopulated — they are not in scope at the action's * submission point. `error` on `failed` is the optional venue raw response → the Body. */ export interface OrderEventRecord { readonly type: "order_event"; readonly event: OrderEventKind; readonly occurredAt: number; /** Cross-flow position.id (`__positionId` on the bus payload). See {@link SignalOutcome.positionId}. */ readonly positionId?: string; readonly orderId?: string; readonly asset: string; readonly direction?: TradeDirection | null; readonly orderType?: string; readonly size?: number; /** Reserved — unpopulated (not in scope at the submission point). */ readonly limitPrice?: number; readonly reduceOnly?: boolean; readonly fillPrice?: number; readonly fillSize?: number; readonly executionAsMaker?: boolean | null; /** Reserved — unpopulated (not in scope). */ readonly fees?: number; /** Reserved — unpopulated (not in scope). */ readonly partial?: boolean; readonly reason?: string; readonly errorName?: string; /** The optional venue raw response on `failed` → the Body. */ readonly error?: string; } /** From a scanner-run payload (complete / skipped / error): complete runs carry the whole * `ScanResult`; skip/error carry status + reason. */ export declare function buildScannerRun(payload: unknown, nowMs: number): ScannerRunRecord | null; /** * The `scanner.summary` anti-spam log record, derived from the SAME scanner-run payload * that {@link buildScannerRun} reads (its `data.summary.funnel.dropped`). * * Returns `null` when there is no `data.summary.funnel`: skip/error runs and scanners without a * funnel produce no summary event (the survivors are detailed by their own `signal.outcome` events, * and "didn't run at all" is `scanner_run.skipped`). One record per scan; the dropped candidates are * one Body array, never one row per drop. */ export declare function buildScannerSummary(payload: unknown, nowMs: number): ScannerSummaryRecord | null; /** * Read-only position.id lookup, (address, dex, asset) → the live id or undefined. Injected by * the runtime as a narrow accessor over the PositionIdRegistry — the registry is the single * source of truth; builders never receive the registry (or RuntimeContext) itself. */ export type PositionIdResolver = (address: string, dex: string, asset: string) => string | undefined; /** * Build the whole pass record from an action-result payload alone — the producers echo the scan * provenance onto the payload (`scannerId`, `scannerSignals`, `minConfidence`, `runId`), and the * decision itself rides `result.data._decisionMeta`, so no pairing is needed. * * `evaluations` is one entry per per-signal result, each enriched with the matching input signal * (or a summary synthesized from the result entry when none matches — e.g. close-position's * static-params path, which carries no `scannerSignals`). A pass without `_decisionMeta` yields no * `decision` (the contract allows a decision-less pass). * * The pass window is a point anchored at the action-result timestamp (`startedAt === endedAt`); the * decision's own `durationMs` (from `_decisionMeta`) carries the timing. */ export declare function buildSignalEvaluated(actionResultPayload: unknown, nowMs: number, resolvePositionId?: PositionIdResolver): SignalEvaluatedRecord | null; /** * The pass's `decision.made` log record, derived from the built pass record — `null` when the pass * carried no decision (a decision-less pass yields no judgment event). One per pass. */ export declare function buildDecisionMade(record: SignalEvaluatedRecord): DecisionMadeRecord | null; /** * One `signal.outcome` log record per evaluation in the pass, derived from the built pass record. * `rawSignal` is the evaluation's {@link SignalSummary} (incl. its `meta`/`factors`) — the variable * scanner evidence. */ export declare function buildSignalOutcomes(record: SignalEvaluatedRecord): SignalOutcomeRecord[]; /** * From the DSL close-attempt monitor payload — the second consumer (alongside the * tick trace). Fields at the payload root: `positionId`, `asset`, `dex`, `closeReason`, `attempt`, * `outcome` ("ok"|"error"), `error`, ISO `timestamp`. `null` when the payload is not an object. */ export declare function buildDslCloseAttempt(payload: unknown, nowMs: number): DslCloseAttemptRecord | null; /** * From the additive order events the open/close actions fire (the HookEvent shape: * fields under a `data` envelope). One record per event; `eventKind` discriminates which fields the * payload carries (placed: order/size; filled: fill; failed: reason/error). `positionId` maps from * `__positionId`. `null` when no `asset` is resolvable — the asset is the one always-present scalar. * * Faithful population only (no redaction here — the mapper is the single redaction seam). */ export declare function buildOrderEvent(eventKind: OrderEventKind, payload: unknown, nowMs: number): OrderEventRecord | null; /** * From an `on_position_*` event. Action-driven events arrive as HookEvents (fields under a `data` * envelope); the DSL monitor / reconcile paths emit `on_position_closed` flat (fields at the root, * always with an `asset`), so a flat payload carrying an `asset` is read as-is. The DSL archive * handler's re-broadcast of a close is marked `source: "dsl_archive"` and skipped — the close it * mirrors was already recorded from the original emit. (`on_open_order_submitted` is deliberately * not recorded — its submit fact is already on the signal-evaluation view, avoiding a double count.) */ export declare function buildPositionLifecycle(event: PositionLifecycleEvent, payload: unknown, nowMs: number): PositionLifecycleRecord | null; /** From a `dsl.*` event (fields live at the payload root, with an ISO `timestamp`). */ export declare function buildDslTransition(transition: DslTransitionKind, payload: unknown, nowMs: number): DslTransitionRecord | null; /** * From the monitor's DSL tick / position / close-attempt / sl-update payloads (fields at the * payload root). Each payload yields one record — * the deterministic ids derived from `tickId` (and `tickId:positionKey`, both bootId-scoped by * the mapper) thread them into one trace, so no pairing is needed here. Essentials are `tickId` * plus a numeric work window; on clock skew (`endedAt < startedAt`) the start is pulled up to the * end, matching the pass-window convention. The price-fetch window is kept only when both stamps * are present (a half window is fabricated extent). No `nowMs` fallback: work windows are * producer-stamped; nothing here is fabricated. */ export declare function buildDslTick(unit: DslTickUnit, payload: unknown): DslTickRecord | null; /** From `signal_processing_error` — a bucket-2 error with no specific domain record home. */ export declare function buildRuntimeError(payload: unknown, nowMs: number): RuntimeErrorRecord | null; /** * From the runtime-started payload. The `config` blob is the injected **unresolved** config (not the * resolved `data.config`): resolving inlines secrets, and the unresolved form is what the * `configHash` is computed over, so blob and hash agree. */ export declare function buildRuntimeStarted(payload: unknown, unresolvedConfig: Record | undefined, nowMs: number): RuntimeLifecycleRecord | null; /** From the runtime-stopped payload. */ export declare function buildRuntimeStopped(payload: unknown, nowMs: number): RuntimeLifecycleRecord | null; /** From the self-pause HookEvents (`on_strategy_self_paused`/`on_daily_limit_hit`/`on_drawdown_cap_hit`). */ export declare function buildRuntimePaused(payload: unknown, fallbackGateId: string, nowMs: number): RuntimeStateRecord | null; //# sourceMappingURL=event-builders.d.ts.map