@senpi/runtime 1.1.0-dev.runtime-phase-2.20260612060625 → 1.1.0-dev.runtime-phase-2.20260617180011

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  2. package/dist/actions/close-position/close-position.action.js +55 -13
  3. package/dist/actions/close-position/close-position.action.js.map +1 -1
  4. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  5. package/dist/actions/open-position/open-position.action.js +66 -15
  6. package/dist/actions/open-position/open-position.action.js.map +1 -1
  7. package/dist/actions/position-tracker/position-tracker.action.d.ts.map +1 -1
  8. package/dist/actions/position-tracker/position-tracker.action.js +10 -1
  9. package/dist/actions/position-tracker/position-tracker.action.js.map +1 -1
  10. package/dist/actions/tracking-helpers.d.ts +16 -1
  11. package/dist/actions/tracking-helpers.d.ts.map +1 -1
  12. package/dist/actions/tracking-helpers.js +55 -9
  13. package/dist/actions/tracking-helpers.js.map +1 -1
  14. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  15. package/dist/dsl/monitor/create-monitor.js +30 -0
  16. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  17. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  18. package/dist/dsl/monitor/process-one-position.js +114 -0
  19. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  20. package/dist/recording/emitter.d.ts +7 -1
  21. package/dist/recording/emitter.d.ts.map +1 -1
  22. package/dist/recording/index.d.ts +1 -1
  23. package/dist/recording/index.d.ts.map +1 -1
  24. package/dist/recording/index.js.map +1 -1
  25. package/dist/recording/noop-emitter.d.ts +6 -0
  26. package/dist/recording/noop-emitter.d.ts.map +1 -1
  27. package/dist/recording/noop-emitter.js +6 -0
  28. package/dist/recording/noop-emitter.js.map +1 -1
  29. package/dist/recording/otel/constants.d.ts +45 -3
  30. package/dist/recording/otel/constants.d.ts.map +1 -1
  31. package/dist/recording/otel/constants.js +79 -3
  32. package/dist/recording/otel/constants.js.map +1 -1
  33. package/dist/recording/otel/ids.d.ts +11 -0
  34. package/dist/recording/otel/ids.d.ts.map +1 -1
  35. package/dist/recording/otel/ids.js +26 -1
  36. package/dist/recording/otel/ids.js.map +1 -1
  37. package/dist/recording/otel/index.d.ts +1 -1
  38. package/dist/recording/otel/index.d.ts.map +1 -1
  39. package/dist/recording/otel/index.js +1 -1
  40. package/dist/recording/otel/index.js.map +1 -1
  41. package/dist/recording/otel/mappers.d.ts +60 -2
  42. package/dist/recording/otel/mappers.d.ts.map +1 -1
  43. package/dist/recording/otel/mappers.js +384 -43
  44. package/dist/recording/otel/mappers.js.map +1 -1
  45. package/dist/recording/otel/otel-emitter.d.ts +7 -1
  46. package/dist/recording/otel/otel-emitter.d.ts.map +1 -1
  47. package/dist/recording/otel/otel-emitter.js +26 -2
  48. package/dist/recording/otel/otel-emitter.js.map +1 -1
  49. package/dist/recording/otel/redaction.d.ts +22 -6
  50. package/dist/recording/otel/redaction.d.ts.map +1 -1
  51. package/dist/recording/otel/redaction.js +54 -6
  52. package/dist/recording/otel/redaction.js.map +1 -1
  53. package/dist/recording/records.d.ts +227 -3
  54. package/dist/recording/records.d.ts.map +1 -1
  55. package/dist/recording/wiring/record-builders.d.ts +59 -26
  56. package/dist/recording/wiring/record-builders.d.ts.map +1 -1
  57. package/dist/recording/wiring/record-builders.js +309 -61
  58. package/dist/recording/wiring/record-builders.js.map +1 -1
  59. package/dist/recording/wiring/subscriber.d.ts.map +1 -1
  60. package/dist/recording/wiring/subscriber.js +59 -57
  61. package/dist/recording/wiring/subscriber.js.map +1 -1
  62. package/dist/runtime/load-runtime-config.d.ts.map +1 -1
  63. package/dist/runtime/load-runtime-config.js +2 -3
  64. package/dist/runtime/load-runtime-config.js.map +1 -1
  65. package/dist/runtime/runtime.d.ts.map +1 -1
  66. package/dist/runtime/runtime.js +7 -1
  67. package/dist/runtime/runtime.js.map +1 -1
  68. package/dist/scanners/implementations/momentum.d.ts.map +1 -1
  69. package/dist/scanners/implementations/momentum.js +16 -0
  70. package/dist/scanners/implementations/momentum.js.map +1 -1
  71. package/dist/tracking/position-id-registry.d.ts +9 -0
  72. package/dist/tracking/position-id-registry.d.ts.map +1 -1
  73. package/dist/tracking/position-id-registry.js +10 -4
  74. package/dist/tracking/position-id-registry.js.map +1 -1
  75. package/package.json +1 -1
@@ -7,19 +7,20 @@
7
7
  * Timestamps: the bus carries ISO strings (and, for a scan result, an epoch-ms number); every record
8
8
  * stores epoch ms, converted once here.
