@engineereddev/fractal-planner 0.1.1
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.
- package/.claude-plugin/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +19 -0
- package/LICENSE +21 -0
- package/README.md +257 -0
- package/agents/fp-analyst.md +96 -0
- package/agents/fp-context-builder.md +87 -0
- package/agents/fp-critic.md +140 -0
- package/agents/fp-decomposer.md +261 -0
- package/agents/fp-interviewer.md +263 -0
- package/agents/fp-linear-sync.md +128 -0
- package/agents/fp-researcher.md +82 -0
- package/agents/fp-task-tracker.md +134 -0
- package/dist/cli/classify-intent.js +118 -0
- package/dist/cli/compute-signals.js +495 -0
- package/dist/cli/generate-plan.js +14209 -0
- package/dist/cli/load-config.js +13661 -0
- package/dist/cli/validate-tasks.js +467 -0
- package/dist/index.js +24598 -0
- package/dist/src/cli/classify-intent.d.ts +3 -0
- package/dist/src/cli/compute-signals.d.ts +14 -0
- package/dist/src/cli/generate-plan.d.ts +3 -0
- package/dist/src/cli/load-config.d.ts +3 -0
- package/dist/src/cli/validate-tasks.d.ts +3 -0
- package/dist/src/config.d.ts +182 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/phases/clearance.d.ts +12 -0
- package/dist/src/phases/decomposition.d.ts +41 -0
- package/dist/src/phases/interview.d.ts +17 -0
- package/dist/src/phases/planning.d.ts +21 -0
- package/dist/src/phases/research.d.ts +9 -0
- package/dist/src/types/index.d.ts +116 -0
- package/dist/src/utils/draft.d.ts +21 -0
- package/dist/src/utils/question-strategies.d.ts +24 -0
- package/dist/src/utils/task-parser.d.ts +3 -0
- package/hooks/hooks.json +27 -0
- package/hooks/nudge-teammate.sh +216 -0
- package/hooks/run-comment-checker.sh +91 -0
- package/package.json +65 -0
- package/skills/commit/SKILL.md +157 -0
- package/skills/fp/SKILL.md +857 -0
- package/skills/fp/scripts/resolve-env.sh +66 -0
- package/skills/handoff/SKILL.md +195 -0
- package/skills/implement/SKILL.md +783 -0
- package/skills/implement/reference.md +935 -0
- package/skills/retry/SKILL.md +333 -0
- package/skills/status/SKILL.md +182 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
// src/cli/compute-signals.ts
|
|
16
|
+
import { readFile, writeFile } from "fs/promises";
|
|
17
|
+
import { join, dirname } from "path";
|
|
18
|
+
import { execSync } from "child_process";
|
|
19
|
+
|
|
20
|
+
// src/utils/task-parser.ts
|
|
21
|
+
var TASK_LINE_RE = /^(\s*)-\s*\[ID:\s*([^\]]+)\]\s*(.+?)\s*\(Complexity:\s*(\d+)\)\s*$/;
|
|
22
|
+
var HEADING_TASK_RE = /^(#{1,6})\s*\[([^\]]+)\]\s*(.+?)\s*$/;
|
|
23
|
+
var ACCEPTANCE_RE = /^\s*-\s*Acceptance(?:\s+Criteria)?:\s*(.*)$/;
|
|
24
|
+
var COMPLEXITY_RE = /^\s*-\s*Complexity:\s*(\d+)\s*$/;
|
|
25
|
+
var DESCRIPTION_RE = /^\s*-\s*Description:\s*/;
|
|
26
|
+
var DEPENDENCIES_RE = /^\s*-\s*Dependencies:\s*(.+)$/;
|
|
27
|
+
var FILES_RE = /^\s*-\s*Files:\s*(.+)$/;
|
|
28
|
+
var TESTS_REQUIRED_RE = /^\s*-\s*Tests Required:\s*(.+)$/;
|
|
29
|
+
var HINTS_RE = /^\s*-\s*Hints:\s*(.*)$/;
|
|
30
|
+
var REFERENCES_RE = /^\s*-\s*References:\s*(.*)$/;
|
|
31
|
+
var GUARDRAILS_RE = /^\s*-\s*Guardrails:\s*(.*)$/;
|
|
32
|
+
var TEST_COMMANDS_RE = /^\s*-\s*Test Commands:\s*(.+)$/;
|
|
33
|
+
var DIMENSIONS_RE = /^\s*-\s*Complexity Dimensions:\s*(.+)$/;
|
|
34
|
+
var BLOCK_ITEM_RE = /^\s*-\s+(.+)$/;
|
|
35
|
+
function parseTaskLine(line) {
|
|
36
|
+
const match = line.match(TASK_LINE_RE);
|
|
37
|
+
if (match) {
|
|
38
|
+
return {
|
|
39
|
+
indent: match[1].length,
|
|
40
|
+
id: match[2].trim(),
|
|
41
|
+
description: match[3].trim(),
|
|
42
|
+
complexity: parseInt(match[4], 10),
|
|
43
|
+
fromHeading: false
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const headingMatch = line.match(HEADING_TASK_RE);
|
|
47
|
+
if (headingMatch) {
|
|
48
|
+
const headingLevel = headingMatch[1].length;
|
|
49
|
+
return {
|
|
50
|
+
indent: (headingLevel - 1) * 2,
|
|
51
|
+
id: headingMatch[2].trim(),
|
|
52
|
+
description: headingMatch[3].trim(),
|
|
53
|
+
complexity: -1,
|
|
54
|
+
fromHeading: true
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function parseAcceptance(raw) {
|
|
60
|
+
return raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
function parseDependencies(raw) {
|
|
63
|
+
const trimmed = raw.trim().toLowerCase();
|
|
64
|
+
if (trimmed === "none" || trimmed === "")
|
|
65
|
+
return [];
|
|
66
|
+
return raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
function parseFiles(raw) {
|
|
69
|
+
const trimmed = raw.trim().toLowerCase();
|
|
70
|
+
if (trimmed === "none" || trimmed === "")
|
|
71
|
+
return [];
|
|
72
|
+
return raw.split(/,\s*/).map((s) => s.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
function parseBlockItem(line) {
|
|
75
|
+
const match = line.match(BLOCK_ITEM_RE);
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
return match[1].trim();
|
|
79
|
+
}
|
|
80
|
+
function collectMetadata(lines, startIdx) {
|
|
81
|
+
const meta = {
|
|
82
|
+
acceptance: [],
|
|
83
|
+
dependencies: [],
|
|
84
|
+
files: [],
|
|
85
|
+
testsRequired: false,
|
|
86
|
+
hints: [],
|
|
87
|
+
references: [],
|
|
88
|
+
guardrails: [],
|
|
89
|
+
testCommands: []
|
|
90
|
+
};
|
|
91
|
+
let currentBlock = null;
|
|
92
|
+
for (let i = startIdx;i < lines.length; i++) {
|
|
93
|
+
const line = lines[i];
|
|
94
|
+
if (TASK_LINE_RE.test(line) || HEADING_TASK_RE.test(line))
|
|
95
|
+
break;
|
|
96
|
+
const accMatch = line.match(ACCEPTANCE_RE);
|
|
97
|
+
if (accMatch) {
|
|
98
|
+
currentBlock = null;
|
|
99
|
+
const content = accMatch[1].trim();
|
|
100
|
+
if (content) {
|
|
101
|
+
meta.acceptance = parseAcceptance(content);
|
|
102
|
+
} else {
|
|
103
|
+
currentBlock = "acceptance";
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const complexityMatch = line.match(COMPLEXITY_RE);
|
|
108
|
+
if (complexityMatch) {
|
|
109
|
+
currentBlock = null;
|
|
110
|
+
meta.complexity = parseInt(complexityMatch[1], 10);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (DESCRIPTION_RE.test(line)) {
|
|
114
|
+
currentBlock = null;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const depMatch = line.match(DEPENDENCIES_RE);
|
|
118
|
+
if (depMatch) {
|
|
119
|
+
currentBlock = null;
|
|
120
|
+
meta.dependencies = parseDependencies(depMatch[1]);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const filesMatch = line.match(FILES_RE);
|
|
124
|
+
if (filesMatch) {
|
|
125
|
+
currentBlock = null;
|
|
126
|
+
meta.files = parseFiles(filesMatch[1]);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const testsMatch = line.match(TESTS_REQUIRED_RE);
|
|
130
|
+
if (testsMatch) {
|
|
131
|
+
currentBlock = null;
|
|
132
|
+
meta.testsRequired = testsMatch[1].trim().toLowerCase() === "yes";
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const hintsMatch = line.match(HINTS_RE);
|
|
136
|
+
if (hintsMatch) {
|
|
137
|
+
currentBlock = null;
|
|
138
|
+
const content = hintsMatch[1].trim();
|
|
139
|
+
if (content) {
|
|
140
|
+
meta.hints = [content];
|
|
141
|
+
} else {
|
|
142
|
+
currentBlock = "hints";
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const refsMatch = line.match(REFERENCES_RE);
|
|
147
|
+
if (refsMatch) {
|
|
148
|
+
currentBlock = null;
|
|
149
|
+
const content = refsMatch[1].trim();
|
|
150
|
+
if (content) {
|
|
151
|
+
meta.references = [content];
|
|
152
|
+
} else {
|
|
153
|
+
currentBlock = "references";
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const guardrailsMatch = line.match(GUARDRAILS_RE);
|
|
158
|
+
if (guardrailsMatch) {
|
|
159
|
+
currentBlock = null;
|
|
160
|
+
const content = guardrailsMatch[1].trim();
|
|
161
|
+
if (content) {
|
|
162
|
+
meta.guardrails = [content];
|
|
163
|
+
} else {
|
|
164
|
+
currentBlock = "guardrails";
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const testCmdsMatch = line.match(TEST_COMMANDS_RE);
|
|
169
|
+
if (testCmdsMatch) {
|
|
170
|
+
currentBlock = null;
|
|
171
|
+
meta.testCommands = testCmdsMatch[1].trim().split(/;\s*/).map((s) => s.trim()).filter(Boolean);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const dimensionsMatch = line.match(DIMENSIONS_RE);
|
|
175
|
+
if (dimensionsMatch) {
|
|
176
|
+
currentBlock = null;
|
|
177
|
+
const dims = {};
|
|
178
|
+
for (const pair of dimensionsMatch[1].split(/,\s*/)) {
|
|
179
|
+
const eqIdx = pair.indexOf("=");
|
|
180
|
+
if (eqIdx > 0) {
|
|
181
|
+
const key = pair.slice(0, eqIdx).trim();
|
|
182
|
+
const val = parseInt(pair.slice(eqIdx + 1).trim(), 10);
|
|
183
|
+
if (key && !isNaN(val))
|
|
184
|
+
dims[key] = val;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (Object.keys(dims).length > 0)
|
|
188
|
+
meta.dimensions = dims;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (currentBlock) {
|
|
192
|
+
const item = parseBlockItem(line);
|
|
193
|
+
if (item) {
|
|
194
|
+
meta[currentBlock].push(item);
|
|
195
|
+
} else if (line.trim() === "") {
|
|
196
|
+
currentBlock = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return meta;
|
|
201
|
+
}
|
|
202
|
+
function parseTasksMarkdown(markdown) {
|
|
203
|
+
const lines = markdown.split(`
|
|
204
|
+
`);
|
|
205
|
+
const flatTasks = [];
|
|
206
|
+
for (let i = 0;i < lines.length; i++) {
|
|
207
|
+
const parsed = parseTaskLine(lines[i]);
|
|
208
|
+
if (!parsed)
|
|
209
|
+
continue;
|
|
210
|
+
const meta = collectMetadata(lines, i + 1);
|
|
211
|
+
flatTasks.push({
|
|
212
|
+
...parsed,
|
|
213
|
+
metadata: meta,
|
|
214
|
+
lineIndex: i
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (flatTasks.length === 0) {
|
|
218
|
+
throw new Error("No tasks found in markdown");
|
|
219
|
+
}
|
|
220
|
+
for (const ft of flatTasks) {
|
|
221
|
+
if (ft.complexity === -1) {
|
|
222
|
+
ft.complexity = ft.metadata.complexity ?? 0;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const hasHeadingFormat = flatTasks.some((ft) => ft.fromHeading);
|
|
226
|
+
const minIndent = Math.min(...flatTasks.map((ft) => ft.indent));
|
|
227
|
+
const topLevel = flatTasks.filter((ft) => ft.indent === minIndent);
|
|
228
|
+
if (hasHeadingFormat && topLevel.length > 1) {
|
|
229
|
+
const maxComplexity = Math.max(...topLevel.map((ft) => ft.complexity));
|
|
230
|
+
flatTasks.unshift({
|
|
231
|
+
indent: minIndent - 2,
|
|
232
|
+
id: "root",
|
|
233
|
+
description: topLevel.map((ft) => ft.description).join(", "),
|
|
234
|
+
complexity: maxComplexity,
|
|
235
|
+
fromHeading: true,
|
|
236
|
+
metadata: {
|
|
237
|
+
acceptance: [],
|
|
238
|
+
dependencies: [],
|
|
239
|
+
files: [],
|
|
240
|
+
testsRequired: false,
|
|
241
|
+
hints: [],
|
|
242
|
+
references: [],
|
|
243
|
+
guardrails: [],
|
|
244
|
+
testCommands: []
|
|
245
|
+
},
|
|
246
|
+
lineIndex: -1
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const toTask = (ft) => {
|
|
250
|
+
const metadata = {
|
|
251
|
+
filesToModify: ft.metadata.files,
|
|
252
|
+
testsRequired: ft.metadata.testsRequired
|
|
253
|
+
};
|
|
254
|
+
if (ft.metadata.hints.length > 0)
|
|
255
|
+
metadata.hints = ft.metadata.hints;
|
|
256
|
+
if (ft.metadata.references.length > 0)
|
|
257
|
+
metadata.references = ft.metadata.references;
|
|
258
|
+
if (ft.metadata.guardrails.length > 0)
|
|
259
|
+
metadata.guardrails = ft.metadata.guardrails;
|
|
260
|
+
if (ft.metadata.testCommands.length > 0)
|
|
261
|
+
metadata.testCommands = ft.metadata.testCommands;
|
|
262
|
+
const task = {
|
|
263
|
+
id: ft.id,
|
|
264
|
+
description: ft.description,
|
|
265
|
+
estimatedComplexity: ft.complexity,
|
|
266
|
+
acceptanceCriteria: ft.metadata.acceptance,
|
|
267
|
+
dependencies: ft.metadata.dependencies,
|
|
268
|
+
status: "pending",
|
|
269
|
+
metadata
|
|
270
|
+
};
|
|
271
|
+
if (ft.metadata.dimensions) {
|
|
272
|
+
const d = ft.metadata.dimensions;
|
|
273
|
+
task.complexityDimensions = {
|
|
274
|
+
scope: d.scope ?? 0,
|
|
275
|
+
risk: d.risk ?? 0,
|
|
276
|
+
novelty: d.novelty ?? 0,
|
|
277
|
+
integration: d.integration ?? 0,
|
|
278
|
+
testing: d.testing ?? 0
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return task;
|
|
282
|
+
};
|
|
283
|
+
return buildTree(flatTasks, toTask);
|
|
284
|
+
}
|
|
285
|
+
function buildTree(flatTasks, toTask) {
|
|
286
|
+
if (flatTasks.length === 1) {
|
|
287
|
+
return toTask(flatTasks[0]);
|
|
288
|
+
}
|
|
289
|
+
const root = toTask(flatTasks[0]);
|
|
290
|
+
const rootIndent = flatTasks[0].indent;
|
|
291
|
+
const stack = [{ task: root, indent: rootIndent }];
|
|
292
|
+
for (let i = 1;i < flatTasks.length; i++) {
|
|
293
|
+
const ft = flatTasks[i];
|
|
294
|
+
const task = toTask(ft);
|
|
295
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= ft.indent) {
|
|
296
|
+
stack.pop();
|
|
297
|
+
}
|
|
298
|
+
const parent = stack[stack.length - 1].task;
|
|
299
|
+
if (!parent.subtasks)
|
|
300
|
+
parent.subtasks = [];
|
|
301
|
+
parent.subtasks.push(task);
|
|
302
|
+
stack.push({ task, indent: ft.indent });
|
|
303
|
+
}
|
|
304
|
+
return root;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/cli/compute-signals.ts
|
|
308
|
+
function clamp(val, min, max) {
|
|
309
|
+
return Math.max(min, Math.min(max, val));
|
|
310
|
+
}
|
|
311
|
+
function exec(cmd) {
|
|
312
|
+
try {
|
|
313
|
+
return execSync(cmd, { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
314
|
+
} catch {
|
|
315
|
+
return "";
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function computeFileScope(files) {
|
|
319
|
+
if (files.length === 0)
|
|
320
|
+
return 1;
|
|
321
|
+
let totalLines = 0;
|
|
322
|
+
for (const file of files) {
|
|
323
|
+
const output = exec(`wc -l < "${file}" 2>/dev/null`);
|
|
324
|
+
totalLines += parseInt(output, 10) || 0;
|
|
325
|
+
}
|
|
326
|
+
const fileCount = files.length;
|
|
327
|
+
if (fileCount <= 1 && totalLines < 100)
|
|
328
|
+
return 1;
|
|
329
|
+
if (fileCount <= 2 && totalLines < 300)
|
|
330
|
+
return 2;
|
|
331
|
+
if (fileCount <= 3 && totalLines < 600)
|
|
332
|
+
return 3;
|
|
333
|
+
if (fileCount <= 4 && totalLines < 1000)
|
|
334
|
+
return 4;
|
|
335
|
+
return 5;
|
|
336
|
+
}
|
|
337
|
+
function computeCoupling(files) {
|
|
338
|
+
if (files.length === 0)
|
|
339
|
+
return 1;
|
|
340
|
+
let totalConnections = 0;
|
|
341
|
+
for (const file of files) {
|
|
342
|
+
const basename = file.replace(/\.(ts|js|tsx|jsx)$/, "").replace(/^.*\//, "");
|
|
343
|
+
const importPattern = `from.*['"].*${basename}['"]`;
|
|
344
|
+
const output = exec(`grep -r -l "${importPattern}" --include="*.ts" --include="*.tsx" --include="*.js" . 2>/dev/null`);
|
|
345
|
+
const fanIn = output ? output.split(`
|
|
346
|
+
`).filter(Boolean).length : 0;
|
|
347
|
+
const fileContent = exec(`cat "${file}" 2>/dev/null`);
|
|
348
|
+
const imports = fileContent.match(/from\s+['"][^'"]+['"]/g);
|
|
349
|
+
const fanOut = imports ? imports.length : 0;
|
|
350
|
+
totalConnections += fanIn + fanOut;
|
|
351
|
+
}
|
|
352
|
+
const avg = totalConnections / files.length;
|
|
353
|
+
if (avg <= 2)
|
|
354
|
+
return 1;
|
|
355
|
+
if (avg <= 5)
|
|
356
|
+
return 2;
|
|
357
|
+
if (avg <= 10)
|
|
358
|
+
return 3;
|
|
359
|
+
if (avg <= 20)
|
|
360
|
+
return 4;
|
|
361
|
+
return 5;
|
|
362
|
+
}
|
|
363
|
+
function computeGitRisk(files) {
|
|
364
|
+
if (files.length === 0)
|
|
365
|
+
return 1;
|
|
366
|
+
let totalChurn = 0;
|
|
367
|
+
let totalAuthors = 0;
|
|
368
|
+
let totalBugFixes = 0;
|
|
369
|
+
let existingFiles = 0;
|
|
370
|
+
for (const file of files) {
|
|
371
|
+
const churnOutput = exec(`git log --oneline --since="6 months ago" -- "${file}" 2>/dev/null`);
|
|
372
|
+
const churn = churnOutput ? churnOutput.split(`
|
|
373
|
+
`).filter(Boolean).length : 0;
|
|
374
|
+
if (churn === 0)
|
|
375
|
+
continue;
|
|
376
|
+
existingFiles++;
|
|
377
|
+
totalChurn += churn;
|
|
378
|
+
const authorOutput = exec(`git log --format="%aN" -- "${file}" 2>/dev/null | sort -u`);
|
|
379
|
+
totalAuthors += authorOutput ? authorOutput.split(`
|
|
380
|
+
`).filter(Boolean).length : 0;
|
|
381
|
+
const bugOutput = exec(`git log --oneline --grep="fix\\|bug" -- "${file}" 2>/dev/null`);
|
|
382
|
+
totalBugFixes += bugOutput ? bugOutput.split(`
|
|
383
|
+
`).filter(Boolean).length : 0;
|
|
384
|
+
}
|
|
385
|
+
if (existingFiles === 0)
|
|
386
|
+
return 1;
|
|
387
|
+
const avgChurn = totalChurn / existingFiles;
|
|
388
|
+
const avgAuthors = totalAuthors / existingFiles;
|
|
389
|
+
const avgBugs = totalBugFixes / existingFiles;
|
|
390
|
+
const score = avgChurn / 10 + avgAuthors / 3 + avgBugs / 2;
|
|
391
|
+
if (score < 1)
|
|
392
|
+
return 1;
|
|
393
|
+
if (score < 2)
|
|
394
|
+
return 2;
|
|
395
|
+
if (score < 4)
|
|
396
|
+
return 3;
|
|
397
|
+
if (score < 7)
|
|
398
|
+
return 4;
|
|
399
|
+
return 5;
|
|
400
|
+
}
|
|
401
|
+
function computeTestCoverage(files, testsRequired) {
|
|
402
|
+
if (files.length === 0)
|
|
403
|
+
return 1;
|
|
404
|
+
let coveredFiles = 0;
|
|
405
|
+
for (const file of files) {
|
|
406
|
+
const base = file.replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
407
|
+
const dir = dirname(file);
|
|
408
|
+
const testPatterns = [
|
|
409
|
+
`${base}.test.ts`,
|
|
410
|
+
`${base}.test.tsx`,
|
|
411
|
+
`${base}.test.js`,
|
|
412
|
+
`${base}.spec.ts`,
|
|
413
|
+
`${base}.spec.js`,
|
|
414
|
+
join(dir, "__tests__", `${file.replace(/^.*\//, "").replace(/\.(ts|js|tsx|jsx)$/, "")}.test.ts`),
|
|
415
|
+
join(dir, "__tests__", `${file.replace(/^.*\//, "").replace(/\.(ts|js|tsx|jsx)$/, "")}.test.js`)
|
|
416
|
+
];
|
|
417
|
+
for (const pattern of testPatterns) {
|
|
418
|
+
const s = exec(`test -f "${pattern}" && echo "exists"`);
|
|
419
|
+
if (s === "exists") {
|
|
420
|
+
coveredFiles++;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const coverage = coveredFiles / files.length;
|
|
426
|
+
if (coverage >= 0.8)
|
|
427
|
+
return 1;
|
|
428
|
+
if (coverage >= 0.5)
|
|
429
|
+
return 2;
|
|
430
|
+
if (coverage >= 0.2)
|
|
431
|
+
return 3;
|
|
432
|
+
if (!testsRequired)
|
|
433
|
+
return 3;
|
|
434
|
+
return testsRequired && coverage === 0 ? 5 : 4;
|
|
435
|
+
}
|
|
436
|
+
function computeComposite(signals) {
|
|
437
|
+
const weighted = (signals.fileScope + signals.coupling * 1.5 + signals.gitRisk * 1.25 + signals.testCoverage * 0.75) / 4.5;
|
|
438
|
+
return clamp(Math.round(weighted), 1, 5);
|
|
439
|
+
}
|
|
440
|
+
function collectLeafTasks(task) {
|
|
441
|
+
if (!task.subtasks || task.subtasks.length === 0)
|
|
442
|
+
return [task];
|
|
443
|
+
return task.subtasks.flatMap(collectLeafTasks);
|
|
444
|
+
}
|
|
445
|
+
async function main() {
|
|
446
|
+
const planId = process.argv[2];
|
|
447
|
+
if (!planId) {
|
|
448
|
+
console.error("Usage: compute-signals.ts <planId>");
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
const plansDir = join(process.cwd(), ".fractal-planner", "plans", planId);
|
|
452
|
+
const tasksPath = join(plansDir, "tasks.md");
|
|
453
|
+
let tasksMarkdown;
|
|
454
|
+
try {
|
|
455
|
+
tasksMarkdown = await readFile(tasksPath, "utf-8");
|
|
456
|
+
} catch {
|
|
457
|
+
console.error(`Cannot read ${tasksPath}`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
const rootTask = parseTasksMarkdown(tasksMarkdown);
|
|
461
|
+
const leafTasks = collectLeafTasks(rootTask);
|
|
462
|
+
const signals = {};
|
|
463
|
+
for (const task of leafTasks) {
|
|
464
|
+
const files = task.metadata?.filesToModify ?? [];
|
|
465
|
+
const testsRequired = task.metadata?.testsRequired ?? false;
|
|
466
|
+
const fileScope = computeFileScope(files);
|
|
467
|
+
const coupling = computeCoupling(files);
|
|
468
|
+
const gitRisk = computeGitRisk(files);
|
|
469
|
+
const testCoverage = computeTestCoverage(files, testsRequired);
|
|
470
|
+
const composite = computeComposite({ fileScope, coupling, gitRisk, testCoverage });
|
|
471
|
+
signals[task.id] = { fileScope, coupling, gitRisk, testCoverage, composite };
|
|
472
|
+
}
|
|
473
|
+
const signalsPath = join(plansDir, "signals.json");
|
|
474
|
+
await writeFile(signalsPath, JSON.stringify(signals, null, 2), "utf-8");
|
|
475
|
+
console.log(JSON.stringify({
|
|
476
|
+
planId,
|
|
477
|
+
taskCount: leafTasks.length,
|
|
478
|
+
signals,
|
|
479
|
+
signalsFile: signalsPath
|
|
480
|
+
}));
|
|
481
|
+
}
|
|
482
|
+
var isMainModule = typeof Bun !== "undefined" ? Bun.main === import.meta.path : __require.main == __require.module;
|
|
483
|
+
if (isMainModule) {
|
|
484
|
+
main().catch((err) => {
|
|
485
|
+
console.error(err);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
export {
|
|
490
|
+
computeTestCoverage,
|
|
491
|
+
computeGitRisk,
|
|
492
|
+
computeFileScope,
|
|
493
|
+
computeCoupling,
|
|
494
|
+
computeComposite
|
|
495
|
+
};
|