brain-dev 0.1.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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/agents/brain-checker.md +33 -0
- package/agents/brain-debugger.md +35 -0
- package/agents/brain-executor.md +37 -0
- package/agents/brain-mapper.md +44 -0
- package/agents/brain-planner.md +49 -0
- package/agents/brain-researcher.md +47 -0
- package/agents/brain-synthesizer.md +43 -0
- package/agents/brain-verifier.md +41 -0
- package/bin/brain-tools.cjs +185 -0
- package/bin/lib/adr.cjs +283 -0
- package/bin/lib/agents.cjs +152 -0
- package/bin/lib/anti-patterns.cjs +183 -0
- package/bin/lib/audit.cjs +268 -0
- package/bin/lib/commands/adr.cjs +126 -0
- package/bin/lib/commands/complete.cjs +270 -0
- package/bin/lib/commands/config.cjs +306 -0
- package/bin/lib/commands/discuss.cjs +237 -0
- package/bin/lib/commands/execute.cjs +415 -0
- package/bin/lib/commands/health.cjs +103 -0
- package/bin/lib/commands/map.cjs +101 -0
- package/bin/lib/commands/new-project.cjs +885 -0
- package/bin/lib/commands/pause.cjs +142 -0
- package/bin/lib/commands/phase-manage.cjs +357 -0
- package/bin/lib/commands/plan.cjs +451 -0
- package/bin/lib/commands/progress.cjs +167 -0
- package/bin/lib/commands/quick.cjs +447 -0
- package/bin/lib/commands/resume.cjs +196 -0
- package/bin/lib/commands/storm.cjs +590 -0
- package/bin/lib/commands/verify.cjs +504 -0
- package/bin/lib/commands.cjs +263 -0
- package/bin/lib/complexity.cjs +138 -0
- package/bin/lib/complexity.test.cjs +108 -0
- package/bin/lib/config.cjs +452 -0
- package/bin/lib/core.cjs +62 -0
- package/bin/lib/detect.cjs +603 -0
- package/bin/lib/git.cjs +112 -0
- package/bin/lib/health.cjs +356 -0
- package/bin/lib/init.cjs +310 -0
- package/bin/lib/logger.cjs +100 -0
- package/bin/lib/platform.cjs +58 -0
- package/bin/lib/requirements.cjs +158 -0
- package/bin/lib/roadmap.cjs +228 -0
- package/bin/lib/security.cjs +237 -0
- package/bin/lib/state.cjs +353 -0
- package/bin/lib/templates.cjs +48 -0
- package/bin/templates/advocate.md +182 -0
- package/bin/templates/checkpoint.md +55 -0
- package/bin/templates/debugger.md +148 -0
- package/bin/templates/discuss.md +60 -0
- package/bin/templates/executor.md +201 -0
- package/bin/templates/mapper.md +129 -0
- package/bin/templates/plan-checker.md +134 -0
- package/bin/templates/planner.md +165 -0
- package/bin/templates/researcher.md +78 -0
- package/bin/templates/storm.html +376 -0
- package/bin/templates/synthesis.md +30 -0
- package/bin/templates/verifier.md +181 -0
- package/commands/brain/adr.md +34 -0
- package/commands/brain/complete.md +37 -0
- package/commands/brain/config.md +37 -0
- package/commands/brain/discuss.md +35 -0
- package/commands/brain/execute.md +38 -0
- package/commands/brain/health.md +33 -0
- package/commands/brain/map.md +35 -0
- package/commands/brain/new-project.md +38 -0
- package/commands/brain/pause.md +26 -0
- package/commands/brain/plan.md +38 -0
- package/commands/brain/progress.md +28 -0
- package/commands/brain/quick.md +51 -0
- package/commands/brain/resume.md +28 -0
- package/commands/brain/storm.md +30 -0
- package/commands/brain/verify.md +39 -0
- package/hooks/bootstrap.sh +54 -0
- package/hooks/post-tool-use.sh +45 -0
- package/hooks/statusline.sh +130 -0
- package/package.json +36 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { parseRequirements } = require('./requirements.cjs');
|
|
6
|
+
const { atomicWriteSync } = require('./state.cjs');
|
|
7
|
+
const { parseRoadmap } = require('./roadmap.cjs');
|
|
8
|
+
const { output, error } = require('./core.cjs');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a requirement matrix Map for the given scoped requirement IDs.
|
|
12
|
+
* Each entry: { required: true, verified: false, built: false }
|
|
13
|
+
* @param {string[]} scopedReqIds
|
|
14
|
+
* @returns {Map<string, {required: boolean, verified: boolean, built: boolean}>}
|
|
15
|
+
*/
|
|
16
|
+
function buildRequirementMatrix(scopedReqIds) {
|
|
17
|
+
const matrix = new Map();
|
|
18
|
+
for (const reqId of scopedReqIds) {
|
|
19
|
+
matrix.set(reqId, { required: true, verified: false, built: false });
|
|
20
|
+
}
|
|
21
|
+
return matrix;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Walk .brain/phases/VERIFICATION.md files and mark verified=true in matrix.
|
|
26
|
+
* Uses same regex as complete.cjs: /- \[x\] (\S+)/g
|
|
27
|
+
* @param {string} brainDir
|
|
28
|
+
* @param {Map} matrix
|
|
29
|
+
*/
|
|
30
|
+
function crossReferenceVerifications(brainDir, matrix) {
|
|
31
|
+
const phasesDir = path.join(brainDir, 'phases');
|
|
32
|
+
if (!fs.existsSync(phasesDir)) return;
|
|
33
|
+
|
|
34
|
+
for (const dir of fs.readdirSync(phasesDir)) {
|
|
35
|
+
const verPath = path.join(phasesDir, dir, 'VERIFICATION.md');
|
|
36
|
+
if (!fs.existsSync(verPath)) continue;
|
|
37
|
+
|
|
38
|
+
const content = fs.readFileSync(verPath, 'utf8');
|
|
39
|
+
const regex = /- \[x\] (\S+)/g;
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = regex.exec(content)) !== null) {
|
|
42
|
+
const reqId = match[1];
|
|
43
|
+
if (matrix.has(reqId)) {
|
|
44
|
+
matrix.get(reqId).verified = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Walk .brain/phases/SUMMARY-*.md files and mark built=true in matrix.
|
|
52
|
+
* Uses same regex as complete.cjs: /requirements-completed:\s*\[([^\]]*)\]/
|
|
53
|
+
* @param {string} brainDir
|
|
54
|
+
* @param {Map} matrix
|
|
55
|
+
*/
|
|
56
|
+
function crossReferenceSummaries(brainDir, matrix) {
|
|
57
|
+
const phasesDir = path.join(brainDir, 'phases');
|
|
58
|
+
if (!fs.existsSync(phasesDir)) return;
|
|
59
|
+
|
|
60
|
+
for (const dir of fs.readdirSync(phasesDir)) {
|
|
61
|
+
const phaseDir = path.join(phasesDir, dir);
|
|
62
|
+
if (!fs.statSync(phaseDir).isDirectory()) continue;
|
|
63
|
+
|
|
64
|
+
for (const f of fs.readdirSync(phaseDir)) {
|
|
65
|
+
if (!/^SUMMARY-\d+\.md$/.test(f)) continue;
|
|
66
|
+
|
|
67
|
+
const content = fs.readFileSync(path.join(phaseDir, f), 'utf8');
|
|
68
|
+
const match = content.match(/requirements-completed:\s*\[([^\]]*)\]/);
|
|
69
|
+
if (!match) continue;
|
|
70
|
+
|
|
71
|
+
const reqIds = match[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
72
|
+
for (const reqId of reqIds) {
|
|
73
|
+
if (matrix.has(reqId)) {
|
|
74
|
+
matrix.get(reqId).built = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Find discrepancies in the requirement matrix.
|
|
83
|
+
* @param {Map} matrix
|
|
84
|
+
* @returns {Array<{reqId: string, type: string, detail: string}>}
|
|
85
|
+
*/
|
|
86
|
+
function findDiscrepancies(matrix) {
|
|
87
|
+
const discrepancies = [];
|
|
88
|
+
|
|
89
|
+
for (const [reqId, entry] of matrix) {
|
|
90
|
+
if (entry.required && !entry.verified) {
|
|
91
|
+
discrepancies.push({
|
|
92
|
+
reqId,
|
|
93
|
+
type: 'not-verified',
|
|
94
|
+
detail: `Requirement ${reqId} exists but has no verification evidence`
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (entry.required && !entry.built) {
|
|
98
|
+
discrepancies.push({
|
|
99
|
+
reqId,
|
|
100
|
+
type: 'not-built',
|
|
101
|
+
detail: `Requirement ${reqId} exists but has no summary/build evidence`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (!entry.required && entry.verified) {
|
|
105
|
+
discrepancies.push({
|
|
106
|
+
reqId,
|
|
107
|
+
type: 'orphan',
|
|
108
|
+
detail: `Verification found for ${reqId} but it is not a scoped requirement`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return discrepancies;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate a Markdown audit report.
|
|
118
|
+
* @param {Map} matrix
|
|
119
|
+
* @param {Array} discrepancies
|
|
120
|
+
* @param {string} version
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
function generateReport(matrix, discrepancies, version) {
|
|
124
|
+
const total = [...matrix.values()].filter(e => e.required).length;
|
|
125
|
+
const verified = [...matrix.values()].filter(e => e.required && e.verified).length;
|
|
126
|
+
const built = [...matrix.values()].filter(e => e.required && e.built).length;
|
|
127
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
128
|
+
const verdict = discrepancies.length === 0 ? 'PASS' : 'FAIL';
|
|
129
|
+
|
|
130
|
+
const lines = [
|
|
131
|
+
'---',
|
|
132
|
+
`version: ${version}`,
|
|
133
|
+
`date: ${date}`,
|
|
134
|
+
`total: ${total}`,
|
|
135
|
+
`verified: ${verified}`,
|
|
136
|
+
`built: ${built}`,
|
|
137
|
+
`discrepancy_count: ${discrepancies.length}`,
|
|
138
|
+
`verdict: ${verdict}`,
|
|
139
|
+
'---',
|
|
140
|
+
'',
|
|
141
|
+
`# Milestone Audit: ${version}`,
|
|
142
|
+
'',
|
|
143
|
+
'## Requirement Matrix',
|
|
144
|
+
'',
|
|
145
|
+
'| Req | Required | Verified | Built | Status |',
|
|
146
|
+
'|-----|----------|----------|-------|--------|'
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
for (const [reqId, entry] of matrix) {
|
|
150
|
+
const status = entry.required && entry.verified && entry.built
|
|
151
|
+
? 'OK'
|
|
152
|
+
: entry.required ? 'GAP' : 'ORPHAN';
|
|
153
|
+
lines.push(
|
|
154
|
+
`| ${reqId} | ${entry.required ? 'Yes' : 'No'} | ${entry.verified ? 'Yes' : 'No'} | ${entry.built ? 'Yes' : 'No'} | ${status} |`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
lines.push('');
|
|
159
|
+
|
|
160
|
+
if (discrepancies.length > 0) {
|
|
161
|
+
lines.push('## Discrepancies');
|
|
162
|
+
lines.push('');
|
|
163
|
+
for (const d of discrepancies) {
|
|
164
|
+
lines.push(`- **${d.reqId}** (${d.type}): ${d.detail}`);
|
|
165
|
+
}
|
|
166
|
+
lines.push('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
lines.push('## Summary');
|
|
170
|
+
lines.push('');
|
|
171
|
+
lines.push(`Verdict: **${verdict}**`);
|
|
172
|
+
lines.push('');
|
|
173
|
+
if (verdict === 'PASS') {
|
|
174
|
+
lines.push('All scoped requirements have both verification and build evidence.');
|
|
175
|
+
} else {
|
|
176
|
+
lines.push(`${discrepancies.length} discrepancy(ies) found. Resolve gaps before milestone completion.`);
|
|
177
|
+
}
|
|
178
|
+
lines.push('');
|
|
179
|
+
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Write audit report file to .brain/audits/.
|
|
185
|
+
* Creates directory if missing.
|
|
186
|
+
* @param {string} brainDir
|
|
187
|
+
* @param {string} version
|
|
188
|
+
* @param {string} report
|
|
189
|
+
*/
|
|
190
|
+
function writeAuditFile(brainDir, version, report) {
|
|
191
|
+
const auditsDir = path.join(brainDir, 'audits');
|
|
192
|
+
if (!fs.existsSync(auditsDir)) {
|
|
193
|
+
fs.mkdirSync(auditsDir, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
const filePath = path.join(auditsDir, `${version}-audit.md`);
|
|
196
|
+
atomicWriteSync(filePath, report);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run the full audit flow.
|
|
201
|
+
* @param {string} brainDir
|
|
202
|
+
* @param {object} state - Brain state object
|
|
203
|
+
* @param {object} opts - Options
|
|
204
|
+
* @returns {{ action: string, discrepancies: Array, report_path: string, verifiedReqIds: string[] }}
|
|
205
|
+
*/
|
|
206
|
+
function runAudit(brainDir, state, opts = {}) {
|
|
207
|
+
// 1. Parse requirements
|
|
208
|
+
const { requirements } = parseRequirements(brainDir);
|
|
209
|
+
const allReqIds = requirements.map(r => r.id);
|
|
210
|
+
|
|
211
|
+
// 2. Scope to completed/verified phases (reuse logic from complete.cjs)
|
|
212
|
+
const completedPhaseReqs = new Set();
|
|
213
|
+
try {
|
|
214
|
+
const roadmap = parseRoadmap(brainDir);
|
|
215
|
+
for (const phase of roadmap.phases) {
|
|
216
|
+
if (['Complete', 'complete', 'Verified', 'verified'].includes(phase.status)) {
|
|
217
|
+
for (const req of phase.requirements) {
|
|
218
|
+
completedPhaseReqs.add(req);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// If roadmap parse fails, use all requirements
|
|
224
|
+
allReqIds.forEach(r => completedPhaseReqs.add(r));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const scopedReqIds = allReqIds.filter(r => completedPhaseReqs.has(r));
|
|
228
|
+
|
|
229
|
+
// 3. Build matrix and cross-reference
|
|
230
|
+
const matrix = buildRequirementMatrix(scopedReqIds);
|
|
231
|
+
crossReferenceVerifications(brainDir, matrix);
|
|
232
|
+
crossReferenceSummaries(brainDir, matrix);
|
|
233
|
+
|
|
234
|
+
// 4. Find discrepancies
|
|
235
|
+
const discrepancies = findDiscrepancies(matrix);
|
|
236
|
+
|
|
237
|
+
// 5. Generate and write report
|
|
238
|
+
const version = state.milestone ? state.milestone.current : 'v1.0';
|
|
239
|
+
const report = generateReport(matrix, discrepancies, version);
|
|
240
|
+
writeAuditFile(brainDir, version, report);
|
|
241
|
+
|
|
242
|
+
// 6. Collect verifiedReqIds (both verified AND built)
|
|
243
|
+
const verifiedReqIds = [];
|
|
244
|
+
for (const [reqId, entry] of matrix) {
|
|
245
|
+
if (entry.required && entry.verified && entry.built) {
|
|
246
|
+
verifiedReqIds.push(reqId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const reportPath = path.join(brainDir, 'audits', `${version}-audit.md`);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
action: 'audit-complete',
|
|
254
|
+
discrepancies,
|
|
255
|
+
report_path: reportPath,
|
|
256
|
+
verifiedReqIds
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
buildRequirementMatrix,
|
|
262
|
+
crossReferenceVerifications,
|
|
263
|
+
crossReferenceSummaries,
|
|
264
|
+
findDiscrepancies,
|
|
265
|
+
generateReport,
|
|
266
|
+
writeAuditFile,
|
|
267
|
+
runAudit
|
|
268
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { output, error, success } = require('../core.cjs');
|
|
5
|
+
const { createADR, listADRs, searchADRs } = require('../adr.cjs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse --key value pairs from args array.
|
|
9
|
+
* @param {string[]} args
|
|
10
|
+
* @returns {object} Parsed key-value pairs
|
|
11
|
+
*/
|
|
12
|
+
function parseFlags(args) {
|
|
13
|
+
const flags = {};
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
if (args[i].startsWith('--') && i + 1 < args.length) {
|
|
16
|
+
const key = args[i].slice(2);
|
|
17
|
+
flags[key] = args[i + 1];
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return flags;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Show ADR command usage.
|
|
26
|
+
*/
|
|
27
|
+
function showUsage() {
|
|
28
|
+
const text = [
|
|
29
|
+
'Usage: brain-dev adr <subcommand> [options]',
|
|
30
|
+
'',
|
|
31
|
+
'Subcommands:',
|
|
32
|
+
' create Create a new Architecture Decision Record',
|
|
33
|
+
' --title <text> Decision title (required)',
|
|
34
|
+
' --context <text> Why this decision came up',
|
|
35
|
+
' --decision <text> What was chosen',
|
|
36
|
+
' --alternatives <t> What was rejected',
|
|
37
|
+
' --consequences <t> Impact of the decision',
|
|
38
|
+
' --phase <n> Phase number',
|
|
39
|
+
' --plan <n> Plan number',
|
|
40
|
+
'',
|
|
41
|
+
' list [--phase <n>] List all ADRs (optionally filtered by phase)',
|
|
42
|
+
'',
|
|
43
|
+
' search <keyword> Search ADR content by keyword'
|
|
44
|
+
].join('\n');
|
|
45
|
+
console.log(text);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run the ADR command.
|
|
50
|
+
* @param {string[]} args - Command arguments
|
|
51
|
+
* @param {object} opts
|
|
52
|
+
* @param {string} opts.brainDir - Path to .brain/ directory
|
|
53
|
+
*/
|
|
54
|
+
function run(args = [], opts = {}) {
|
|
55
|
+
const brainDir = opts && opts.brainDir
|
|
56
|
+
? opts.brainDir
|
|
57
|
+
: path.join(process.cwd(), '.brain');
|
|
58
|
+
|
|
59
|
+
const subcommand = args && args[0];
|
|
60
|
+
|
|
61
|
+
if (!subcommand || subcommand === 'help') {
|
|
62
|
+
showUsage();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
switch (subcommand) {
|
|
67
|
+
case 'create': {
|
|
68
|
+
const flags = parseFlags(args.slice(1));
|
|
69
|
+
if (!flags.title) {
|
|
70
|
+
error('--title is required for adr create');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const result = createADR(brainDir, {
|
|
74
|
+
title: flags.title,
|
|
75
|
+
context: flags.context || '',
|
|
76
|
+
decision: flags.decision || '',
|
|
77
|
+
alternatives: flags.alternatives || '',
|
|
78
|
+
consequences: flags.consequences || '',
|
|
79
|
+
phase: flags.phase || '',
|
|
80
|
+
plan: flags.plan || '',
|
|
81
|
+
deciders: flags.deciders || 'agent'
|
|
82
|
+
});
|
|
83
|
+
output(result, `Created ${result.id} at ${result.path}`);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'list': {
|
|
88
|
+
const flags = parseFlags(args.slice(1));
|
|
89
|
+
const adrs = listADRs(brainDir, flags.phase ? { phase: flags.phase } : undefined);
|
|
90
|
+
if (adrs.length === 0) {
|
|
91
|
+
output({ adrs: [] }, 'No ADRs found.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const lines = adrs.map(a =>
|
|
95
|
+
`${String(a.id).padStart(3, '0')} ${a.status.padEnd(12)} ${a.title}`
|
|
96
|
+
);
|
|
97
|
+
output({ adrs }, lines.join('\n'));
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'search': {
|
|
102
|
+
const keyword = args[1];
|
|
103
|
+
if (!keyword) {
|
|
104
|
+
error('Usage: brain-dev adr search <keyword>');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const results = searchADRs(brainDir, keyword);
|
|
108
|
+
if (results.length === 0) {
|
|
109
|
+
output({ results: [] }, `No ADRs matching "${keyword}".`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const lines = results.map(a =>
|
|
113
|
+
`${String(a.id).padStart(3, '0')} ${a.status.padEnd(12)} ${a.title}`
|
|
114
|
+
);
|
|
115
|
+
output({ results }, lines.join('\n'));
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
default:
|
|
120
|
+
error(`Unknown adr subcommand: '${subcommand}'. Use: create, list, search`);
|
|
121
|
+
showUsage();
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { run };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readState, writeState } = require('../state.cjs');
|
|
6
|
+
const { parseRoadmap, writeRoadmap } = require('../roadmap.cjs');
|
|
7
|
+
const { gitTag } = require('../git.cjs');
|
|
8
|
+
const { output, error, success } = require('../core.cjs');
|
|
9
|
+
const audit = require('../audit.cjs');
|
|
10
|
+
const requirements = require('../requirements.cjs');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Find a phase directory under .brain/phases/ matching a phase number.
|
|
14
|
+
* @param {string} brainDir
|
|
15
|
+
* @param {number} phaseNumber
|
|
16
|
+
* @returns {string|null}
|
|
17
|
+
*/
|
|
18
|
+
function findPhaseDir(brainDir, phaseNumber) {
|
|
19
|
+
const phasesDir = path.join(brainDir, 'phases');
|
|
20
|
+
if (!fs.existsSync(phasesDir)) return null;
|
|
21
|
+
|
|
22
|
+
const padded = String(phaseNumber).padStart(2, '0');
|
|
23
|
+
const match = fs.readdirSync(phasesDir).find(d => d.startsWith(`${padded}-`));
|
|
24
|
+
return match ? path.join(phasesDir, match) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Scan phase directory for PLAN and SUMMARY files.
|
|
29
|
+
* @param {string} phaseDir
|
|
30
|
+
* @returns {{ plans: number[], summaries: number[] }}
|
|
31
|
+
*/
|
|
32
|
+
function scanPhaseArtifacts(phaseDir) {
|
|
33
|
+
if (!phaseDir || !fs.existsSync(phaseDir)) return { plans: [], summaries: [] };
|
|
34
|
+
|
|
35
|
+
const files = fs.readdirSync(phaseDir);
|
|
36
|
+
const plans = [];
|
|
37
|
+
const summaries = [];
|
|
38
|
+
|
|
39
|
+
for (const f of files) {
|
|
40
|
+
const planMatch = f.match(/^PLAN-(\d+)\.md$/);
|
|
41
|
+
if (planMatch) plans.push(parseInt(planMatch[1], 10));
|
|
42
|
+
|
|
43
|
+
const sumMatch = f.match(/^SUMMARY-(\d+)\.md$/);
|
|
44
|
+
if (sumMatch) summaries.push(parseInt(sumMatch[1], 10));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { plans, summaries };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract requirements-completed from SUMMARY frontmatter.
|
|
52
|
+
* @param {string} content
|
|
53
|
+
* @returns {string[]}
|
|
54
|
+
*/
|
|
55
|
+
function extractCompletedReqs(content) {
|
|
56
|
+
const match = content.match(/requirements-completed:\s*\[([^\]]*)\]/);
|
|
57
|
+
if (!match) return [];
|
|
58
|
+
return match[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract verified requirement IDs from VERIFICATION.md.
|
|
63
|
+
* @param {string} content
|
|
64
|
+
* @returns {string[]}
|
|
65
|
+
*/
|
|
66
|
+
function extractVerifiedReqs(content) {
|
|
67
|
+
const reqs = [];
|
|
68
|
+
const regex = /- \[x\] (\S+)/g;
|
|
69
|
+
let match;
|
|
70
|
+
while ((match = regex.exec(content)) !== null) {
|
|
71
|
+
reqs.push(match[1]);
|
|
72
|
+
}
|
|
73
|
+
return reqs;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract all requirement IDs from REQUIREMENTS.md.
|
|
78
|
+
* Looks for ## REQ-ID: patterns.
|
|
79
|
+
* @param {string} content
|
|
80
|
+
* @returns {string[]}
|
|
81
|
+
*/
|
|
82
|
+
function extractAllReqIds(content) {
|
|
83
|
+
const ids = [];
|
|
84
|
+
const regex = /^## (\S+):/gm;
|
|
85
|
+
let match;
|
|
86
|
+
while ((match = regex.exec(content)) !== null) {
|
|
87
|
+
ids.push(match[1]);
|
|
88
|
+
}
|
|
89
|
+
return ids;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Increment a semver-like version string.
|
|
94
|
+
* v1.0 -> v1.1, v1.9 -> v1.10
|
|
95
|
+
* @param {string} version
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
function incrementVersion(version) {
|
|
99
|
+
const match = version.match(/^v?(\d+)\.(\d+)$/);
|
|
100
|
+
if (!match) return 'v1.1';
|
|
101
|
+
const major = parseInt(match[1], 10);
|
|
102
|
+
const minor = parseInt(match[2], 10);
|
|
103
|
+
return `v${major}.${minor + 1}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run the complete command.
|
|
108
|
+
*
|
|
109
|
+
* Default (no flags): Perform 3-source audit for milestone completion.
|
|
110
|
+
* --phase N: Mark a single phase as complete.
|
|
111
|
+
*
|
|
112
|
+
* @param {string[]} args
|
|
113
|
+
* @param {object} [opts] - Options (brainDir, skipGitTag for testing)
|
|
114
|
+
* @returns {object}
|
|
115
|
+
*/
|
|
116
|
+
async function run(args = [], opts = {}) {
|
|
117
|
+
const brainDir = opts.brainDir || path.join(process.cwd(), '.brain');
|
|
118
|
+
const state = readState(brainDir);
|
|
119
|
+
|
|
120
|
+
if (!state) {
|
|
121
|
+
error("No brain state found. Run 'brain-dev init' first.");
|
|
122
|
+
return { error: 'no-state' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --audit-only: run audit without triggering milestone completion
|
|
126
|
+
if (args.includes('--audit-only')) {
|
|
127
|
+
return audit.runAudit(brainDir, state, opts);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const phaseIdx = args.indexOf('--phase');
|
|
131
|
+
if (phaseIdx >= 0) {
|
|
132
|
+
return handlePhaseComplete(args, phaseIdx, brainDir, state);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return handleMilestoneComplete(brainDir, state, args, opts);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Handle --phase N: mark single phase as complete.
|
|
140
|
+
*/
|
|
141
|
+
function handlePhaseComplete(args, phaseIdx, brainDir, state) {
|
|
142
|
+
const phaseNumber = parseInt(args[phaseIdx + 1], 10);
|
|
143
|
+
const phaseDir = findPhaseDir(brainDir, phaseNumber);
|
|
144
|
+
|
|
145
|
+
if (!phaseDir) {
|
|
146
|
+
error(`Phase ${phaseNumber} directory not found.`);
|
|
147
|
+
return { error: 'phase-not-found' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { plans, summaries } = scanPhaseArtifacts(phaseDir);
|
|
151
|
+
|
|
152
|
+
// Check all plans have summaries
|
|
153
|
+
const missingPlans = plans.filter(p => !summaries.includes(p));
|
|
154
|
+
if (missingPlans.length > 0) {
|
|
155
|
+
error(`Plans not yet executed: ${missingPlans.join(', ')}. Run /brain:execute first.`);
|
|
156
|
+
return { error: 'incomplete-plans', missing: missingPlans, nextAction: '/brain:execute' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check verification was run (VERIFICATION.md should exist)
|
|
160
|
+
const verificationPath = path.join(phaseDir, 'VERIFICATION.md');
|
|
161
|
+
if (!fs.existsSync(verificationPath) && state.phase.status !== 'verified') {
|
|
162
|
+
error(`Phase ${phaseNumber} not verified. Run /brain:verify first.`);
|
|
163
|
+
return { error: 'not-verified', nextAction: '/brain:verify' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Mark phase as complete in state (handle both string and object formats)
|
|
167
|
+
if (Array.isArray(state.phase.phases)) {
|
|
168
|
+
const idx = state.phase.phases.findIndex(p =>
|
|
169
|
+
typeof p === 'object' ? p.number === phaseNumber : false
|
|
170
|
+
);
|
|
171
|
+
if (idx >= 0 && typeof state.phase.phases[idx] === 'object') {
|
|
172
|
+
state.phase.phases[idx].status = 'complete';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Advance to next phase
|
|
177
|
+
const nextPhase = phaseNumber + 1;
|
|
178
|
+
const totalPhases = state.phase.total || (Array.isArray(state.phase.phases) ? state.phase.phases.length : 0);
|
|
179
|
+
const hasNextPhase = totalPhases > 0 && nextPhase <= totalPhases;
|
|
180
|
+
if (hasNextPhase) {
|
|
181
|
+
state.phase.current = nextPhase;
|
|
182
|
+
state.phase.status = 'pending';
|
|
183
|
+
} else {
|
|
184
|
+
state.phase.status = 'complete';
|
|
185
|
+
}
|
|
186
|
+
writeState(brainDir, state);
|
|
187
|
+
|
|
188
|
+
// Update roadmap
|
|
189
|
+
try {
|
|
190
|
+
const roadmap = parseRoadmap(brainDir);
|
|
191
|
+
const roadmapPhase = roadmap.phases.find(p => p.number === phaseNumber);
|
|
192
|
+
if (roadmapPhase) {
|
|
193
|
+
roadmapPhase.status = 'Complete';
|
|
194
|
+
}
|
|
195
|
+
writeRoadmap(brainDir, roadmap);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
error(`Warning: Could not update roadmap: ${e.message}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const nextActionCmd = hasNextPhase ? '/brain:discuss' : '/brain:complete';
|
|
201
|
+
const msg = hasNextPhase
|
|
202
|
+
? `Phase ${phaseNumber} complete. Next: Phase ${nextPhase}. Run ${nextActionCmd} to continue.`
|
|
203
|
+
: `Phase ${phaseNumber} complete. All phases done! Run /brain:complete for milestone completion.`;
|
|
204
|
+
output({ action: 'phase-complete', phase: phaseNumber, nextPhase: hasNextPhase ? nextPhase : null, nextAction: nextActionCmd }, `[brain] ${msg}`);
|
|
205
|
+
return { action: 'phase-complete', phase: phaseNumber, nextPhase: hasNextPhase ? nextPhase : null, nextAction: nextActionCmd };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Handle milestone completion with 3-source audit.
|
|
210
|
+
* Now delegates cross-referencing to audit.cjs and wires requirements status updates.
|
|
211
|
+
*/
|
|
212
|
+
function handleMilestoneComplete(brainDir, state, args = [], opts = {}) {
|
|
213
|
+
// Run the full audit
|
|
214
|
+
const auditResult = audit.runAudit(brainDir, state, opts);
|
|
215
|
+
const hasForce = args.includes('--force');
|
|
216
|
+
|
|
217
|
+
if (auditResult.discrepancies.length > 0 && !hasForce) {
|
|
218
|
+
const gaps = auditResult.discrepancies.map(d => d.reqId);
|
|
219
|
+
const uniqueGaps = [...new Set(gaps)];
|
|
220
|
+
const msg = [
|
|
221
|
+
`Gaps detected: ${uniqueGaps.length} requirement(s) not fully covered.`,
|
|
222
|
+
`Missing: ${uniqueGaps.join(', ')}`,
|
|
223
|
+
`Run gap closure: /brain:plan --gaps, then /brain:execute, then /brain:verify, then /brain:complete again.`,
|
|
224
|
+
`Or use --force to override.`
|
|
225
|
+
].join('\n');
|
|
226
|
+
|
|
227
|
+
output({ action: 'gaps-found', gaps: uniqueGaps, audit: auditResult }, `[brain] ${msg}`);
|
|
228
|
+
return { action: 'gaps-found', gaps: uniqueGaps, audit: auditResult };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// No gaps (or --force override) - proceed with milestone completion
|
|
232
|
+
|
|
233
|
+
// Update requirements status for verified requirements
|
|
234
|
+
for (const reqId of auditResult.verifiedReqIds) {
|
|
235
|
+
try {
|
|
236
|
+
requirements.updateStatus(brainDir, reqId, true);
|
|
237
|
+
requirements.updateTraceability(brainDir, reqId, { status: 'Complete' });
|
|
238
|
+
output(`Updated requirement ${reqId} to Complete`);
|
|
239
|
+
} catch (e) {
|
|
240
|
+
error(`Warning: Could not update requirement ${reqId}: ${e.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const version = state.milestone.current;
|
|
245
|
+
|
|
246
|
+
// Create git tag (unless skipped for testing)
|
|
247
|
+
if (!opts.skipGitTag) {
|
|
248
|
+
const projectDir = path.dirname(brainDir);
|
|
249
|
+
gitTag(version, `Milestone: ${state.milestone.name || version}`, projectDir);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Archive milestone
|
|
253
|
+
state.milestone.history.push({
|
|
254
|
+
version,
|
|
255
|
+
name: state.milestone.name,
|
|
256
|
+
completed: new Date().toISOString().slice(0, 10)
|
|
257
|
+
});
|
|
258
|
+
state.milestone.current = incrementVersion(version);
|
|
259
|
+
state.milestone.name = null;
|
|
260
|
+
writeState(brainDir, state);
|
|
261
|
+
|
|
262
|
+
const forceWarning = hasForce && auditResult.discrepancies.length > 0
|
|
263
|
+
? ' (forced with discrepancies)'
|
|
264
|
+
: '';
|
|
265
|
+
const msg = `Milestone ${version} complete!${forceWarning}${opts.skipGitTag ? '' : ' Tagged in git.'}`;
|
|
266
|
+
output({ action: 'milestone-complete', version, audit: auditResult }, `[brain] ${msg}`);
|
|
267
|
+
return { action: 'milestone-complete', version, audit: auditResult };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = { run };
|