/** * The faf_loop decision core — the brain of "100% or ask human". * * A pure, snapshot decision: given a parsed .faf, classify its remaining gaps * as SOURCEABLE (detection can fill — language, stack: no human needed) or * HUMAN (only the human knows — the goal + the 6Ws), and return a verdict: * * done — nothing left (or already 100%). Stop, success. * can-source — sourceable gaps remain → the loop runs auto and re-scores. * needs-human — ONLY human gaps remain → the wall; ask (or seed-and-confirm). * * The classification IS the human/sourced boundary at runtime: a slot is human * iff its slot category is 'human' (the 6Ws) or it's the goal sentence. The * loop sources everything it can FIRST (minimise human asks); only when the * sourceable well is dry does it turn to the human. It never invents a human * slot to reach 100% — needs-human is a legitimate, honest terminal, not a * failure. * * This is the deterministic engine the CLI `faf loop` and the agentic * `/faf-loop` skill both decide from. Orchestration (run auto, re-score, the * no-progress + iteration-cap guards) lives in the command on top of this. */ export type LoopStatus = 'done' | 'can-source' | 'needs-human'; export interface LoopGaps { /** Empty active slots only the human can give — the goal + the 6Ws. */ human: string[]; /** Empty active slots detection can fill — language + the stack. */ sourceable: string[]; } export interface LoopVerdict { status: LoopStatus; score: number; gaps: LoopGaps; /** The questions to put to the human — populated only when status is * 'needs-human' (sourceable work is done; the human is all that's left). */ ask: Array<{ path: string; question: string; }>; } /** Default empty test: null/undefined, blank, or a placeholder token. The * What-Not (`slotignored`) is handled separately — it is never a gap. */ export declare function isEmptyValue(value: unknown): boolean; /** A path is HUMAN (only the human can give it) iff it is the goal sentence or * its slot category is 'human' (the 6Ws). Everything else — name, language, * the stack — is sourceable by detection. */ export declare function isHumanSlot(path: string): boolean; /** * Classify the empty, non-ignored interviewable slots of a .faf into human vs * sourceable. Scope = the interviewable slots (the 6Ws + name + goal + the core * stack); `slotignored` (the What-Not) is never a gap. */ export declare function classifyGaps(data: Record, isEmpty?: (value: unknown) => boolean): LoopGaps; /** * The loop verdict for a .faf snapshot. `score` is the current AI-readiness * score (the loop's termination test). Precedence is deliberate: source * everything possible BEFORE asking the human — so 'can-source' wins while any * sourceable gap remains, and 'needs-human' fires only once they're exhausted. */ export declare function loopVerdict(score: number, data: Record, isEmpty?: (value: unknown) => boolean): LoopVerdict; export type LoopRunStatus = 'done' | 'needs-human' | 'stuck' | 'capped' | 'no-faf'; export interface LoopDeps { /** The current .faf as {data, yaml}, or null when none exists. */ read(): { data: Record; yaml: string; } | null; /** Score raw .faf yaml → 0..100 (the termination test). */ score(yaml: string): number; /** Source what detection can (auto): fill the stack and WRITE the .faf. */ runAuto(): void; } export interface LoopRunOptions { /** Hard cap on auto rounds (default 5). */ maxRounds?: number; isEmpty?: (value: unknown) => boolean; } export interface LoopRunResult { status: LoopRunStatus; score: number; /** Auto rounds actually run. */ rounds: number; /** Score after each read — the climb, for narration. */ history: number[]; /** The human questions to put — populated only when status is 'needs-human'. */ ask: Array<{ path: string; question: string; }>; } export declare function runLoop(deps: LoopDeps, opts?: LoopRunOptions): LoopRunResult;