@afokapu/atdd-flow 0.2.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.
@@ -0,0 +1,333 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { execFile as execute } from "node:child_process";
3
+ import { resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { choice, TypeSafeClient } from "@typesafe-ai/sdk";
6
+ import { type ModelCandidate, paths, readYaml, required } from "./core";
7
+ import { type Task } from "./tasks";
8
+
9
+ type ChoiceAnswer = { choice: string; confidence: number };
10
+ type JudgmentClient = { systemOne(request: unknown): Promise<{ model?: string; answers: Record<string, ChoiceAnswer> }> };
11
+ type Credential = () => Promise<string | undefined>;
12
+ type Unavailable = { available: false; reason: string };
13
+
14
+ export type ScoutResult = { path: string; relevance: "relevant" | "not_relevant"; confidence: number };
15
+ export type ScoutResponse = { available: true; model?: string; results: ScoutResult[] } | Unavailable;
16
+ export type FocusJudgment = "REQUIRED" | "USEFUL_BUT_NOT_REQUIRED" | "SPECULATIVE";
17
+ export type FocusResponse = { available: true; model?: string; judgment: FocusJudgment; confidence: number } | Unavailable;
18
+ export type ScoutInput = { goal: string; candidates: string[]; question?: string };
19
+ export type FocusInput = { title: string; body?: string; doneWhen: string[]; proposedAction: string };
20
+ export type ReviewClass = "LOCAL" | "ASSEMBLED" | "JOURNEY" | "SYSTEM";
21
+ export type ProofBoundary = "LOCAL_ACCEPTANCE" | "ASSEMBLED_API_RUNTIME" | "INTERLOCKING_ROUTE" | "TRAIN" | "USER_JOURNEY";
22
+ export type ReviewRoutingInput = {
23
+ title: string;
24
+ body?: string;
25
+ source?: string;
26
+ doneWhen: Array<{ text: string; proof?: string }>;
27
+ changedFiles?: string[];
28
+ planArtifacts?: string[];
29
+ criticalCategories?: string[];
30
+ };
31
+ export type ReviewRoutingSignals = {
32
+ behaviorEffect: "BEHAVIOR_AFFECTING" | "BEHAVIOR_PRESERVING";
33
+ proofBoundary: ProofBoundary;
34
+ crossesMultiplePaths: boolean;
35
+ consequence: "ORDINARY" | "HIGH_CONSEQUENCE";
36
+ reconciliation: "SINGLE_PATH" | "MULTI_PATH_SYSTEM";
37
+ runtimeObservation: "AVAILABLE" | "NOT_AVAILABLE";
38
+ };
39
+ export type ReviewRoutingResponse = {
40
+ available: true;
41
+ model?: string;
42
+ classification: ReviewClass;
43
+ confidence: number;
44
+ signals: ReviewRoutingSignals;
45
+ } | Unavailable;
46
+ export type ModelSelectionInput = {
47
+ seat: { address: string; role: string; purpose?: string };
48
+ tasks: Array<{
49
+ id: string;
50
+ title: string;
51
+ status: string;
52
+ body?: string;
53
+ doneWhen: string[];
54
+ blocker?: string;
55
+ }>;
56
+ candidates: ModelCandidate[];
57
+ reviewRouting?: {
58
+ classification: ReviewClass;
59
+ confidence: number;
60
+ signals: ReviewRoutingSignals;
61
+ };
62
+ };
63
+ export type ModelSelectionResponse = {
64
+ available: true;
65
+ model?: string;
66
+ selected_model: string;
67
+ confidence: number;
68
+ } | Unavailable;
69
+ export type JudgmentOptions = { client?: JudgmentClient; credential?: Credential };
70
+
71
+ const excerptLimit = 6_000;
72
+ const keychainService = "atdd-workflow.typesafe";
73
+ const execFile = promisify(execute);
74
+ const unavailable = (reason: string): Unavailable => ({ available: false, reason });
75
+
76
+ async function keychainCredential(): Promise<string | undefined> {
77
+ if (process.platform !== "darwin") return undefined;
78
+ const account = process.env.USER ?? process.env.LOGNAME;
79
+ if (!account) return undefined;
80
+ try {
81
+ const { stdout } = await execFile("/usr/bin/security", [
82
+ "find-generic-password", "-s", keychainService, "-a", account, "-w",
83
+ ], { encoding: "utf8", timeout: 1_000 });
84
+ return stdout.trim() || undefined;
85
+ } catch {
86
+ return undefined;
87
+ }
88
+ }
89
+
90
+ /** Resolve the optional Jev credential without writing it to workflow state. */
91
+ export async function resolveJevApiKey(options: JudgmentOptions = {}): Promise<string | undefined> {
92
+ return process.env.TYPESAFE_API_KEY?.trim() || await (options.credential ?? keychainCredential)();
93
+ }
94
+
95
+ async function client(options: JudgmentOptions) {
96
+ if (options.client) return options.client;
97
+ const apiKey = await resolveJevApiKey(options);
98
+ if (!apiKey) return undefined;
99
+ return new TypeSafeClient({ apiKey, logLevel: "off", timeout: 5_000, retry: { maxRetries: 0 } }) as unknown as JudgmentClient;
100
+ }
101
+
102
+ async function candidate(path: string) {
103
+ try {
104
+ return { path, excerpt: (await readFile(resolve(path), "utf8")).slice(0, excerptLimit) };
105
+ } catch {
106
+ return { path, excerpt: "" };
107
+ }
108
+ }
109
+
110
+ export async function scout(input: ScoutInput, options: JudgmentOptions = {}): Promise<ScoutResponse> {
111
+ if (!input.candidates.length) return unavailable("No candidate paths were supplied.");
112
+ const judge = await client(options);
113
+ if (!judge) return unavailable("Jev credentials are not configured; continue using repository evidence.");
114
+ const candidates = await Promise.all(input.candidates.map(candidate));
115
+ const questions = Object.fromEntries(candidates.map((entry, index) => [`candidate_${index}`, choice({
116
+ question: "Is this file relevant to the current implementation goal?",
117
+ goal: input.goal,
118
+ focus: input.question ?? "Identify files worth reading before implementation.",
119
+ candidate_path: entry.path,
120
+ }, {
121
+ relevant: "The file is likely needed to understand, implement, or verify the stated goal.",
122
+ not_relevant: "The file is unlikely to help with the stated goal at this time.",
123
+ })]));
124
+ try {
125
+ const response = await judge.systemOne({ state: { goal: input.goal, question: input.question ?? null, candidates }, questions });
126
+ return {
127
+ available: true,
128
+ ...(response.model ? { model: response.model } : {}),
129
+ results: candidates.map((entry, index) => {
130
+ const answer = response.answers[`candidate_${index}`];
131
+ return { path: entry.path, relevance: answer?.choice === "relevant" ? "relevant" : "not_relevant", confidence: answer?.confidence ?? 0 };
132
+ }),
133
+ };
134
+ } catch (error) {
135
+ return unavailable(`Scout unavailable: ${(error as Error).message}`);
136
+ }
137
+ }
138
+
139
+ export async function focusCheck(input: FocusInput, options: JudgmentOptions = {}): Promise<FocusResponse> {
140
+ const judge = await client(options);
141
+ if (!judge) return unavailable("Jev credentials are not configured; prefer the smaller reversible solution.");
142
+ const questions = {
143
+ scope: choice({
144
+ question: "How necessary is the proposed action for the current task?",
145
+ task_title: input.title,
146
+ task_body: input.body ?? null,
147
+ done_when: input.doneWhen,
148
+ proposed_action: input.proposedAction,
149
+ }, {
150
+ REQUIRED: "Necessary now to satisfy a done_when criterion, correct a verified failure, or resolve a verified blocker.",
151
+ USEFUL_BUT_NOT_REQUIRED: "Could improve the system but is not necessary for the current task; prefer the smaller solution.",
152
+ SPECULATIVE: "Future-proofing, unrelated cleanup, generalized abstraction, or other work outside the current task.",
153
+ }),
154
+ };
155
+ try {
156
+ const response = await judge.systemOne({ state: { task: input, proposed_action: input.proposedAction }, questions });
157
+ const answer = response.answers.scope;
158
+ const judgment = (["REQUIRED", "USEFUL_BUT_NOT_REQUIRED", "SPECULATIVE"] as const).includes(answer?.choice as FocusJudgment)
159
+ ? answer.choice as FocusJudgment
160
+ : "USEFUL_BUT_NOT_REQUIRED";
161
+ return { available: true, ...(response.model ? { model: response.model } : {}), judgment, confidence: answer?.confidence ?? 0 };
162
+ } catch (error) {
163
+ return unavailable(`Focus check unavailable: ${(error as Error).message}`);
164
+ }
165
+ }
166
+
167
+ export async function focusTask(root: string, project: string, taskId: string, proposedAction: string, options: JudgmentOptions = {}) {
168
+ const task = await readYaml<Task>(paths(root).taskFile(project, taskId));
169
+ if (task.schema !== "atdd-workflow/task/v1") throw new Error(`Unsupported task schema: ${taskId}`);
170
+ return focusCheck({
171
+ title: task.title,
172
+ ...(task.body ? { body: task.body } : {}),
173
+ doneWhen: task.done_when.map((item) => item.text),
174
+ proposedAction: required(proposedAction, "--action"),
175
+ }, options);
176
+ }
177
+
178
+
179
+ const reviewRoutingConfidenceFloor = 0.75;
180
+ const modelSelectionConfidenceFloor = 0.75;
181
+
182
+ export async function routeBehavioralReview(input: ReviewRoutingInput, options: JudgmentOptions = {}): Promise<ReviewRoutingResponse> {
183
+ const judge = await client(options);
184
+ if (!judge) return unavailable("Jev credentials are not configured; route final review conservatively as SYSTEM.");
185
+ const questions = {
186
+ classification: choice({
187
+ question: "What is the required complexity of the final behavioral reconciliation?",
188
+ task_title: input.title,
189
+ task_body: input.body ?? null,
190
+ source: input.source ?? null,
191
+ done_when: input.doneWhen,
192
+ changed_files: input.changedFiles ?? [],
193
+ plan_artifacts: input.planArtifacts ?? [],
194
+ critical_categories: input.criticalCategories ?? ["authorization", "destructive mutation", "persistence", "financial effect", "security"],
195
+ }, {
196
+ LOCAL: "One local acceptance or similarly narrow behavioral path is sufficient.",
197
+ ASSEMBLED: "The behavior must be reconciled at an assembled API/runtime or interlocking boundary.",
198
+ JOURNEY: "The user-visible or train/journey behavior must be exercised end to end.",
199
+ SYSTEM: "The delivery crosses multiple paths, boundaries, journeys, trains, or high-consequence system behavior and needs system-level reconciliation.",
200
+ }),
201
+ behavior_effect: choice({
202
+ question: "Is the delivery externally behavior-affecting or intended to preserve behavior?",
203
+ task_title: input.title,
204
+ task_body: input.body ?? null,
205
+ done_when: input.doneWhen.map((item) => item.text),
206
+ changed_files: input.changedFiles ?? [],
207
+ }, {
208
+ BEHAVIOR_AFFECTING: "The delivery intentionally changes externally observable behavior.",
209
+ BEHAVIOR_PRESERVING: "The delivery is intended to preserve externally observable behavior while changing structure or implementation.",
210
+ }),
211
+ proof_boundary: choice({
212
+ question: "What is the highest relevant executable proof boundary for this delivery?",
213
+ source: input.source ?? null,
214
+ done_when: input.doneWhen,
215
+ plan_artifacts: input.planArtifacts ?? [],
216
+ }, {
217
+ LOCAL_ACCEPTANCE: "A local acceptance/unit behavioral boundary is the highest relevant proof.",
218
+ ASSEMBLED_API_RUNTIME: "An assembled API or runtime boundary is required.",
219
+ INTERLOCKING_ROUTE: "An interlocking route is the highest relevant proof boundary.",
220
+ TRAIN: "A train-level boundary is required.",
221
+ USER_JOURNEY: "A user journey or equivalent end-to-end boundary is required.",
222
+ }),
223
+ cross_paths: choice({
224
+ question: "Does the delivery cross multiple wagons, trains, routes, journeys, or equivalent behavioral paths?",
225
+ plan_artifacts: input.planArtifacts ?? [],
226
+ changed_files: input.changedFiles ?? [],
227
+ }, {
228
+ SINGLE: "The delivery stays within one relevant behavioral path.",
229
+ MULTIPLE: "The delivery crosses multiple relevant behavioral paths or ownership boundaries.",
230
+ }),
231
+ consequence: choice({
232
+ question: "Does the delivery affect a high-consequence domain or configured critical category?",
233
+ task_title: input.title,
234
+ task_body: input.body ?? null,
235
+ changed_files: input.changedFiles ?? [],
236
+ critical_categories: input.criticalCategories ?? ["authorization", "destructive mutation", "persistence", "financial effect", "security"],
237
+ }, {
238
+ ORDINARY: "No high-consequence or configured critical category is materially involved.",
239
+ HIGH_CONSEQUENCE: "A high-consequence or configured critical category is materially involved.",
240
+ }),
241
+ reconciliation: choice({
242
+ question: "Is the final review primarily a single-path reconciliation or a multi-path/system reconciliation?",
243
+ plan_artifacts: input.planArtifacts ?? [],
244
+ changed_files: input.changedFiles ?? [],
245
+ }, {
246
+ SINGLE_PATH: "One behavioral path can be reconciled against intent.",
247
+ MULTI_PATH_SYSTEM: "Several paths or system interactions must be reconciled together.",
248
+ }),
249
+ runtime: choice({
250
+ question: "Is direct runtime observation available from the declared plan, harness, or executable proof?",
251
+ done_when: input.doneWhen,
252
+ plan_artifacts: input.planArtifacts ?? [],
253
+ }, {
254
+ AVAILABLE: "The declared plan or harness exposes a runtime boundary the reviewer can exercise directly.",
255
+ NOT_AVAILABLE: "No direct runtime observation is declared or available from the supplied context.",
256
+ }),
257
+ };
258
+ try {
259
+ const response = await judge.systemOne({ state: { task: input }, questions });
260
+ const classificationAnswer = response.answers.classification;
261
+ const behavior = response.answers.behavior_effect;
262
+ const boundary = response.answers.proof_boundary;
263
+ const crossPaths = response.answers.cross_paths;
264
+ const consequence = response.answers.consequence;
265
+ const reconciliation = response.answers.reconciliation;
266
+ const runtime = response.answers.runtime;
267
+ const classifications = ["LOCAL", "ASSEMBLED", "JOURNEY", "SYSTEM"] as const;
268
+ const boundaries = ["LOCAL_ACCEPTANCE", "ASSEMBLED_API_RUNTIME", "INTERLOCKING_ROUTE", "TRAIN", "USER_JOURNEY"] as const;
269
+ const confidence = Math.min(
270
+ classificationAnswer?.confidence ?? 0,
271
+ behavior?.confidence ?? 0,
272
+ boundary?.confidence ?? 0,
273
+ crossPaths?.confidence ?? 0,
274
+ consequence?.confidence ?? 0,
275
+ reconciliation?.confidence ?? 0,
276
+ runtime?.confidence ?? 0,
277
+ );
278
+ const rawClassification = classifications.includes(classificationAnswer?.choice as ReviewClass)
279
+ ? classificationAnswer!.choice as ReviewClass
280
+ : "SYSTEM";
281
+ const classification = confidence >= reviewRoutingConfidenceFloor ? rawClassification : "SYSTEM";
282
+ const signals: ReviewRoutingSignals = {
283
+ behaviorEffect: behavior?.choice === "BEHAVIOR_PRESERVING" ? "BEHAVIOR_PRESERVING" : "BEHAVIOR_AFFECTING",
284
+ proofBoundary: boundaries.includes(boundary?.choice as ProofBoundary) ? boundary!.choice as ProofBoundary : "USER_JOURNEY",
285
+ crossesMultiplePaths: crossPaths?.choice === "MULTIPLE",
286
+ consequence: consequence?.choice === "ORDINARY" ? "ORDINARY" : "HIGH_CONSEQUENCE",
287
+ reconciliation: reconciliation?.choice === "SINGLE_PATH" ? "SINGLE_PATH" : "MULTI_PATH_SYSTEM",
288
+ runtimeObservation: runtime?.choice === "AVAILABLE" ? "AVAILABLE" : "NOT_AVAILABLE",
289
+ };
290
+ return { available: true, ...(response.model ? { model: response.model } : {}), classification, confidence, signals };
291
+ } catch (error) {
292
+ return unavailable(`Behavioral review routing unavailable: ${(error as Error).message}`);
293
+ }
294
+ }
295
+
296
+ export async function selectModel(input: ModelSelectionInput, options: JudgmentOptions = {}): Promise<ModelSelectionResponse> {
297
+ const candidates = input.candidates.filter((entry) => entry.enabled !== false);
298
+ if (!candidates.length) return unavailable("No enabled model candidates were supplied.");
299
+ if (candidates.length === 1) return { available: true, selected_model: candidates[0]!.id, confidence: 1 };
300
+ const judge = await client(options);
301
+ if (!judge) return unavailable("Jev credentials are not configured; use the strongest available model.");
302
+ const criteria = Object.fromEntries(candidates.map((entry, index) => [entry.id,
303
+ `Rank ${index + 1} of ${candidates.length}, ordered strongest to weakest. Select this only when it is the weakest listed model that can reliably complete the current responsibility.${entry.description ? ` ${entry.description}` : ""}`,
304
+ ]));
305
+ const questions = {
306
+ model: choice({
307
+ question: "Which listed model is the weakest candidate sufficient to complete the seat's current work reliably?",
308
+ policy: "Prefer later/weaker candidates whenever they are sufficient. Use review routing complexity, cross-system scope, high-consequence flags, and residual uncertainty to justify stronger candidates; do not treat role names as capability requirements.",
309
+ seat: input.seat,
310
+ tasks: input.tasks,
311
+ review_routing: input.reviewRouting ?? null,
312
+ candidates: candidates.map((entry, index) => ({ id: entry.id, rank: index + 1, description: entry.description ?? null })),
313
+ }, criteria),
314
+ };
315
+ try {
316
+ const response = await judge.systemOne({ state: input, questions });
317
+ const answer = response.answers.model;
318
+ if (!answer || !candidates.some((entry) => entry.id === answer.choice)) {
319
+ return unavailable("Jev returned a model outside the configured portfolio; use the strongest available model.");
320
+ }
321
+ if ((answer.confidence ?? 0) < modelSelectionConfidenceFloor) {
322
+ return unavailable("Jev model selection confidence is low; use the strongest available model.");
323
+ }
324
+ return {
325
+ available: true,
326
+ ...(response.model ? { model: response.model } : {}),
327
+ selected_model: answer.choice,
328
+ confidence: answer.confidence ?? 0,
329
+ };
330
+ } catch (error) {
331
+ return unavailable(`Model selection unavailable: ${(error as Error).message}`);
332
+ }
333
+ }