/** * plan-solver — the ONE routine that turns per-pattern data into a * "cut N% of the bill" plan, shared by the POC path (log10x_poc_from_local) * and the analysis path (log10x_estimate_savings / cost_options). Before this, * the two paths ran different algorithms — POC a byte "droppable fraction", * analysis a byte-denominated greedy — and disagreed (65% vs 0% on the same * environment). This makes them agree by construction. * * The model is the destination ladder, made explicit: * * keep everything, recoverable lossy, opt-in * ───────────────────────────── ────────────── * 1 compact in SIEM, smaller 4 sample keep 1 in N * 2 tier_down in SIEM, cheaper tier 5 drop keep nothing * 3 offload out of SIEM, in your S3 (needs the retriever) * * Rules the solver enforces: * - Errors and warnings are pinned at `pass` — never eligible for anything. * - The target is met with keep-everything rungs (1-3) FIRST. Lossy rungs are * surfaced only as a labelled opt-in when the keep-everything ceiling falls * short, and applied only when the caller passes allowLossy. * - offload (rung 3) is available only when the retriever is installed, since * without it the offloaded events are unreachable. So a destination with no * compact and no tier_down and no retriever collapses to drop — honestly. * - Accounting is in DOLLARS (bill = bytes × rate; each lever cuts its own * factor via cost.projectAction), so tier_down — which keeps every byte and * cuts the rate — reads as the real cut it is instead of a byte "0%". * - Scope is a set of services (or all). A pattern touching any scoped service * is a candidate, weighted by its bytes in those services. */ import { type Action } from './cost.js'; import { type SiemId } from './siem/pricing.js'; /** One pattern (message type) as the solver consumes it. */ export interface SolverPattern { /** * The template body with `$` marking variable slots — the message type's * own skeleton, the unit made visible. Present where the source has real * templates (the POC extraction); absent on the analysis path until its * metrics carry one. First line only for multi-line templates. */ skeleton?: string; /** Stable identity (tenx_hash / templateHash). */ hash: string; /** Human-readable message-type face. */ name: string; /** Service → bytes attributed to this pattern in that service. A pattern * usually maps to one service, but a shared log line can span several, so * the shape is a distribution. */ services: Record; /** Dominant severity for the pattern. */ severity: string; /** Total observed bytes for the pattern (over whatever window the caller * measured — the plan is percentage-correct at any scale). */ bytes: number; /** Average event size in bytes; degrades the compact ratio for tiny events. */ avgEventBytes?: number; } /** * What the plan is solving FOR. Three denominations: * - percent: "cut X% of the bill" — a one-shot project. * - usd_budget: "keep the (scoped) bill under $B/mo" — a standing constraint; * the reduction is derived: max(0, bill - budget). Idempotent: * already under budget -> an empty plan with headroom. * - gb_budget: "keep (scoped) ingest under V GB/mo" — BYTE accounting. * tier_down keeps every byte, so it contributes NOTHING to this * target and is excluded from the ladder; compact counts only * where it lands on the billed wire (compactsInPlace). */ export type PlanTarget = { kind: 'percent'; value: number; } | { kind: 'usd_budget'; value: number; } | { kind: 'gb_budget'; value: number; }; export interface SolveOpts { destination: SiemId; /** Whether the S3 retriever is installed. Gates the offload rung. */ retrieverInstalled: boolean; /** Reduction goal as a percent of the (scoped) bill, e.g. 50. Ignored when * `target` is supplied; kept for the existing percent callers. */ targetPct?: number; /** The full target union; wins over targetPct when present. */ target?: PlanTarget; /** Services to solve for; omit or 'all' for the whole estate. */ scope?: string[] | 'all'; /** Permit the lossy rungs (sample/drop) to close a keep-everything shortfall. * Default false: the plan stops at the keep-everything ceiling and names the * gap instead of silently discarding data. */ allowLossy?: boolean; /** Services pinned at pass (the POC's exception_services): their patterns * stay in the bill but are never planned, exactly like protected severities. * Matched case-insensitively against the pattern's dominant service. */ exceptionServices?: string[]; /** * The customer's blended all-in $/GB for this destination (their contracted * or invoice-derived rate). When present, every dollar on the plan is scaled * from the list-price structure to this blend — the ladder physics (lever * ratios) stay list-structure, the absolute dollars become theirs. Provenance * is echoed on the plan (rateSource / rateBasis). Absent = list price. */ customerRatePerGb?: number; /** * Specific pattern hashes pinned at pass — the referenced-types exclusion: * a type a monitor or saved search names stays exactly as it is unless the * user explicitly trades it back in. Pinned like protected severities. */ pinnedHashes?: string[]; /** * Whether the customer runs this destination themselves. Decides whether * compaction is available on the Elasticsearch/OpenSearch family: the * expander is the l1es PLUGIN, installable on your own nodes and nowhere * else. Absent = unknown, and unknown prices only the levers that hold on * any deployment. Never inferred. */ selfManaged?: boolean; /** * Specific pattern hashes the user EXPLICITLY unpinned from the severity * floor — a warn-level retry storm they chose to act on. Bypasses ONLY the * severity protection for the listed hashes; pinnedHashes and * exceptionServices still win. Never inferred, never estate-wide. */ unprotectPatterns?: string[]; } export interface PlannedRow { hash: string; name: string; /** Readable opener for the card: the identifier de-underscored and * whitespace-collapsed. Tool-emitted so every host opens the card with the * same noun instead of each agent deriving its own. Mechanical, never * interpretive: same tokens as `name`. */ displayName: string; dominantService: string; serviceMix: { service: string; sharePct: number; }[]; severity: string; billUsd: number; action: Action | 'pass'; savedUsd: number; /** The template skeleton ($-slots), passed through from the source. */ skeleton?: string; /** Volume-budget plans only: bytes this row removes from the billed wire. */ savedBytes?: number; keepsEverything: boolean; } export interface Plan { destination: SiemId; retrieverInstalled: boolean; scope: string[] | 'all'; /** The ask, echoed. For budgets this is the user's budget, and targetPct is * the DERIVED reduction percent the solver actually chased. */ target: PlanTarget; targetPct: number; billUsd: number; /** The keep-everything lever the destination resolves to (rung 1-3), or null * when only lossy rungs remain. */ keepEverythingLever: Action | null; /** Max percent of the bill removable while keeping everything (rungs 1-3 on * every non-error pattern). */ keepEverythingCeilingPct: number; achievedPct: number; met: boolean; /** Sum of planned rows' savedUsd — equals billUsd minus landsAtUsd, so the * arithmetic on a rendered plan closes visibly. */ totalSavedUsd: number; /** The pricing basis behind every dollar on this plan, one human-readable * line. Render it verbatim. */ rateBasis: string; /** Whose dollars these are: the destination list-price model, or the * customer's supplied blended rate scaled over the list structure. */ rateSource: 'list_price' | 'customer_supplied'; /** * True when this plan's dollars rest on a model rather than on the * destination's own meter. ClickHouse only, today: the per-GB rate behind * these figures is a storage rate and the bill is compute. Renderers must * carry the word "modeled" wherever they print one of these dollars. */ modeled: boolean; /** Why the dollars are modeled, in one line. Present only when modeled. */ modeledNote?: string; /** * Notes the cost model attached to this plan's projections, deduplicated. * The compute term's own sentences ride here, so a renderer that shows the * plan can show why a ClickHouse dollar is what it is. */ notes?: string[]; /** * What the levers this plan actually uses REQUIRE — the app, plugin, tier, * or licence, with its platform and version constraint. Only for levers the * plan uses. A priced lever with an unstated prerequisite is a lever we are * guessing at, so the render states these before the user agrees. */ prerequisites: string[]; /** Echo of the supplied blended rate when rateSource is customer_supplied. */ customerRatePerGb?: number; /** Total scoped bytes/mo behind the bill — the reconciliation multiplicand: * bytesInMonthly times the rate should foot against the invoice line. */ bytesInMonthly: number; /** usd_budget / percent targets: the bill after the plan, in $/mo. */ landsAtUsd?: number; /** gb_budget targets: monthly bytes toward the destination after the plan. */ landsAtBytesMonthly?: number; planned: PlannedRow[]; kept: PlannedRow[]; /** Present when the keep-everything ceiling is below the target. Names the * remedies (install the retriever, or accept loss) rather than pretending. */ gap: null | { remainingPct: number; remedies: Array<'install_retriever' | 'accept_loss'>; message: string; }; } /** * The destination's best keep-everything lever, respecting the ladder and the * retriever gate. compact wins over tier_down (both keep in-SIEM queryability; * compact also shrinks the bytes); offload is last and needs the retriever. */ export declare function keepEverythingLever(destination: SiemId, retrieverInstalled: boolean, opts?: { selfManaged?: boolean; }): Action | null; export declare function solvePlan(rawPatterns: SolverPattern[], opts: SolveOpts): Plan;