faf-cli 6.13.0 → 6.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,7 @@
17
17
  * Authoring doctrine baked into the prompts: 6Ws answers are terse LABELS
18
18
  * (3-4 words, hard cap <6) — a scannable spec card, not prose.
19
19
  */
20
+ import type { SeededContextDetailed } from '../detect/relentless.js';
20
21
  /** Bump when questions/options change — consumers can pin or report it. */
21
22
  export declare const INTERVIEW_VERSION = "faf-interview/1";
22
23
  export interface InterviewOption {
@@ -24,9 +25,9 @@ export interface InterviewOption {
24
25
  value: string;
25
26
  description: string;
26
27
  }
27
- export interface InterviewQuestion {
28
+ export interface InterviewQuestion<P extends string = string> {
28
29
  /** Canonical slot path (core/slots.ts is the spine). */
29
- path: string;
30
+ path: P;
30
31
  question: string;
31
32
  /** Short chip/header label (max ~12 chars) for option-UI consumers. */
32
33
  header: string;
@@ -34,22 +35,37 @@ export interface InterviewQuestion {
34
35
  required: boolean;
35
36
  options?: InterviewOption[];
36
37
  }
38
+ /**
39
+ * The human/sourced boundary, as TYPES — the load-bearing line in FAF.
40
+ *
41
+ * HumanSlotPath = the 8 things only a human knows (name + goal + the 6Ws).
42
+ * SourcedSlotPath = the slots detection fills (language + the stack).
43
+ *
44
+ * They are DISJOINT by construction. Typing the two interviews against these
45
+ * makes it a COMPILE error to put a sourced slot in the human interview (or
46
+ * vice-versa) — the "Interview-16" drift becomes unrepresentable at the Truth,
47
+ * not merely caught by a downstream test. "I'll remember" is not a fix; a type
48
+ * is. (claude-faf-mcp's wjttc-faf-go-boundary test is the belt; this is the
49
+ * braces — the source itself can no longer drift.)
50
+ */
51
+ export type HumanSlotPath = 'project.name' | 'project.goal' | 'human_context.who' | 'human_context.what' | 'human_context.why' | 'human_context.where' | 'human_context.when' | 'human_context.how';
52
+ export type SourcedSlotPath = 'project.main_language' | 'stack.frontend' | 'stack.backend' | 'stack.database' | 'stack.runtime' | 'stack.hosting' | 'stack.build' | 'stack.cicd';
37
53
  /**
38
54
  * THE 8-Q 6Ws INTERVIEW — the core. Project identity (name + goal) plus the
39
55
  * six Ws. Language is deliberately NOT here: detection finds it; humans are
40
56
  * only asked what machines cannot derive. The 6Ws are the underivable half.
41
57
  */
42
- export declare const SIX_WS_INTERVIEW: InterviewQuestion[];
58
+ export declare const SIX_WS_INTERVIEW: InterviewQuestion<HumanSlotPath>[];
43
59
  /**
44
60
  * Stack interview — asked only for slots ACTIVE for the app_type and still
45
61
  * empty. Selects where a common vocabulary exists; every select includes
46
62
  * Other (specify) and None where absence is legitimate.
47
63
  */
48
- export declare const STACK_INTERVIEW: InterviewQuestion[];
64
+ export declare const STACK_INTERVIEW: InterviewQuestion<SourcedSlotPath>[];
49
65
  /** The full ordered registry: the 8-Q core first, then stack. */
50
66
  export declare const INTERVIEW: InterviewQuestion[];
51
67
  /** Lookup by slot path. */
52
- export declare const INTERVIEW_BY_PATH: Map<string, InterviewQuestion>;
68
+ export declare const INTERVIEW_BY_PATH: Map<string, InterviewQuestion<string>>;
53
69
  /**
54
70
  * Plain-object companion to INTERVIEW_BY_PATH. A Map JSON-serializes to `{}`,
55
71
  * which reads as "the export shipped empty" to any consumer that crosses a
@@ -90,6 +106,12 @@ export interface TableOf8Row {
90
106
  value: string;
91
107
  status: BoxStatus;
92
108
  seeded: boolean;
109
+ /** Provenance for a SEEDED row — where the suggestion was sourced from, so the
110
+ * human confirms informed: 'project goal' or a README locus ('README:## Why').
111
+ * Absent on filled/empty rows. */
112
+ source?: string;
113
+ /** 0..1 confidence for a seeded row, by source quality. Absent otherwise. */
114
+ confidence?: number;
93
115
  }
94
116
  export interface TableOf8 {
95
117
  version: string;
@@ -100,11 +122,16 @@ export interface TableOf8 {
100
122
  complete: boolean;
101
123
  }
102
124
  /**
103
- * Build the Table-of-8 from a .faf object (any/empty) + an optional goal. The
104
- * goal seeds WHO/WHAT/WHERE via seedSixWsFromGoal (facts only); WHY/WHEN/HOW are
105
- * never seeded. Rows the .faf already fills are 'filled'; goal-seeded ones are
106
- * 'seeded' (suggestions); the rest are 'empty' (ask the human). Pure.
125
+ * Build the Table-of-8 from a .faf object (any/empty) + an optional goal and an
126
+ * optional sourced README extraction (relentlessContextDetailed). The goal seeds
127
+ * WHO/WHAT/WHERE via seedSixWsFromGoal (facts only, source 'project goal'); the
128
+ * detailed README form covers WHY/WHEN/HOW and any slot the goal didn't reach,
129
+ * each carrying its own provenance. Rows the .faf already fills are 'filled';
130
+ * seeded ones carry `source`+`confidence` so the human confirms informed; the
131
+ * rest are 'empty' (ask the human). Goal wins over README where both have a
132
+ * fact (the deliberate sentence beats the extraction). Pure.
107
133
  */
108
134
  export declare function buildTableOf8(faf: Record<string, unknown>, opts?: {
109
135
  goal?: string;
136
+ detailed?: SeededContextDetailed;
110
137
  }): TableOf8;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The faf_loop decision core — the brain of "100% or ask human".
3
+ *
4
+ * A pure, snapshot decision: given a parsed .faf, classify its remaining gaps
5
+ * as SOURCEABLE (detection can fill — language, stack: no human needed) or
6
+ * HUMAN (only the human knows — the goal + the 6Ws), and return a verdict:
7
+ *
8
+ * done — nothing left (or already 100%). Stop, success.
9
+ * can-source — sourceable gaps remain → the loop runs auto and re-scores.
10
+ * needs-human — ONLY human gaps remain → the wall; ask (or seed-and-confirm).
11
+ *
12
+ * The classification IS the human/sourced boundary at runtime: a slot is human
13
+ * iff its slot category is 'human' (the 6Ws) or it's the goal sentence. The
14
+ * loop sources everything it can FIRST (minimise human asks); only when the
15
+ * sourceable well is dry does it turn to the human. It never invents a human
16
+ * slot to reach 100% — needs-human is a legitimate, honest terminal, not a
17
+ * failure.
18
+ *
19
+ * This is the deterministic engine the CLI `faf loop` and the agentic
20
+ * `/faf-loop` skill both decide from. Orchestration (run auto, re-score, the
21
+ * no-progress + iteration-cap guards) lives in the command on top of this.
22
+ */
23
+ export type LoopStatus = 'done' | 'can-source' | 'needs-human';
24
+ export interface LoopGaps {
25
+ /** Empty active slots only the human can give — the goal + the 6Ws. */
26
+ human: string[];
27
+ /** Empty active slots detection can fill — language + the stack. */
28
+ sourceable: string[];
29
+ }
30
+ export interface LoopVerdict {
31
+ status: LoopStatus;
32
+ score: number;
33
+ gaps: LoopGaps;
34
+ /** The questions to put to the human — populated only when status is
35
+ * 'needs-human' (sourceable work is done; the human is all that's left). */
36
+ ask: Array<{
37
+ path: string;
38
+ question: string;
39
+ }>;
40
+ }
41
+ /** Default empty test: null/undefined, blank, or a placeholder token. The
42
+ * What-Not (`slotignored`) is handled separately — it is never a gap. */
43
+ export declare function isEmptyValue(value: unknown): boolean;
44
+ /** A path is HUMAN (only the human can give it) iff it is the goal sentence or
45
+ * its slot category is 'human' (the 6Ws). Everything else — name, language,
46
+ * the stack — is sourceable by detection. */
47
+ export declare function isHumanSlot(path: string): boolean;
48
+ /**
49
+ * Classify the empty, non-ignored interviewable slots of a .faf into human vs
50
+ * sourceable. Scope = the interviewable slots (the 6Ws + name + goal + the core
51
+ * stack); `slotignored` (the What-Not) is never a gap.
52
+ */
53
+ export declare function classifyGaps(data: Record<string, unknown>, isEmpty?: (value: unknown) => boolean): LoopGaps;
54
+ /**
55
+ * The loop verdict for a .faf snapshot. `score` is the current AI-readiness
56
+ * score (the loop's termination test). Precedence is deliberate: source
57
+ * everything possible BEFORE asking the human — so 'can-source' wins while any
58
+ * sourceable gap remains, and 'needs-human' fires only once they're exhausted.
59
+ */
60
+ export declare function loopVerdict(score: number, data: Record<string, unknown>, isEmpty?: (value: unknown) => boolean): LoopVerdict;
61
+ export type LoopRunStatus = 'done' | 'needs-human' | 'stuck' | 'capped' | 'no-faf';
62
+ export interface LoopDeps {
63
+ /** The current .faf as {data, yaml}, or null when none exists. */
64
+ read(): {
65
+ data: Record<string, unknown>;
66
+ yaml: string;
67
+ } | null;
68
+ /** Score raw .faf yaml → 0..100 (the termination test). */
69
+ score(yaml: string): number;
70
+ /** Source what detection can (auto): fill the stack and WRITE the .faf. */
71
+ runAuto(): void;
72
+ }
73
+ export interface LoopRunOptions {
74
+ /** Hard cap on auto rounds (default 5). */
75
+ maxRounds?: number;
76
+ isEmpty?: (value: unknown) => boolean;
77
+ }
78
+ export interface LoopRunResult {
79
+ status: LoopRunStatus;
80
+ score: number;
81
+ /** Auto rounds actually run. */
82
+ rounds: number;
83
+ /** Score after each read — the climb, for narration. */
84
+ history: number[];
85
+ /** The human questions to put — populated only when status is 'needs-human'. */
86
+ ask: Array<{
87
+ path: string;
88
+ question: string;
89
+ }>;
90
+ }
91
+ export declare function runLoop(deps: LoopDeps, opts?: LoopRunOptions): LoopRunResult;
@@ -19,4 +19,38 @@ export interface SeededContext {
19
19
  when?: string;
20
20
  how?: string;
21
21
  }
22
+ /**
23
+ * A sourced 6-W value WITH its provenance — where it came from and how much to
24
+ * trust it. The honesty gate: a fill the human can audit before confirming.
25
+ * - `source`: the artifact + locus it was relocated from, e.g.
26
+ * 'package.json:description' or 'README:## Why'. If we can't name a source,
27
+ * we don't emit a value — sourced-or-empty, never invented.
28
+ * - `confidence`: 0..1 by source quality (structured field > named section >
29
+ * heuristic match). A signal for the seeded/confirm UI, not a score.
30
+ */
31
+ export interface SourcedValue {
32
+ value: string;
33
+ source: string;
34
+ confidence: number;
35
+ }
36
+ export interface SeededContextDetailed {
37
+ who?: SourcedValue;
38
+ what?: SourcedValue;
39
+ why?: SourcedValue;
40
+ where?: SourcedValue;
41
+ when?: SourcedValue;
42
+ how?: SourcedValue;
43
+ }
44
+ /**
45
+ * The 6-W extractor WITH provenance — each slot carries {value, source,
46
+ * confidence}. This is the auditable form: the seeded/confirm UI can show the
47
+ * human WHERE a value was relocated from before they accept it.
48
+ */
49
+ export declare function relentlessContextDetailed(dir: string): SeededContextDetailed;
50
+ /**
51
+ * The bare 6-W extractor — values only, backward-compatible. A pure projection
52
+ * of `relentlessContextDetailed` (single internal source; identical output to
53
+ * the pre-provenance version). Existing consumers (claude-faf-mcp's faf_auto)
54
+ * keep working unchanged; opt into provenance via the Detailed form.
55
+ */
22
56
  export declare function relentlessContext(dir: string): SeededContext;
package/dist/index.d.ts CHANGED
@@ -8,13 +8,15 @@ export { findFafFile, readFaf, readFafRaw } from './interop/faf.js';
8
8
  export { generateProjectHtml, writeProjectHtml } from './interop/projecthtml.js';
9
9
  export { generateServerCard, writeServerCard, fafContextBlock, registryMeta, registryName, REGISTRY_PUBLISHER_KEY, } from './interop/servercard.js';
10
10
  export type { ServerCardOptions } from './interop/servercard.js';
11
- export type { InterviewQuestion, InterviewOption, GoalSeed, TableOf8, TableOf8Row, BoxStatus } from './core/interview.js';
11
+ export type { InterviewQuestion, InterviewOption, HumanSlotPath, SourcedSlotPath, GoalSeed, TableOf8, TableOf8Row, BoxStatus } from './core/interview.js';
12
12
  export { INTERVIEW, SIX_WS_INTERVIEW, STACK_INTERVIEW, INTERVIEW_BY_PATH, INTERVIEW_PATHS, INTERVIEW_VERSION, questionForSlot, interviewForMissing, seedSixWsFromGoal, buildTableOf8, } from './core/interview.js';
13
+ export type { LoopStatus, LoopGaps, LoopVerdict, LoopRunStatus, LoopDeps, LoopRunOptions, LoopRunResult } from './core/loop.js';
14
+ export { classifyGaps, loopVerdict, isHumanSlot, isEmptyValue, runLoop } from './core/loop.js';
13
15
  export { BENCH_VERSION, deriveQuestionSet, publicQuestions, gradeAnswers, buildReceipt, normalizeAnswer, answersMatch, ALIAS_GROUPS, } from './commands/bench.js';
14
16
  export type { BenchQuestion, QuestionSet, GradeResult, BenchState, RunRecord } from './commands/bench.js';
15
17
  export { turboCatScan, turboCatSlots } from './detect/turbo-cat.js';
16
18
  export type { TurboCatResult, DiscoveredFormat } from './detect/turbo-cat.js';
17
- export { relentlessContext } from './detect/relentless.js';
18
- export type { SeededContext } from './detect/relentless.js';
19
+ export { relentlessContext, relentlessContextDetailed } from './detect/relentless.js';
20
+ export type { SeededContext, SeededContextDetailed, SourcedValue } from './detect/relentless.js';
19
21
  export { assembleFreshFaf } from './detect/assemble.js';
20
22
  export * as kernel from './wasm/kernel.js';