/** * Recurring tick runner — one deterministic execution of the cost-reduction * policy loop. * * Called by `bin/tenx-recur.ts` (CLI) once per invocation. * * Five-step flow: * 1. Load policy (already parsed by caller and passed in). * 2. For each scoped service, query Prometheus for top patterns over the * policy lookback window. * 3. Rank patterns by bytes; walk top-N applying severity + threshold rules. * 4. Compute delta vs. current applied state (read existing CSVs / action-intent). * 5a. If delta < min_delta_pp → exit 0 with "no change needed". * 5b. Else write mute/action-intent files and commit via gh CLI or * direct git push. * * The runner NEVER directly imports heavy MCP tool modules — it only uses the * lower-level libs (cost.ts, mute-csv-writer.ts, * action-intent-writer.ts) so it can run as a standalone CLI binary. * * Prometheus queries go through the existing `customer-metrics.ts` adapter * (LOG10X_CUSTOMER_METRICS_URL / LOG10X_CUSTOMER_METRICS_TYPE / …) which is * what `log10x_top_patterns` also uses under the hood. */ import type { Policy } from './policy-loader.js'; import type { CustomerMetricsBackend } from './customer-metrics.js'; import type { Action } from './cost.js'; import { type BudgetFiles, type DdQuery } from './budget-tick.js'; import { type EsRequest } from './elastic-budget-tick.js'; import { type ChQuery } from './clickhouse-rows-tick.js'; import type { DatadogMetricsCreds } from './datadog-metrics-read.js'; import { type K8sApi } from './k8s-in-cluster.js'; export interface TickOptions { dryRun?: boolean; verbose?: boolean; } export type TickStatus = 'no_change' | 'applied' | 'dry_run' | 'read_failed' | 'refused' | 'error'; export interface PatternDecision { /** * The pattern's message_pattern NAME (the engine's mute-file key — * rateReceiver fieldNames joins to message_pattern), NOT an 11-char * tenx_hash. The field name predates that distinction. */ pattern_hash: string; service: string; severity: string; bytes: number; share_pct: number; action: Action; reason: string; } export interface TickResult { status: TickStatus; message: string; /** Cumulative projected savings as a % of total observed bytes. */ projected_savings_pct: number; /** * Set when the projection leans on an assumption rather than a measurement: * the assumption, stated. Every place the percentage renders carries it. */ projected_savings_basis?: string; /** All per-pattern decisions the tick made (including 'pass'). */ applied_changes: PatternDecision[]; /** How many patterns changed vs. the prior state. */ delta_patterns: number; /** Delta in savings percentage points vs. prior state. */ delta_pp: number; /** Path to the appended run-history JSONL line (when not dry-run). */ history_path?: string; /** Budget run only: the report, in Markdown. */ budget_report?: string; /** Budget run only: the files written, or that would be. */ budget_files?: BudgetFiles; } /** * How the runner resolves its metrics backend. Defaults to the same * ambient-env resolution the MCP tools use (`loadBackendFromEnv`). * * Tests inject a deterministic mock via `_setBackendLoader()` instead of * monkey-patching the frozen `customer-metrics` ESM namespace (which throws * under strict ESM). Mirrors the `_set*` runner-injection seam used by other * libs in this package. */ type BackendLoader = () => Promise; /** Test seam: override how the runner resolves its metrics backend. */ export declare function _setBackendLoader(loader: BackendLoader): void; /** Test seam: restore the default ambient-env backend loader. */ export declare function _resetBackendLoader(): void; export declare class PromUnreachableError extends Error { constructor(message: string); } export declare function writeOutputFiles(repoPath: string, decisions: PatternDecision[], verbose: boolean): void; /** * The thermostat's audit trail: one markdown report per APPLIED tick, written * into the config repo so it rides the same commit as the CSVs it describes. * "When a scheduled run cuts something on a Tuesday, where do I see what it * did" — here, in the artifact the change-control process already reviews. * noop ticks write nothing (a daily no-change commit would be churn); they * land in the local history file instead. */ export declare function writeRunReport(repoPath: string, policy: Policy, decisions: PatternDecision[], totalBytes: number, projectedSavingsPct: number, deltaPatterns: number, deltaPp: number, basis?: string): void; /** * Run one tick of the recurring cost-reduction policy. * * @param policy Parsed and validated policy (from parsePolicyYaml). * @param opts Runtime options (dryRun, verbose). * @returns TickResult with status, projected savings, decisions made. * * @throws PromUnreachableError (exit code 2) when Prometheus cannot be reached. */ export declare function runTick(policy: Policy, opts?: TickOptions): Promise; /** Where the budget run's files sit on the policy branch: the folder the engine's git pull reads (GH_ENABLED). */ export declare const BUDGET_FILE_DIR = "log10x/policy"; export declare const BUDGET_REPORT_KEY = "budget-report.md"; /** The branch the engine's gitops pull reads the budget run's files from (GH_BRANCH). */ export declare const POLICY_BRANCH_DEFAULT = "log10x-policy"; interface BudgetDeps { creds?: DatadogMetricsCreds | null; query?: DdQuery; /** null: no cluster API (the run reports a read failure on configmap delivery). */ k8s?: K8sApi | null; nowMs?: () => number; /** The hot-tier run's Elasticsearch access; default from LOG10X_ES_URL and its key. */ es?: EsRequest | null; /** The rows run's ClickHouse access; default from CLICKHOUSE_URL and its key. */ ch?: ChQuery | null; /** The rows run's Prometheus; default the ambient customer metrics backend. */ prom?: CustomerMetricsBackend | null; } /** Test seam: the Datadog reads, the cluster API and the clock for the budget run. */ export declare function _setBudgetDeps(deps: BudgetDeps): void; /** Test seam: restore the ambient Datadog keys, in-cluster API and clock. */ export declare function _resetBudgetDeps(): void; /** * One budget run (`reduction.budget_usd_monthly`). Reads month-to-date * Standard usage and the Receiver's series from Datadog, discovers or reads * the node counts, re-derives every row, and writes the four files to the * policy ConfigMap or the gitops policy branch. On a failed read nothing is * written; on a Receiver not recorded as counting events nothing is written. */ export declare function runBudgetTick(policy: Policy, opts?: TickOptions): Promise; /** * One rows run (`reduction.rows_per_hour`): reads the rows ClickHouse wrote in the * last complete hour and the Receiver's counters, rewrites the limits and totals * in lines so the table takes the rate, and writes the files the way the budget * run does. An offloaded line never enters the table, so there is nothing in flight. */ export declare function runRowsTick(policy: Policy, opts?: TickOptions): Promise; /** * One hot-tier run (`reduction.hot_tier_gb`): reads the hot nodes and the * Receiver's indices, rewrites the byte limits so the hot tier settles under * the size, and writes the files the way the budget run does. */ export declare function runHotTierTick(policy: Policy, opts?: TickOptions): Promise; /** * An S3 prefix can serve as the config plane instead of a git repo — the * AWS-native path for estates with no cluster and no GitHub (the Lambda * engine polls `TENX_RECEIVE_MUTE_S3_URI` from the same prefix). Requires * the aws CLI on PATH; state moves with `aws s3 cp` so no SDK dependency * is added. */ export declare function isS3ConfigPlane(repo: string): boolean; export {};