9
9
  */
10
- import type { DslTransitionKind, DslTransitionRecord, PositionLifecycleEvent, PositionLifecycleRecord, RuntimeErrorRecord, RuntimeLifecycleRecord, RuntimeStateRecord, ScannerRunRecord, SignalDecision, SignalEvaluatedRecord, SignalSummary } from "../records.js";
10
+ import type { DecisionMadeRecord, DslCloseAttemptRecord, DslTickRecord, DslTickUnit, DslTransitionKind, DslTransitionRecord, OrderEventKind, OrderEventRecord, PositionLifecycleEvent, PositionLifecycleRecord, RuntimeErrorRecord, RuntimeLifecycleRecord, RuntimeStateRecord, ScannerRunRecord, ScannerSummaryRecord, SignalEvaluatedRecord, SignalOutcomeRecord } from "../records.js";
11
11
  /** From a `tracking.scanner_run` / `tracking.scanner_run_skipped` / `tracking.scanner_run_error`
12
12
  * event (complete runs carry the whole `ScanResult`; skip/error carry status + reason). */
13
13
  export declare function buildScannerRun(payload: unknown, nowMs: number): ScannerRunRecord | null;
14
- /** Pass-level decision state held between `tracking.decision` and its `tracking.action_result`. */
15
- export interface PendingDecision {
16
- readonly scannerId?: string;
17
- readonly startedAtMs: number;
18
- readonly decision: SignalDecision;
19
- readonly signals: readonly SignalSummary[];
20
- }
21
- /** Build the pending decision from a `tracking.decision` payload. */
22
- export declare function buildPendingDecision(payload: unknown, nowMs: number): PendingDecision | null;
14
+ /**
15
+ * The `scanner.summary` anti-spam log record, derived from the SAME `tracking.scanner_run` payload
16
+ * that {@link buildScannerRun} reads (its `data.summary.funnel.dropped`) — no new bus event.
17
+ *
18
+ * Returns `null` when there is no `data.summary.funnel`: skip/error runs and scanners without a
19
+ * funnel produce no summary event (the survivors are detailed by their own `signal.outcome` events,
20
+ * and "didn't run at all" is `scanner_run.skipped`). One record per scan; the dropped candidates are
21
+ * one Body array, never one row per drop.
22
+ */
23
+ export declare function buildScannerSummary(payload: unknown, nowMs: number): ScannerSummaryRecord | null;
23
24
  /**
24
25
  * Read-only position.id lookup, (address, dex, asset) → the live id or undefined. Injected by
25
26
  * the runtime as a narrow accessor over the PositionIdRegistry — the registry is the single
@@ -27,27 +28,48 @@ export declare function buildPendingDecision(payload: unknown, nowMs: number): P
27
28
  */
28
29
  export type PositionIdResolver = (address: string, dex: string, asset: string) => string | undefined;
29
30
  /**
30
- * Pair a held {@link PendingDecision} with its `tracking.action_result` payload into one record.
31
- * `evaluations` is one entry per per-signal result, each enriched with the matching input signal.
32
- * The pass span runs decision-ts → action_result-ts; on clock skew (`endedAt < startedAt`) the start
33
- * is pulled up to `endedAt`, yielding a zero (never negative) duration.
31
+ * Build the whole pass record from a `tracking.action_result` payload alone — the producers echo
32
+ * the decision window and scan provenance onto the payload (`scannerId`, `scannerSignals`,
33
+ * `minConfidence`, `decisionStartedAt`, `decisionEndedAt`, `runId`), and the decision itself rides
34
+ * `result.data._decisionMeta`, so no pairing with `tracking.decision` is needed.
35
+ *
36
+ * `evaluations` is one entry per per-signal result, each enriched with the matching input signal
37
+ * (or a summary synthesized from the result entry when none matches — e.g. close-position's
38
+ * static-params path, which carries no `scannerSignals`). A pass without `_decisionMeta` yields no
39
+ * `decision` (the contract allows a decision-less pass).
40
+ *
41
+ * The pass span runs `decisionStartedAt` → action_result-ts; when the start stamp is absent
42
+ * (defensive only — the bus is in-process) `startedAt = endedAt`. On clock skew
43
+ * (`endedAt < startedAt`) the start is pulled up to `endedAt`, yielding a zero (never negative)
44
+ * duration.
45
+ */
46
+ export declare function buildSignalEvaluated(actionResultPayload: unknown, nowMs: number, resolvePositionId?: PositionIdResolver): SignalEvaluatedRecord | null;
47
+ /**
48
+ * The pass's `decision.made` log record, derived from the built pass record — `null` when the pass
49
+ * carried no decision (a decision-less pass yields no judgment event). One per pass.
50
+ */
51
+ export declare function buildDecisionMade(record: SignalEvaluatedRecord): DecisionMadeRecord | null;
52
+ /**
53
+ * One `signal.outcome` log record per evaluation in the pass, derived from the built pass record.
54
+ * `rawSignal` is the evaluation's {@link SignalSummary} (incl. its `meta`/`factors`) — the variable
55
+ * scanner evidence; redaction over it happens in the mapper, not here.
34
56
  */
