/**
 * GrowthDigestPublisher — Slice 2 of the proactive growth analyst.
 *
 * WHY THIS EXISTS (Justin, 2026-06-06, topic 21624): "I have YET to have an agent
 * proactively check in with me about ANY of these." Slice 1
 * (GrowthMilestoneAnalyst) already COMPUTES the growth picture (R1–R6) and exposes
 * it via read routes — but nothing ever SENDS it. This component is the voice:
 * on a cadence it takes the analyst's already-computed `GrowthDigest`, decides
 * whether there is anything worth saying, formats ONE consolidated "growth
 * check-in," and routes it through the SAME flood-guarded post-update funnel the
 * `/telegram/post-update` route uses.
 *
 * It owns NO analysis. It is a cadence + lease-check + decide-to-speak + format +
 * deliver + audit wrapper. It can never block, delay, or rewrite anything — it
 * only sends a message or stays quiet. Spec:
 * docs/specs/PROACTIVE-GROWTH-DIGEST-PUBLISHER-SLICE2-SPEC.md.
 *
 * Hardened by the Slice-2 convergence review (§9):
 *  - MULTI-MACHINE: an in-process croner runs on BOTH the awake and the standby
 *    machine, so the digest would double-send. The publisher is lease-gated
 *    (`isAwake`) — only the awake machine sends (mirrors the scheduler /
 *    ActivitySentinel precedent the superseded job relied on).
 *  - SINGLE FUNNEL: the `send` dep is the shared res-free `evaluateOutbound` path
 *    (`postToUpdatesTopic`), never a raw `sendToTopic` — one guarded chokepoint,
 *    not two.
 *  - NO CALM NOISE: a fully-calm week is silent by default
 *    (`sendOnCalmWeeks:false`) — a weekly "all healthy" is the exact noise the
 *    operator killed burnDetection for.
 *  - MISSED-RUN CATCH-UP: croner schedules only the next fire; a fire that elapsed
 *    while the box was asleep is replayed once on `.start()` (the proactive
 *    check-in must not be silently dropped for a week). Idempotent on the window
 *    ISO so a restart loop can't re-fire the same window.
 */
