/** * Template Drift Detector — Tier-2 sentient proposal source (T9895). * * Emits a Tier-2 proposal for each {@link TemplateManifestEntry} whose * deployed copy at `installPath` has drifted from the rendered source AND * whose `updateStrategy` supports refresh (`'overwrite-on-bump'` or * `'manifest-merge'`). `'immutable'` entries are intentionally locked and * are never proposed; `'diff-prompt'` entries are skipped because they * already require interactive owner action and a Tier-2 proposal would be * redundant noise. Uninstalled entries are also skipped — installation is a * separate lifecycle event handled by `cleo templates install`. * * Design principles: * - NO LLM calls. All data comes from a structured manifest read + a * byte-for-byte file comparison. * - Plural detector: returns an array of proposals (one per drifted * entry) rather than a single nullable proposal. Companion to T9896 * {@link import('./context-staleness.js').detectContextStaleness}. * - Generation vs. persistence are decoupled — the detector ALWAYS * computes and returns proposals; the wrapper * {@link safeRunTemplateDriftScan} respects kill-switch + tier2Enabled * before any side effect. * - All file/read/registry errors collapse to empty array (best-effort — * never throw). * * Integration point: {@link safeRunTemplateDriftScan} is fired (best-effort) * from `tick.ts` alongside the existing stage-drift / hygiene / context- * staleness scans (wiring is a separate task — this module is the leaf). * * @task T9895 * @epic T9855 * @see T9896 — Context-staleness detector (pattern reference) * @see T9886 — `cleo templates diff ` (CLI equivalent) * @see T9877 — Template registry SSoT * @see ADR-076 — Project context detection * @see ADR-054 — Sentient Loop Tier-2 */ /** * Default cadence (12 hours) between template-drift scan passes when the * detector is wired into the tick loop. Longer than context-staleness * (6 h) because template drift only changes when the consumer manually * edits an installed template file — a low-frequency event. */ export declare const TEMPLATE_DRIFT_SCAN_INTERVAL_MS: number; /** Discriminant for template-refresh proposals. */ export declare const TEMPLATE_REFRESH_KIND: "template-refresh"; /** * Severity assigned to detector-emitted Tier-2 proposals. * Mirrors the `severity` column allowlist (`P0`|`P1`|`P2`|`P3`) defined in * `packages/contracts/src/enums.ts`. */ export type Tier2ProposalSeverity = 'P0' | 'P1' | 'P2' | 'P3'; /** * Detector-side Tier-2 proposal shape for template drift (T9895 contract). * * Same lightweight shape used by the sibling T9896 detector but with a * dedicated `kind` discriminant. Each entry maps to ONE proposal so that * the persistence layer can dedup on `id` and the orchestrator can render * a per-template fix prompt. */ export interface Tier2Proposal { /** * Stable identifier — `prop-template-drift-`. Deterministic per * `(detector, entry)` so repeated detections dedup cleanly on persistence. */ id: string; /** Discriminant — `'template-refresh'` for this detector. */ kind: typeof TEMPLATE_REFRESH_KIND; /** Short human-readable headline. */ title: string; /** Severity bucket; fixed at `'P3'` for template-drift (low priority). */ severity: Tier2ProposalSeverity; /** CLI command the owner should run to reconcile the drift. */ fixAction: string; /** Why this proposal was emitted (includes entry id, kind, paths, strategy). */ reason: string; } /** Options accepted by {@link safeRunTemplateDriftScan}. */ export interface TemplateDriftScanOptions { /** Absolute path to the project root (contains `.cleo/`). */ projectRoot: string; /** Absolute path to sentient-state.json. */ statePath: string; /** * Override the kill-switch check. Injected by tests to avoid touching the * state file. When omitted, reads the daemon state file directly. */ isKilled?: () => Promise; /** * Override the tier2Enabled check. Injected by tests. When omitted, reads * the daemon state file directly. */ isTier2Enabled?: () => Promise; /** * Override the detector function. Injected by tests so the scan wrapper * can be exercised without a real template registry on disk. */ detect?: (projectRoot: string) => Promise; } /** Outcome discriminant produced by {@link safeRunTemplateDriftScan}. */ export type TemplateDriftScanKind = 'killed' | 'disabled' | 'no-drift' | 'drifted' | 'error'; /** Structured outcome of one scan pass. */ export interface TemplateDriftScanOutcome { /** How the scan ended. */ kind: TemplateDriftScanKind; /** Every proposal generated by the detector (empty when none). */ proposals: Tier2Proposal[]; /** Human-readable detail (one line). */ detail: string; } /** * Run the template-drift detector against `projectRoot` and return one * {@link Tier2Proposal} per drifted entry. * * Returns an empty array when: * - No templates are registered. * - Every refreshable entry is either uninstalled or in-sync. * - The registry cannot be enumerated (best-effort — never throws). * * Entries with `updateStrategy: 'immutable'` or `'diff-prompt'` are * skipped (see {@link REFRESHABLE_STRATEGIES}). Entries that are not yet * installed (`getInstalledStatus(...).installed === false`) are also * skipped — installation is a separate lifecycle event. * * This function MUST NOT throw — all errors collapse to `[]`. * * @param projectRoot - Absolute path to the project root to probe. * @returns Array of drift proposals (empty when no action is required). * * @task T9895 */ export declare function detectTemplateDrift(projectRoot: string): Promise; /** * Run the template-drift detector with sentient gating applied. * * Steps: * 1. Honour the kill-switch — abort before any side effect. * 2. Run the detector. If it returns `[]`, return outcome `'no-drift'`. * 3. When proposals are generated, honour `tier2Enabled` — the proposals * are returned to the caller either way, but the outcome discriminant * marks whether persistence is permitted. * * This wrapper NEVER throws — all errors collapse to an `'error'` outcome. * * @param options - See {@link TemplateDriftScanOptions}. * @returns {@link TemplateDriftScanOutcome}. * * @task T9895 */ export declare function safeRunTemplateDriftScan(options: TemplateDriftScanOptions): Promise; //# sourceMappingURL=template-drift.d.ts.map