35
- export declare function buildSignalEvaluated(pending: PendingDecision, actionResultPayload: unknown, nowMs: number, resolvePositionId?: PositionIdResolver): SignalEvaluatedRecord;
57
+ export declare function buildSignalOutcomes(record: SignalEvaluatedRecord): SignalOutcomeRecord[];
36
58
  /**
37
- * A `tracking.action_result` with no held decision — a producer that never emits `tracking.decision`
38
- * for the pass (e.g. close-position's static-params path, `decision_mode: 'none'` + `params.asset`).
39
- * The record carries the per-signal outcomes with signals synthesized from the result entries and no
40
- * `decision` (the contract allows a decision-less pass); zero duration (`startedAt = endedAt` = the
41
- * action_result timestamp).
59
+ * From the existing `tracking.dsl_close_attempt` monitor event — the second consumer (alongside the
60
+ * tick trace). Fields at the payload root: `positionId`, `asset`, `dex`, `closeReason`, `attempt`,
61
+ * `outcome` ("ok"|"error"), `error`, ISO `timestamp`. `null` when the payload is not an object.
42
62
  */
43
- export declare function buildResultOnlyEvaluated(actionResultPayload: unknown, nowMs: number, resolvePositionId?: PositionIdResolver): SignalEvaluatedRecord | null;
63
+ export declare function buildDslCloseAttempt(payload: unknown, nowMs: number): DslCloseAttemptRecord | null;
44
64
  /**
45
- * A decision that never received its action_result (the action threw or returned before emitting
46
- * one). Flushed when the same action's next decision arrives, so a decided-but-unresulted pass is
47
- * visible on the signal axis rather than silently dropped: no evaluations, zero duration
48
- * (`startedAt = endedAt =` the decision timestamp).
65
+ * From the additive `tracking.order_*` events the open/close actions fire (the HookEvent shape:
66
+ * fields under a `data` envelope). One record per event; `eventKind` discriminates which fields the
67
+ * payload carries (placed: order/size; filled: fill; failed: reason/error). `positionId` maps from
68
+ * `__positionId`. `null` when no `asset` is resolvable — the asset is the one always-present scalar.
69
+ *
70
+ * Faithful population only (no redaction here — the mapper is the single redaction seam).
49
71
  */
50
- export declare function buildDecisionOnlyEvaluated(pending: PendingDecision): SignalEvaluatedRecord;
72
+ export declare function buildOrderEvent(eventKind: OrderEventKind, payload: unknown, nowMs: number): OrderEventRecord | null;
51
73
  /**
52
74
  * From an `on_position_*` event. Action-driven events arrive as HookEvents (fields under a `data`
53
75
  * envelope); the DSL monitor / reconcile paths emit `on_position_closed` flat (fields at the root,
@@ -59,6 +81,17 @@ export declare function buildDecisionOnlyEvaluated(pending: PendingDecision): Si
59
81
  export declare function buildPositionLifecycle(event: PositionLifecycleEvent, payload: unknown, nowMs: number): PositionLifecycleRecord | null;
60
82
  /** From a `dsl.*` event (fields live at the payload root, with an ISO `timestamp`). */
61
83
  export declare function buildDslTransition(transition: DslTransitionKind, payload: unknown, nowMs: number): DslTransitionRecord | null;
84
+ /**
85
+ * From the monitor's `tracking.dsl_tick` / `tracking.dsl_position` / `tracking.dsl_close_attempt`
86
+ * / `tracking.dsl_sl_update` events (fields at the payload root). Each event yields one record —
87
+ * the deterministic ids derived from `tickId` (and `tickId:positionKey`, both bootId-scoped by
88
+ * the mapper) thread them into one trace, so no pairing is needed here. Essentials are `tickId`
89
+ * plus a numeric work window; on clock skew (`endedAt < startedAt`) the start is pulled up to the
90
+ * end, matching the pass-window convention. The price-fetch window is kept only when both stamps
91
+ * are present (a half window is fabricated extent). No `nowMs` fallback: work windows are
92
+ * producer-stamped; nothing here is fabricated.
93
+ */
94
+ export declare function buildDslTick(unit: DslTickUnit, payload: unknown): DslTickRecord | null;
62
95
  /** From `signal_processing_error` — a bucket-2 error with no specific domain record home. */
63
96
  export declare function buildRuntimeError(payload: unknown, nowMs: number): RuntimeErrorRecord | null;