import type { GrowthDigest } from './GrowthMilestoneAnalyst.js';
export type GrowthDigestDelivery = 'off' | 'dry-run' | 'live';
export type GrowthDigestTrigger = 'cron' | 'catchup' | 'manual';
export interface DeliveryResult {
    ok: boolean;
    /** Why a non-send happened (dedup/budget/tone block, no-updates-topic, …). A
     *  block is a NORMAL outcome, never an error — the publisher never re-acts. */
    reason?: string;
}
export interface GrowthDigestAuditEntry {
    ts: string;
    action: 'sent' | 'send-blocked' | 'dry-run' | 'skipped-standby' | 'skipped-off' | 'skipped-overlap' | 'skipped-calm' | 'error';
    trigger?: GrowthDigestTrigger;
    /** ISO of the scheduled window this cycle belongs to. The idempotency key for
     *  catch-up: recorded ONLY on a real post-lease decision (never on a pre-lease
     *  `skipped-standby`), so the awake machine still owns an un-consumed window. */
    window?: string;
    reason?: string;
    counts?: GrowthDigest['counts'];
    /** For `dry-run`: the EXACT message that WOULD have been sent (how the operator
     *  inspects a real sample before going live). */
    wouldSend?: string;
    /** §3.5 belt: on a live send while `initiative-digest-review` is still enabled,
     *  a SIGNAL (never a cross-component mutation) that the old voice should be
     *  disabled to avoid two voices on the same initiatives. */
    supersedeConflict?: boolean;
}
export interface GrowthDigestPublisherDeps {
    /** Bound to the live analyst (`(now) => analyst.buildDigest(now)`). */
    buildDigest: (now: Date) => GrowthDigest;
    /** `monitoring.growthAnalyst.digestCron` (default '0 11 * * 1'). */
    cron: string;
    /** Rollout stage. The publisher is only constructed when mode !== 'off'. */
    mode: GrowthDigestDelivery;
    /** IANA tz for BOTH the cron fire and the rendered header date (default UTC). */
    timezone?: string;
    /** Send on a fully-calm week? Default false (no "all healthy" heartbeat). */
    sendOnCalmWeeks?: boolean;
    /** The SINGLE guarded funnel to the Updates topic. Attached at route
     *  registration via `attachSender` (where `ctx`/the route helper lives). */
    send?: (text: string) => Promise<DeliveryResult>;
    /** Multi-machine lease gate — only the awake machine sends. Default no-op. */
    isAwake?: () => boolean;
    /** Append-one-JSON-line audit sink (default → logs/growth-digest.jsonl). */
    audit?: (entry: GrowthDigestAuditEntry) => void;
    /** The set of window ISOs already decided (for catch-up idempotency). */
    recordedWindows?: () => Set<string>;
    /** §3.5 belt: is the superseded `initiative-digest-review` job still enabled? */
    supersededJobStillEnabled?: () => boolean;
    now?: () => Date;
    onError?: (where: string, err: unknown) => void;
    /** Settle delay before the missed-run catch-up (default 60s — long enough for
     *  the multi-machine lease to settle so a freshly-booted machine that acquires
     *  the lease isn't wrongly skipped). */
    settleMs?: number;
    /** Formatter: low/normal bulk cap per rule (default 5). */
    perRuleCap?: number;
    /** Formatter: per-detail char cap (default 200). */
    detailCap?: number;
}
export declare class GrowthDigestPublisher {
    private readonly deps;
    private readonly cron;
    private readonly mode;
    private readonly timezone?;
    private readonly sendOnCalmWeeks;
    private readonly settleMs;
    private readonly perRuleCap;
    private readonly detailCap;
    private sender?;
    private cronTask;
    private settleTimer;
    private running;
    constructor(deps: GrowthDigestPublisherDeps);
    private nowFn;
    /** Attach the guarded sender after construction (route-registration time). */
    attachSender(send: (text: string) => Promise<DeliveryResult>): void;
    /** True once the cron task is scheduled (false if refused by the sanity-floor
     *  or an invalid cron). Used by wiring tests. */
    isStarted(): boolean;
    /**
     * Schedule the cadence + arm the missed-run catch-up. Idempotent. Refuses a
     * sub-hourly cadence (sanity-floor) and an invalid cron (both logged via
     * onError; the publisher simply does not start, which is the safe direction —
     * an observe-only-derived component never crashes the server).
     */
    start(): void;
    /** Stop the cron + cancel a pending catch-up. Idempotent. */
    stop(): void;
    /** Replay a single fire time that elapsed while the box was down/asleep. */
    private catchUp;
    /**
     * Run ONE cadence cycle. PUBLIC so tests (and a future debug route) can drive a
     * cycle deterministically. Never throws — an observe-only-derived component must
     * never crash the server, so every branch is wrapped and audited.
     */
    publishOnce(now: Date, trigger: GrowthDigestTrigger, windowKey?: string): Promise<void>;
    private record;
    /** True if the cadence's two soonest fires are ≥1h apart (or not computable). */
    private cadenceWithinFloor;
    /**
     * The most-recent scheduled fire time at/under `now`. croner's `previousRun()`
     * only tracks the instance's own executions (null on a fresh instance), so we
     * derive the cadence interval from two future `nextRun` probes and scan forward
     * from a bounded lookback to find the last fire ≤ now.
     */
    private previousScheduledFire;
}
export interface FormatDigestOptions {
    timezone?: string;
    perRuleCap?: number;
    detailCap?: number;
}
/**
 * Render a `GrowthDigest` into ONE compact Telegram message. The analyst already
 * decided what crosses a rule — the formatter only renders it. Guarantees:
 *  - Priority-never-truncate: every `priority:'high'` finding and every
 *    decision-demanding maturity action (R1 promote, R6 dev-gate-dark) is rendered
 *    IN FULL, never capped.
 *  - Only the low/normal BULK is capped at `perRuleCap` per rule with a "+N more".
 *  - Cap-before-concat: bulk sections stop appending as the running length nears
 *    4096 — the full N-line string is never materialised then sliced.
 *  - Render-boundary scrub: every title/detail passes through `scrubSecrets`, and
 *    each detail is hard-capped to `detailCap` chars (covers dry-run text too).
 */
export declare function formatDigest(digest: GrowthDigest, opts?: FormatDigestOptions): string;
/** Default audit sink + window-reader over logs/growth-digest.jsonl. The publisher
 *  injects `audit` (write) and `recordedWindows` (read) from this so the same file
 *  is the durable "did we publish this window?" record. */
export declare function createGrowthDigestAuditSink(stateDir: string): {
    write: (entry: GrowthDigestAuditEntry) => void;
    recordedWindows: () => Set<string>;
    logPath: string;
};
//# sourceMappingURL=GrowthDigestPublisher.d.ts.map