/** * The report an update returns — and the reason the verb is worth building. * * Most of what an in-place update can get wrong is invisible: a masked field that changes nothing, * a clamp that quietly rewrites a number, an exit ladder that does not move on positions already * open. None of those throw. A truthful report neutralises them more cheaply than code does, so the * report is the product and the swap is the plumbing. * * ## The obligation this discharges * * `dsl_preset` changes are forward-only: a new preset governs positions opened after it, and open * positions keep the ladder they were opened under (design doc §4.3). The preset's `exit:` siblings * — `order_type`, `interval_seconds`, the fee options — are read live and DO reach open positions, * so the forward-only claim is made about the ladder specifically, never about the whole block. * That is the right semantics, * and it is a trap for the user, because someone asking to tighten a stop is usually thinking about * a position they hold *right now*. If the report does not say "this does not affect that position", * they will believe they are protected and they are not. * * So the open-position table is not a nicety. It is the half of forward-only that makes it honest. * * ## Pure by construction * * Every runtime fact — open positions, gate state, the venue's own numbers — arrives as an * argument. This module reads nothing and decides nothing about whether to proceed; it renders what * it was handed. That keeps the whole thing golden-file testable, and it means `--dry-run` and a * real apply produce the identical document apart from {@link UpdateReport.applied}. */ import type { Finding } from "../validate/types.js"; import type { EffectiveConfig, EffectiveNote } from "./effective-config.js"; /** One open position at the moment of the update, as the report needs to describe it. */ export interface ReportedPosition { asset: string; direction: "LONG" | "SHORT"; entryPrice: number; /** The engine's current effective floor. */ floorPrice: number; phase: 1 | 2; /** True when the backend ratchet owns the venue stop and the runtime cannot re-configure it. */ backendManaged: boolean; } /** A guardrail that is currently refusing entries, as reported by the risk layer. */ export interface ReportedGate { gate: string; reason: string; } export interface UpdateReportInputs { runtimeId: string; /** False for `--dry-run`: the identical document, minus the claim that anything happened. */ applied: boolean; before: EffectiveConfig; after: EffectiveConfig; openPositions: readonly ReportedPosition[]; /** Gates currently CLOSED. Empty means entries are flowing. */ closedGates: readonly ReportedGate[]; /** `slots` in the new config; omitted when the recipe does not set it. */ slots?: number; /** Whether the new config changes `exit.dsl_preset`. Drives the forward-only wording. */ exitPresetChanged: boolean; /** * Conditions that do NOT stop a dry run but WILL refuse the apply — a missing or stale proof * being the one that exists today. * * A dry run changes nothing, so refusing it costs the reader the very information they asked for; * reporting the blocker instead lets them see the plan AND what stands between them and applying * it, in one pass rather than two. */ applyBlockers?: readonly Finding[]; /** * The two recipes describe the same strategy. * * Not the same as "nothing to do": an apply rebuilds the components AND restarts the external * scanner children, which is how a pure code change reaches a running strategy — a scanner reads * its entrypoint at spawn, so the restart is the delivery. The report has to say that, because * "no changes" over a recipe the user just edited the scanner beside reads as "your edit did * nothing". */ recipeUnchanged?: boolean; /** Every leaf this update moves. Empty when the two recipes agree. */ changedFields?: readonly ChangedField[]; /** * Set when the recipe applied but the external scanner children could not be restarted. * * The apply succeeded — the new configuration is live in-process — so this is not a failure. But * the outgoing children were already stopped, so for a strategy whose entries come from an * external scanner it is a silent trading halt, and a report that says only "Updated" would be * the last thing anyone read before wondering why nothing traded. */ scannersUnwired?: string; } export interface UpdateReport extends UpdateReportInputs { /** Notes present after the update that were not present before — the newly-ineffective fields. */ newlyIneffective: EffectiveNote[]; /** * `slots − open` WITHOUT the `Math.max(0, …)` the runtime applies. Negative means over-subscribed: * the new cap is below the current book, so entries stop until positions close. The runtime's own * clamp makes "2 slots, 3 open" and "2 slots, 2 open" both report zero available, which is why * this is computed here rather than read from the strategy state. */ slotHeadroom?: number; } /** One leaf of the recipe that this update moves. */ export interface ChangedField { /** Dotted path, with named list entries keyed by name rather than index. */ path: string; kind: "changed" | "added" | "removed"; before?: unknown; after?: unknown; } /** * Every leaf the new recipe moves, relative to the running one. * * The report used to describe consequences — masked fields, clamped values, open positions — and * never the change itself, so a plan over a recipe that altered eleven fields rendered identically * to a plan over an unchanged one. For a verb that plans by default so a human can check the change * before committing it, that left nothing to check. */ export declare function diffRecipes(before: unknown, after: unknown): ChangedField[]; export declare function buildUpdateReport(inputs: UpdateReportInputs): UpdateReport; /** * The human rendering. Ordered by what a reader needs first: what changed, then what it will not * touch, then what to expect afterwards. */ export declare function renderUpdateReport(report: UpdateReport): string; //# sourceMappingURL=update-report.d.ts.map