64
97
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"record-builders.d.ts","sourceRoot":"","sources":["../../../src/recording/wiring/record-builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAElB,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EAIrB,aAAa,EAEd,MAAM,eAAe,CAAC;AAoDvB;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA4BxF;AAID,mGAAmG;AACnG,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;CAC5C;AAED,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAwB5F;AA+DD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;AA8CrG;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,eAAe,EACxB,mBAAmB,EAAE,OAAO,EAC5B,KAAK,EAAE,MAAM,EACb,iBAAiB,CAAC,EAAE,kBAAkB,GACrC,qBAAqB,CAuBvB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,mBAAmB,EAAE,OAAO,EAC5B,KAAK,EAAE,MAAM,EACb,iBAAiB,CAAC,EAAE,kBAAkB,GACrC,qBAAqB,GAAG,IAAI,CAY9B;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,eAAe,GAAG,qBAAqB,CAS1F;AAUD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAqCrI;AAID,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAgD7H;AAID,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CA0B5F;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAczJ;AAED,uCAAuC;AACvC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAWlG;AAID,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAcrH"}
1
+ {"version":3,"file":"record-builders.d.ts","sourceRoot":"","sources":["../../../src/recording/wiring/record-builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAEV,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAElB,gBAAgB,EAChB,oBAAoB,EAEpB,qBAAqB,EAIrB,mBAAmB,EAIpB,MAAM,eAAe,CAAC;AAoDvB;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA6BxF;AAID;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CA6ChG;AAsED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;AA0DrG;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,mBAAmB,EAAE,OAAO,EAC5B,KAAK,EAAE,MAAM,EACb,iBAAiB,CAAC,EAAE,kBAAkB,GACrC,qBAAqB,GAAG,IAAI,CAkD9B;AA8BD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,qBAAqB,GAAG,kBAAkB,GAAG,IAAI,CAiB1F;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,mBAAmB,EAAE,CA0BxF;AASD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAclG;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA2BnH;AAUD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI,CAuCrI;AAID,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAgD7H;AASD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAkCtF;AAID,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CA2B5F;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAczJ;AAED,uCAAuC;AACvC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAWlG;AAID,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAcrH"}
@@ -71,6 +71,7 @@ export function buildScannerRun(payload, nowMs) {
71
71
  return {
72
72
  type: "scanner_run",
73
73
  scannerId: str(data.scannerId) ?? "",
74
+ runId: str(data.runId),
74
75
  outcome,
75
76
  statusDetail: status,
76
77
  startedAt,
@@ -84,33 +85,63 @@ export function buildScannerRun(payload, nowMs) {
84
85
  error: outcome === "error" ? str(data.reason) : undefined,
85
86
  };
86
87
  }
87
- /** Build the pending decision from a `tracking.decision` payload. */
88
- export function buildPendingDecision(payload, nowMs) {
88
+ // ─── Scanner funnel summary (Task 8a, derived from `tracking.scanner_run`) ────────
89
+ /**
90
+ * The `scanner.summary` anti-spam log record, derived from the SAME `tracking.scanner_run` payload
91
+ * that {@link buildScannerRun} reads (its `data.summary.funnel.dropped`) — no new bus event.
92
+ *
93
+ * Returns `null` when there is no `data.summary.funnel`: skip/error runs and scanners without a
94
+ * funnel produce no summary event (the survivors are detailed by their own `signal.outcome` events,
95
+ * and "didn't run at all" is `scanner_run.skipped`). One record per scan; the dropped candidates are
96
+ * one Body array, never one row per drop.
97
+ */
98
+ export function buildScannerSummary(payload, nowMs) {
89
99
  const p = asObject(payload);
90
100
  const data = asObject(p?.data);
91
101
  if (!data)
92
102
  return null;
93
- const startedAtMs = toEpochMs(p?.timestamp, nowMs);
94
- const d = asObject(data.decision) ?? {};
95
- const tel = asObject(d.decisionTelemetry);
96
- const decision = {
97
- execute: bool(d.execute) ?? false,
98
- actionType: str(data.actionType) ?? str(d.actionType) ?? "",
99
- actionName: str(data.actionName),
100
- mode: str(data.decisionMode),
101
- confidence: num(d.confidence) ?? 0,
102
- confidenceThreshold: num(data.minConfidence),
103
- reasoning: str(d.reasoning) ?? "",
104
- model: str(data.decisionModel) ?? str(tel?.model),
105
- tokensUsed: numOrNull(tel?.tokensUsed),
106
- durationMs: num(tel?.durationMs),
107
- payload: asObject(d.payload),
108
- };
109
- const signals = Array.isArray(data.scannerSignals)
110
- ? data.scannerSignals.map((s) => buildSignalSummary(s, startedAtMs))
103
+ const summary = asObject(data.summary);
104
+ const funnel = asObject(summary?.funnel);
105
+ if (!funnel)
106
+ return null;
107
+ const droppedCandidates = Array.isArray(funnel.dropped)
108
+ ? funnel.dropped
109
+ .map((d) => asObject(d))
110
+ .filter((d) => d !== undefined)
111
111
  : [];
112
- return { scannerId: str(data.scannerId), startedAtMs, decision, signals };
112
+ // Survivors: the emitted signals' assets — a small linkage list to the per-signal events.
113
+ const passedAssets = Array.isArray(data.signals)
114
+ ? data.signals
115
+ .map((s) => str(asObject(s)?.asset))
116
+ .filter((a) => a !== undefined)
117
+ : [];
118
+ const droppedByReason = {};
119
+ for (const d of droppedCandidates) {
120
+ const reason = str(d.reason);
121
+ if (reason === undefined)
122
+ continue;
123
+ droppedByReason[reason] = (droppedByReason[reason] ?? 0) + 1;
124
+ }
125
+ const droppedCount = droppedCandidates.length;
126
+ const passedCount = num(data.signalCount) ?? passedAssets.length;
127
+ // Grain is "one per scan that *considered* candidates": an idle scan (nothing passed, nothing
128
+ // dropped) carries an empty funnel and yields no summary — emitting an all-empty row every tick
129
+ // would defeat the anti-spam intent and risk evicting high-value rows on transport overflow.
130
+ if (droppedCount === 0 && passedCount === 0)
131
+ return null;
132
+ return {
133
+ type: "scanner_summary",
134
+ occurredAt: toEpochMs(data.timestamp, toEpochMs(p?.timestamp, nowMs)),
135
+ scannerId: str(data.scannerId) ?? "",
136
+ scannedCount: num(data.scannedCount) ?? passedCount + droppedCount,
137
+ passedCount,
138
+ droppedCount,
139
+ droppedByReason: Object.keys(droppedByReason).length > 0 ? droppedByReason : undefined,
140
+ passedAssets: passedAssets.length > 0 ? passedAssets : undefined,
141
+ droppedCandidates: droppedCount > 0 ? droppedCandidates : undefined,
142
+ };
113
143
  }
144
+ // ─── Signal evaluation (single-sourced from `tracking.action_result`) ────────────
114
145
  /** Reason codes where the system evaluated a trade and actively blocked it (see {@link classifyOutcome}). */
115
146
  const REJECTED_REASONS = new Set(["insufficient_margin", "invalid_direction", "no_margin_configured", "below_min_notional"]);
116
147
  /** Reason codes where no trade was applicable / attempted. */
@@ -164,13 +195,13 @@ function buildOutcome(raw, positionId) {
164
195
  availableMarginAmount: num(r.availableMarginAmount),
165
196
  };
166
197
  }
167
- /** The per-signal `results[]` entries from a `tracking.action_result` payload. */
198
+ /** The `data` envelope, embedded `result`, and per-signal `results[]` entries from a `tracking.action_result` payload. */
168
199
  function actionResults(actionResultPayload) {
169
200
  const p = asObject(actionResultPayload);
170
201
  const data = asObject(p?.data);
171
202
  const result = asObject(data?.result);
172
203
  const resultData = asObject(result?.data);
173
- return { data, results: Array.isArray(resultData?.results) ? resultData.results : [] };
204
+ return { data, result, resultData, results: Array.isArray(resultData?.results) ? resultData.results : [] };
174
205
  }
175
206
  /**
176
207
  * The V2 stamping rule: every per-signal outcome that pertains to a real position — open,
@@ -188,6 +219,11 @@ function resolveOutcomePositionId(resolve, address, r, asset) {
188
219
  /**
189
220
  * One {@link SignalEvaluation} per per-signal result entry, each enriched with the matching input
190
221
  * signal (via `findSignal`) or a synthesized summary from the result entry itself when none matches.
222
+ *
223
+ * The per-signal work window (`startedAt`/`endedAt`, stamped by the action loop) is threaded onto
224
+ * the evaluation. A half window is fabricated extent, so both stamps must be present or neither is
225
+ * kept; on clock skew (`endedAt < startedAt`) the start is pulled up to the end, matching the
226
+ * pass-window convention.
191
227
  */
192
228
  function buildEvaluations(results, findSignal, fallbackTs, resolvePositionId, address) {
193
229
  return results.map((raw) => {
@@ -200,67 +236,223 @@ function buildEvaluations(results, findSignal, fallbackTs, resolvePositionId, ad
200
236
  score: num(r.score) ?? 0,
201
237
  timestamp: fallbackTs,
202
238
  };
203
- return { signal, outcome: buildOutcome(r, resolveOutcomePositionId(resolvePositionId, address, r, asset)) };
239
+ let startedAt = num(r.startedAt);
240
+ const endedAt = num(r.endedAt);
241
+ if (startedAt !== undefined && endedAt !== undefined && endedAt < startedAt)
242
+ startedAt = endedAt;
243
+ return {
244
+ signal,
245
+ outcome: buildOutcome(r, resolveOutcomePositionId(resolvePositionId, address, r, asset)),
246
+ ...(startedAt !== undefined && endedAt !== undefined ? { startedAt, endedAt } : {}),
247
+ };
204
248
  });
205
249
  }
206
250
  /**
207
- * Pair a held {@link PendingDecision} with its `tracking.action_result` payload into one record.
208
- * `evaluations` is one entry per per-signal result, each enriched with the matching input signal.
209
- * The pass span runs decision-ts → action_result-ts; on clock skew (`endedAt < startedAt`) the start
210
- * is pulled up to `endedAt`, yielding a zero (never negative) duration.
251
+ * Build the whole pass record from a `tracking.action_result` payload alone — the producers echo
252
+ * the decision window and scan provenance onto the payload (`scannerId`, `scannerSignals`,
253
+ * `minConfidence`, `decisionStartedAt`, `decisionEndedAt`, `runId`), and the decision itself rides
254
+ * `result.data._decisionMeta`, so no pairing with `tracking.decision` is needed.
255
+ *
256
+ * `evaluations` is one entry per per-signal result, each enriched with the matching input signal
257
+ * (or a summary synthesized from the result entry when none matches — e.g. close-position's
258
+ * static-params path, which carries no `scannerSignals`). A pass without `_decisionMeta` yields no
259
+ * `decision` (the contract allows a decision-less pass).
260
+ *
261
+ * The pass span runs `decisionStartedAt` → action_result-ts; when the start stamp is absent
262
+ * (defensive only — the bus is in-process) `startedAt = endedAt`. On clock skew
263
+ * (`endedAt < startedAt`) the start is pulled up to `endedAt`, yielding a zero (never negative)
264
+ * duration.
211
265
  */
212
- export function buildSignalEvaluated(pending, actionResultPayload, nowMs, resolvePositionId) {
266
+ export function buildSignalEvaluated(actionResultPayload, nowMs, resolvePositionId) {
267
+ const { data, result, resultData, results } = actionResults(actionResultPayload);
268
+ if (!data)
269
+ return null;
213
270
  const p = asObject(actionResultPayload);
214
- const { results } = actionResults(actionResultPayload);
215
- const evaluations = buildEvaluations(results, (asset) => pending.signals.find((s) => s.asset === asset), pending.startedAtMs, resolvePositionId, str(p?.address));
216
- let startedAt = pending.startedAtMs;
217
- let endedAt = toEpochMs(p?.timestamp, nowMs);
271
+ const endedAt = toEpochMs(p?.timestamp, nowMs);
272
+ let startedAt = num(data.decisionStartedAt) ?? endedAt;
218
273
  if (endedAt < startedAt)
219
274
  startedAt = endedAt;
275
+ const signals = Array.isArray(data.scannerSignals)
276
+ ? data.scannerSignals.map((s) => buildSignalSummary(s, startedAt))
277
+ : [];
278
+ // Indexed once (first occurrence wins, matching find() semantics) — this runs in the producer's
279
+ // synchronous emit path, so the per-evaluation lookup must not rescan the signal array.
280
+ const byAsset = new Map();
281
+ for (const s of signals)
282
+ if (!byAsset.has(s.asset))
283
+ byAsset.set(s.asset, s);
284
+ const evaluations = buildEvaluations(results, (asset) => byAsset.get(asset), startedAt, resolvePositionId, str(p?.address));
285
+ const meta = asObject(resultData?._decisionMeta);
286
+ const decision = meta
287
+ ? {
288
+ execute: bool(meta.execute) ?? false,
289
+ actionType: str(result?.action_type) ?? "",
290
+ actionName: str(data.actionName),
291
+ mode: str(meta.mode),
292
+ confidence: num(meta.confidence) ?? 0,
293
+ confidenceThreshold: num(data.minConfidence),
294
+ reasoning: str(meta.reasoning) ?? "",
295
+ model: str(meta.model),
296
+ tokensUsed: numOrNull(meta.tokensUsed),
297
+ durationMs: num(meta.durationMs),
298
+ endedAt: num(data.decisionEndedAt),
299
+ }
300
+ : undefined;
220
301
  return {
221
302
  type: "signal_evaluated",
222
- scannerId: pending.scannerId,
303
+ scannerId: str(data.scannerId),
304
+ runId: str(data.runId),
223
305
  startedAt,
224
306
  endedAt,
225
- decision: pending.decision,
307
+ decision,
226
308
  evaluations,
227
309
  };
228
310
  }
311
+ // ─── Self-learning event log: decision.made + signal.outcome (Task 8) ─────────────
312
+ //
313
+ // Both are DERIVED from the SAME built SignalEvaluatedRecord (single-source, DRY) — no re-parse of
314
+ // the payload, no new bus event. The mapper does the redaction; these builders populate faithfully.
315
+ /** Decision action type → the coarse open/close/none intent. */
316
+ function actionIntent(actionType) {
317
+ if (actionType === "open_position")
318
+ return "open";
319
+ if (actionType === "close_position")
320
+ return "close";
321
+ return "none";
322
+ }
323
+ /** The four-value disposition from the granular per-signal {@link SignalOutcomeResult}. */
324
+ function disposition(result) {
325
+ switch (result) {
326
+ case "executed":
327
+ return "accepted";
328
+ case "below_confidence":
329
+ return "rejected";
330
+ case "error":
331
+ return "error";
332
+ // rejected (actively blocked by a gate) and skipped (not attempted) both read as "blocked"
333
+ // at the disposition grain; the granular `reasonCode` is kept verbatim for the why.
334
+ default:
335
+ return "blocked";
336
+ }
337
+ }
229
338
  /**
230
- * A `tracking.action_result` with no held decision — a producer that never emits `tracking.decision`
231
- * for the pass (e.g. close-position's static-params path, `decision_mode: 'none'` + `params.asset`).
232
- * The record carries the per-signal outcomes with signals synthesized from the result entries and no
233
- * `decision` (the contract allows a decision-less pass); zero duration (`startedAt = endedAt` = the
234
- * action_result timestamp).
339
+ * The pass's `decision.made` log record, derived from the built pass record — `null` when the pass
340
+ * carried no decision (a decision-less pass yields no judgment event). One per pass.
235
341
  */
236
- export function buildResultOnlyEvaluated(actionResultPayload, nowMs, resolvePositionId) {
237
- const { data, results } = actionResults(actionResultPayload);
238
- if (!data)
342
+ export function buildDecisionMade(record) {
343
+ const d = record.decision;
344
+ if (!d)
239
345
  return null;
240
- const p = asObject(actionResultPayload);
241
- const endedAt = toEpochMs(p?.timestamp, nowMs);
242
346
  return {
243
- type: "signal_evaluated",
244
- scannerId: str(data.scannerId),
245
- startedAt: endedAt,
246
- endedAt,
247
- evaluations: buildEvaluations(results, () => undefined, endedAt, resolvePositionId, str(p?.address)),
347
+ type: "decision_made",
348
+ occurredAt: record.endedAt,
349
+ mode: d.mode,
350
+ byLlm: d.mode === "llm",
351
+ model: d.model,
352
+ tokensUsed: d.tokensUsed,
353
+ confidence: d.confidence,
354
+ confidenceThreshold: d.confidenceThreshold,
355
+ durationMs: d.durationMs,
356
+ actionIntent: actionIntent(d.actionType),
357
+ // Market context + marginSource are NOT on the payload today → left undefined (see impl notes).
358
+ reasoning: d.reasoning,
359
+ };
360
+ }
361
+ /**
362
+ * One `signal.outcome` log record per evaluation in the pass, derived from the built pass record.
363
+ * `rawSignal` is the evaluation's {@link SignalSummary} (incl. its `meta`/`factors`) — the variable
364
+ * scanner evidence; redaction over it happens in the mapper, not here.
365
+ */
366
+ export function buildSignalOutcomes(record) {
367
+ return record.evaluations.map((ev) => {
368
+ const { signal, outcome } = ev;
369
+ return {
370
+ type: "signal_outcome",
371
+ occurredAt: record.endedAt,
372
+ scannerId: record.scannerId,
373
+ signalType: signal.signalType !== "" ? signal.signalType : undefined,
374
+ asset: signal.asset,
375
+ direction: signal.direction,
376
+ score: signal.score,
377
+ signalTs: signal.timestamp,
378
+ factors: signal.factors,
379
+ result: disposition(outcome.result),
380
+ reasonCode: outcome.reasonCode,
381
+ positionId: outcome.positionId,
382
+ marginAmount: outcome.marginAmount,
383
+ leverage: outcome.leverage,
384
+ notionalValue: outcome.notionalValue,
385
+ size: outcome.size,
386
+ // marginSource is NOT on the payload today → left undefined (see impl notes).
387
+ orderId: outcome.orderId,
388
+ errorName: outcome.errorName,
389
+ rawSignal: { ...signal },
390
+ };
391
+ });
392
+ }
393
+ // ─── Self-learning event log: dsl.close_attempt (Task 8) ──────────────────────────
394
+ /** Coerces the producer's outcome string to the close-attempt result enum; default `error`. */
395
+ function closeAttemptResult(value) {
396
+ return value === "ok" ? "ok" : "error";
397
+ }
398
+ /**
399
+ * From the existing `tracking.dsl_close_attempt` monitor event — the second consumer (alongside the
400
+ * tick trace). Fields at the payload root: `positionId`, `asset`, `dex`, `closeReason`, `attempt`,
401
+ * `outcome` ("ok"|"error"), `error`, ISO `timestamp`. `null` when the payload is not an object.
402
+ */
403
+ export function buildDslCloseAttempt(payload, nowMs) {
404
+ const p = asObject(payload);
405
+ if (!p)
406
+ return null;
407
+ return {
408
+ type: "dsl_close_attempt",
409
+ occurredAt: toEpochMs(p.timestamp, nowMs),
410
+ positionId: str(p.positionId),
411
+ asset: str(p.asset),
412
+ dex: str(p.dex),
413
+ reason: str(p.closeReason),
414
+ attempt: num(p.attempt),
415
+ result: closeAttemptResult(p.outcome),
416
+ error: str(p.error),
248
417
  };
249
418
  }
419
+ // ─── Self-learning event log: order.{placed,filled,failed} (Task 8a) ──────────────
250
420
  /**
251
- * A decision that never received its action_result (the action threw or returned before emitting
252
- * one). Flushed when the same action's next decision arrives, so a decided-but-unresulted pass is
253
- * visible on the signal axis rather than silently dropped: no evaluations, zero duration
254
- * (`startedAt = endedAt =` the decision timestamp).
421
+ * From the additive `tracking.order_*` events the open/close actions fire (the HookEvent shape:
422
+ * fields under a `data` envelope). One record per event; `eventKind` discriminates which fields the
423
+ * payload carries (placed: order/size; filled: fill; failed: reason/error). `positionId` maps from
424
+ * `__positionId`. `null` when no `asset` is resolvable — the asset is the one always-present scalar.
425
+ *
426
+ * Faithful population only (no redaction here — the mapper is the single redaction seam).
255
427
  */
256
- export function buildDecisionOnlyEvaluated(pending) {
428
+ export function buildOrderEvent(eventKind, payload, nowMs) {
429
+ const p = asObject(payload);
430
+ const data = asObject(p?.data);
431
+ if (!data)
432
+ return null;
433
+ const asset = str(data.asset);
434
+ if (asset === undefined)
435
+ return null;
257
436
  return {
258
- type: "signal_evaluated",
259
- scannerId: pending.scannerId,
260
- startedAt: pending.startedAtMs,
261
- endedAt: pending.startedAtMs,
262
- decision: pending.decision,
263
- evaluations: [],
437
+ type: "order_event",
438
+ event: eventKind,
439
+ occurredAt: toEpochMs(p?.timestamp, nowMs),
440
+ positionId: str(data.__positionId),
441
+ orderId: str(data.orderId),
442
+ asset,
443
+ direction: direction(data.direction),
444
+ orderType: str(data.orderType),
445
+ size: num(data.size),
446
+ limitPrice: num(data.limitPrice),
447
+ reduceOnly: bool(data.reduceOnly),
448
+ fillPrice: num(data.fillPrice),
449
+ fillSize: num(data.fillSize),
450
+ executionAsMaker: boolOrNull(data.executionAsMaker),
451
+ fees: num(data.fees),
452
+ partial: bool(data.partial),
453
+ reason: str(data.reason),
454
+ errorName: str(data.errorName),
455
+ error: str(data.error),
264
456
  };
265
457
  }
266
458
  // ─── Position lifecycle ──────────────────────────────────────────────────────
@@ -316,6 +508,8 @@ export function buildPositionLifecycle(event, payload, nowMs) {
316
508
  marginAmount: num(data.marginAmount),
317
509
  filled: boolOrNull(data.filled),
318
510
  reasoning: str(data.reasoning),
511
+ // Stamped by the position-tracker action on `on_position_opened` only (absent elsewhere).
512
+ reconciliationSource: str(data.reconciliationSource),
319
513
  };
320
514
  }
321
515
  // ─── DSL transitions ─────────────────────────────────────────────────────────
@@ -370,6 +564,59 @@ export function buildDslTransition(transition, payload, nowMs) {
370
564
  backendDslPositionId: typeof p.backendDslPositionId === "string" ? p.backendDslPositionId : p.backendDslPositionId === null ? null : undefined,
371
565
  };
372
566
  }
567
+ // ─── DSL monitor tick work units ─────────────────────────────────────────────
568
+ /** Coerces the producer's outcome string to the record's closed enum; anything else is dropped. */
569
+ function dslTickOutcome(value) {
570
+ return value === "ok" || value === "error" ? value : undefined;
571
+ }
572
+ /**
573
+ * From the monitor's `tracking.dsl_tick` / `tracking.dsl_position` / `tracking.dsl_close_attempt`
574
+ * / `tracking.dsl_sl_update` events (fields at the payload root). Each event yields one record —
575
+ * the deterministic ids derived from `tickId` (and `tickId:positionKey`, both bootId-scoped by
576
+ * the mapper) thread them into one trace, so no pairing is needed here. Essentials are `tickId`
577
+ * plus a numeric work window; on clock skew (`endedAt < startedAt`) the start is pulled up to the
578
+ * end, matching the pass-window convention. The price-fetch window is kept only when both stamps
579
+ * are present (a half window is fabricated extent). No `nowMs` fallback: work windows are
580
+ * producer-stamped; nothing here is fabricated.
581
+ */
582
+ export function buildDslTick(unit, payload) {
583
+ const p = asObject(payload);
584
+ if (!p)
585
+ return null;
586
+ const tickId = str(p.tickId);
587
+ let startedAt = num(p.startedAt);
588
+ const endedAt = num(p.endedAt);
589
+ if (tickId === undefined || startedAt === undefined || endedAt === undefined)
590
+ return null;
591
+ if (endedAt < startedAt)
592
+ startedAt = endedAt;
593
+ const pricesStartedAt = num(p.pricesStartedAt);
594
+ const pricesEndedAt = num(p.pricesEndedAt);
595
+ const bothPrices = pricesStartedAt !== undefined && pricesEndedAt !== undefined;
596
+ return {
597
+ type: "dsl_tick",
598
+ unit,
599
+ tickId,
600
+ startedAt,
601
+ endedAt,
602
+ strategyCount: num(p.strategyCount),
603
+ activePositionCount: num(p.activePositionCount),
604
+ positionsProcessed: num(p.positionsProcessed),
605
+ pricesStartedAt: bothPrices ? pricesStartedAt : undefined,
606
+ pricesEndedAt: bothPrices ? pricesEndedAt : undefined,
607
+ positionKey: str(p.key),
608
+ positionId: str(p.positionId),
609
+ asset: str(p.asset),
610
+ dex: str(p.dex),
611
+ outcome: dslTickOutcome(p.outcome),
612
+ error: str(p.error),
613
+ closeReason: str(p.closeReason),
614
+ attempt: num(p.attempt),
615
+ newSLPrice: num(p.newSLPrice),
616
+ slOrderId: num(p.slOrderId),
617
+ syncReason: str(p.syncReason),
618
+ };
619
+ }
373
620
  // ─── Runtime error ───────────────────────────────────────────────────────────
374
621
  /** From `signal_processing_error` — a bucket-2 error with no specific domain record home. */
375
622
  export function buildRuntimeError(payload, nowMs) {
@@ -398,6 +645,7 @@ export function buildRuntimeError(payload, nowMs) {
398
645
  message,
399
646
  errorName,
400
647
  scannerId: str(p.scannerId),
648
+ runId: str(p.runId),
401
649
  stack,
402
650
  };
403
651
  }