mddd-cli 6.2.2 → 7.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/.agents/skills/md-audit/SKILL.md +36 -34
- package/.agents/skills/md-edit/SKILL.md +38 -50
- package/.agents/skills/md-impl/SKILL.md +58 -48
- package/.agents/skills/md-new/SKILL.md +29 -40
- package/.agents/templates/spec-template.md +22 -40
- package/AGENTS.md +192 -0
- package/bin/cli.js +15 -1
- package/package.json +2 -2
- package/readme.md +105 -19
- package/src/commands/init.js +3 -3
- package/src/commands/status.js +534 -0
- package/src/commands/status.spec.md +131 -0
- package/src/services/FileSystemService.spec.md +3 -1
- package/src/services/InitService.js +1 -1
- package/src/services/InitService.spec.md +7 -5
- package/system_prompt.md +0 -171
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes the `md status` command.
|
|
3
|
+
* Generates a beautiful MDDD coverage report with metrics from all .spec.md files.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} SpecMetrics
|
|
12
|
+
* @property {string} relativePath
|
|
13
|
+
* @property {string} version
|
|
14
|
+
* @property {'draft' | 'stable'} status
|
|
15
|
+
* @property {'Coeso' | 'Caótico' | undefined} classification
|
|
16
|
+
* @property {number} tasksTotal
|
|
17
|
+
* @property {number} tasksCompleted
|
|
18
|
+
* @property {number} tasksPending
|
|
19
|
+
* @property {number} totalChanges
|
|
20
|
+
* @property {{ major: number, minor: number, patch: number }} changeBreakdown
|
|
21
|
+
* @property {{ discovery: number, fix: number, improvement: number, documentation: number, refactor: number, other: number }} changeTypes
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} DashboardSummary
|
|
26
|
+
* @property {number} totalSpecs
|
|
27
|
+
* @property {number} coesoCount
|
|
28
|
+
* @property {number} caoticoCount
|
|
29
|
+
* @property {number} unclassifiedCount
|
|
30
|
+
* @property {number} totalTasks
|
|
31
|
+
* @property {number} completedTasks
|
|
32
|
+
* @property {number} pendingTasks
|
|
33
|
+
* @property {number} totalChanges
|
|
34
|
+
* @property {number} totalDiscoveries
|
|
35
|
+
* @property {number} totalFixes
|
|
36
|
+
* @property {number} totalImprovements
|
|
37
|
+
* @property {number} totalDocumentation
|
|
38
|
+
* @property {number} totalRefactors
|
|
39
|
+
* @property {number} totalMajors
|
|
40
|
+
* @property {number} totalMinors
|
|
41
|
+
* @property {number} totalPatches
|
|
42
|
+
* @property {string[]} criticalPoints
|
|
43
|
+
* @property {SpecMetrics[]} specs // detailed per-spec data for testing
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Classifies a change summary entry into a type category.
|
|
48
|
+
* @param {string} summary
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function classifyChangeType(summary) {
|
|
52
|
+
const s = summary.toLowerCase();
|
|
53
|
+
|
|
54
|
+
if (/\b(discovery|descoberta|found|encontrado)\b/.test(s)) return 'discovery';
|
|
55
|
+
if (/\b(fix|bug|correç[ãa]o|corrigido|defeito)\b/.test(s)) return 'fix';
|
|
56
|
+
if (/\b(improvement|melhoria|enhance|refinamento|aprimoramento)\b/.test(s)) return 'improvement';
|
|
57
|
+
if (/\b(documentation|doc|docs|diagram|documentaç[ãa]o|readme)\b/.test(s)) return 'documentation';
|
|
58
|
+
if (/\b(refactor|refatoraç[ãa]o|reestrutura|simplifica|reescrita)\b/.test(s)) return 'refactor';
|
|
59
|
+
|
|
60
|
+
return 'other';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Analyzes a single .spec.md file and extracts metrics.
|
|
65
|
+
* @param {string} relativePath
|
|
66
|
+
* @returns {SpecMetrics}
|
|
67
|
+
*/
|
|
68
|
+
function analyzeSpec(relativePath) {
|
|
69
|
+
const absolutePath = path.resolve(process.cwd(), relativePath);
|
|
70
|
+
let content;
|
|
71
|
+
try {
|
|
72
|
+
content = fs.readFileSync(absolutePath, 'utf-8');
|
|
73
|
+
} catch {
|
|
74
|
+
return {
|
|
75
|
+
relativePath,
|
|
76
|
+
version: 'unknown',
|
|
77
|
+
status: 'draft',
|
|
78
|
+
classification: undefined,
|
|
79
|
+
tasksTotal: 0,
|
|
80
|
+
tasksCompleted: 0,
|
|
81
|
+
tasksPending: 0,
|
|
82
|
+
totalChanges: 0,
|
|
83
|
+
changeBreakdown: { major: 0, minor: 0, patch: 0 },
|
|
84
|
+
changeTypes: { discovery: 0, fix: 0, improvement: 0, documentation: 0, refactor: 0, other: 0 }
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extract version — try multiple patterns
|
|
89
|
+
let version = 'unknown';
|
|
90
|
+
const versionMatch = content.match(/@spec-version\s+v?(\d+\.\d+\.\d+)/);
|
|
91
|
+
if (versionMatch) {
|
|
92
|
+
version = versionMatch[1];
|
|
93
|
+
} else {
|
|
94
|
+
const specVersionMatch = content.match(/SPEC_VERSION:\s*v?(\d+\.\d+\.\d+)/i);
|
|
95
|
+
if (specVersionMatch) version = specVersionMatch[1];
|
|
96
|
+
}
|
|
97
|
+
if (version === 'unknown') {
|
|
98
|
+
// Fallback: match any standalone vX.Y.Z at the start of a line
|
|
99
|
+
const fallbackMatch = content.match(/^#+?\s+v?(\d+\.\d+\.\d+)/m);
|
|
100
|
+
if (fallbackMatch) version = fallbackMatch[1];
|
|
101
|
+
}
|
|
102
|
+
if (version === 'unknown') {
|
|
103
|
+
// Fallback: match vX.Y.Z near the top of the file (first 3 lines)
|
|
104
|
+
const topLines = content.split('\n').slice(0, 3).join('\n');
|
|
105
|
+
const topMatch = topLines.match(/v?(\d+\.\d+\.\d+)/);
|
|
106
|
+
if (topMatch) version = topMatch[1];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Extract status — multiple formats:
|
|
110
|
+
// "SPEC_VERSION: v1.0.0 — stable" (MDDD-CLI padrão)
|
|
111
|
+
// "*SPEC_VERSION:** v1.0.0 stable" (Appfy/legacy)
|
|
112
|
+
// "SPEC_VERSION: v1.0.0 — draft"
|
|
113
|
+
// "**SPEC_VERSION: v1.0.0** (draft|stable)"
|
|
114
|
+
let status = 'draft';
|
|
115
|
+
const statusPatterns = [
|
|
116
|
+
// Pattern 1: **SPEC_VERSION:** vX.Y.Z — stable|draft (bold label format)
|
|
117
|
+
/\*{0,2}SPEC_VERSION\*{0,2}:?\*{0,2}\s+v?\d+\.\d+\.\d+\s*[—–-]?\s*(draft|stable)/i,
|
|
118
|
+
// Pattern 2: *SPEC_VERSION:** vX.Y.Z stable (Appfy/legacy — bold before and after)
|
|
119
|
+
/\*{0,2}SPEC_VERSION\*{0,2}:?\*{1,2}\s+v?\d+\.\d+\.\d+\s+(draft|stable)/i,
|
|
120
|
+
// Pattern 3: Title line: "# CLI Module | v6.3.0 (Stable)" or "(Draft)"
|
|
121
|
+
/^#\s+.*?\(?(Stable|Draft)\)?/im,
|
|
122
|
+
// Pattern 4: SPEC_VERSION: vX.Y.Z — stable|draft (plain label, no bold)
|
|
123
|
+
/SPEC_VERSION:\s*v?\d+\.\d+\.\d+\s*[—–-]\s*(draft|stable)/i,
|
|
124
|
+
];
|
|
125
|
+
for (const pattern of statusPatterns) {
|
|
126
|
+
const match = content.match(pattern);
|
|
127
|
+
if (match) {
|
|
128
|
+
status = /** @type {'draft' | 'stable'} */ (match[1].toLowerCase());
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Extract classification (Coeso / Caótico)
|
|
134
|
+
// Common patterns in specs:
|
|
135
|
+
// "**Classification:** Coeso" (English, bold label)
|
|
136
|
+
// "Classificação: **Coeso**" (Portuguese, bold value)
|
|
137
|
+
// "classificado como **Caótico/Acoplado**" (Portuguese, full sentence)
|
|
138
|
+
// "Código classificado como **Caótico/Acoplado**" (Portuguese variant)
|
|
139
|
+
let classification;
|
|
140
|
+
const classificationPatterns = [
|
|
141
|
+
// Pattern 1: "**Classification:** Coeso" or "**Classification:** Caótico" (English)
|
|
142
|
+
/Classification:\s*\*{0,2}\s*(Coeso|Caótico)\s*\*{0,2}/i,
|
|
143
|
+
// Pattern 2: "Classificação: Coeso" or "Classificação: **Coeso**" or "Classificação: **Caótico**"
|
|
144
|
+
// (title line or body, with or without bold markers around the value)
|
|
145
|
+
/classificaç[ãa]o:\s*\*{0,2}\s*(Coeso|Caótico)\s*\*{0,2}/i,
|
|
146
|
+
// Pattern 3: "classificado como **Caótico/Acoplado**" or "Código classificado como **Coeso**"
|
|
147
|
+
/classificado\s+como\s*\*\*(Caótico|Coeso)/i,
|
|
148
|
+
// Pattern 4: "classificado com 'Caótico'" or "classificado com "Caótico""
|
|
149
|
+
/classificado\s+com\s*['"](Caótico|Coeso)['"]/i,
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
for (const pattern of classificationPatterns) {
|
|
153
|
+
const match = content.match(pattern);
|
|
154
|
+
if (match) {
|
|
155
|
+
classification = match[1];
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Heuristic fallback if no explicit classification found
|
|
161
|
+
if (!classification) {
|
|
162
|
+
if (/\bcaótico\b/i.test(content) && /\baco(lamento|plado)\b/i.test(content)) {
|
|
163
|
+
classification = 'Caótico';
|
|
164
|
+
} else if (/\bcoeso\b/i.test(content) && /\b(baixo|clean|modular|bau)\b/i.test(content)) {
|
|
165
|
+
classification = 'Coeso';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Extract tasks from the Tasks section
|
|
170
|
+
// Match sections like "## 4. Tasks" or "## Tasks" with checklist items
|
|
171
|
+
const tasksSection = content.match(/##\s+\d*\.?\s*Tasks?\s*\n([\s\S]*?)(?=\n##\s|$)/i);
|
|
172
|
+
const tasksText = tasksSection ? tasksSection[1] : '';
|
|
173
|
+
|
|
174
|
+
const pendingMatches = tasksText.match(/- \[ \]/g);
|
|
175
|
+
const completedMatches = tasksText.match(/- \[x\]/gi);
|
|
176
|
+
const tasksPending = pendingMatches ? pendingMatches.length : 0;
|
|
177
|
+
const tasksCompleted = completedMatches ? completedMatches.length : 0;
|
|
178
|
+
const tasksTotal = tasksPending + tasksCompleted;
|
|
179
|
+
|
|
180
|
+
// Extract Audit History entries
|
|
181
|
+
const auditSection = content.match(/(?:##\s+\d*\.?\s*(?:Audit History|Change History)\s*\n[\s\S]*?)(?=\n##\s|$)/i);
|
|
182
|
+
const auditText = auditSection ? auditSection[0] : '';
|
|
183
|
+
|
|
184
|
+
// Count data rows in the audit table (lines that start with `|` and contain a date pattern)
|
|
185
|
+
const auditLines = auditText.split('\n').filter(line => {
|
|
186
|
+
const trimmed = line.trim();
|
|
187
|
+
// Match rows that start with `|` and contain a date like YYYY-MM-DD or a version like v1.2.3
|
|
188
|
+
return /^\s*\|.*\d{4}-\d{2}-\d{2}.*\|/.test(trimmed) || /^\s*\|.*v\d+\.\d+\.\d+.*\|/.test(trimmed);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const totalChanges = auditLines.length;
|
|
192
|
+
|
|
193
|
+
// Breakdown by MAJOR/MINOR/PATCH
|
|
194
|
+
let major = 0, minor = 0, patch = 0;
|
|
195
|
+
for (const line of auditLines) {
|
|
196
|
+
if (/\bMAJOR\b/i.test(line) || /\bMajor\b/.test(line)) major++;
|
|
197
|
+
else if (/\bMINOR\b/i.test(line) || /\bMinor\b/.test(line)) minor++;
|
|
198
|
+
else if (/\bPATCH\b/i.test(line) || /\bPatch\b/.test(line)) patch++;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Classify change types from the "Change Summary" column
|
|
202
|
+
// Two common table formats:
|
|
203
|
+
// (a) Date | Agent | Version | Change Summary | Change Type | (5 cols, summary = 2nd-to-last)
|
|
204
|
+
// (b) Data | Auditor | Versão | Resumo das Mudanças | (4 cols, summary = last)
|
|
205
|
+
// Heuristic: if last column contains MAJOR/MINOR/PATCH, it's a Change Type column → use 2nd-to-last as summary
|
|
206
|
+
const changeTypes = { discovery: 0, fix: 0, improvement: 0, documentation: 0, refactor: 0, other: 0 };
|
|
207
|
+
for (const line of auditLines) {
|
|
208
|
+
const columns = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
209
|
+
if (columns.length < 2) continue;
|
|
210
|
+
|
|
211
|
+
const lastCol = columns[columns.length - 1];
|
|
212
|
+
// Detect if last column is a Change Type column (short, just MAJOR/MINOR/PATCH)
|
|
213
|
+
// vs a full sentence summary that happens to mention those words
|
|
214
|
+
const isChangeTypeColumn = columns.length >= 4
|
|
215
|
+
&& lastCol.length <= 10
|
|
216
|
+
&& /^(MAJOR|MINOR|PATCH)$/i.test(lastCol.trim());
|
|
217
|
+
|
|
218
|
+
let summary;
|
|
219
|
+
if (isChangeTypeColumn) {
|
|
220
|
+
// Format (a): last col is Change Type (e.g. "MAJOR"), summary is 2nd-to-last
|
|
221
|
+
summary = columns[columns.length - 2];
|
|
222
|
+
} else {
|
|
223
|
+
// Format (b) or fallback: last col is the summary itself
|
|
224
|
+
summary = lastCol;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const type = classifyChangeType(summary);
|
|
228
|
+
changeTypes[type]++;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
relativePath,
|
|
233
|
+
version,
|
|
234
|
+
status,
|
|
235
|
+
classification,
|
|
236
|
+
tasksTotal,
|
|
237
|
+
tasksCompleted,
|
|
238
|
+
tasksPending,
|
|
239
|
+
totalChanges,
|
|
240
|
+
changeBreakdown: { major, minor, patch },
|
|
241
|
+
changeTypes
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Creates a simple text-based progress bar.
|
|
247
|
+
* @param {number} completed
|
|
248
|
+
* @param {number} total
|
|
249
|
+
* @param {number} width
|
|
250
|
+
* @returns {string}
|
|
251
|
+
*/
|
|
252
|
+
function progressBar(completed, total, width = 10) {
|
|
253
|
+
if (total === 0) return '░'.repeat(width);
|
|
254
|
+
const filled = Math.round((completed / total) * width);
|
|
255
|
+
const empty = width - filled;
|
|
256
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
257
|
+
const pct = total === 0 ? 0 : Math.round((completed / total) * 100);
|
|
258
|
+
return `${bar} ${pct}%`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Prints the dashboard to the console.
|
|
263
|
+
* @param {DashboardSummary} summary
|
|
264
|
+
*/
|
|
265
|
+
function printDashboard(summary) {
|
|
266
|
+
if (summary.totalSpecs === 0) {
|
|
267
|
+
console.log(pc.yellow(pc.bold('\n ⚠️ No .spec.md files found in this project.')));
|
|
268
|
+
console.log(pc.gray(' Run "md init" to create the initial specification.'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
var W = 58;
|
|
273
|
+
|
|
274
|
+
function boxTop() { return pc.cyan(pc.bold(' +' + '='.repeat(W) + '+')); }
|
|
275
|
+
function boxBot() { return pc.cyan(pc.bold(' +' + '='.repeat(W) + '+')); }
|
|
276
|
+
function boxMid() { return pc.cyan(pc.bold(' +' + '-'.repeat(W) + '+')); }
|
|
277
|
+
function boxLine(content) {
|
|
278
|
+
var visible = content.replace(/\x1b\[[0-9;]*m/g, '');
|
|
279
|
+
var pad = Math.max(0, W - visible.length);
|
|
280
|
+
return pc.cyan(pc.bold(' |')) + content + ' '.repeat(pad) + pc.cyan(pc.bold('|'));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function section(title) {
|
|
284
|
+
console.log(boxTop());
|
|
285
|
+
console.log(boxLine(pc.bold(pc.white(' ' + title))));
|
|
286
|
+
console.log(boxMid());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Header
|
|
290
|
+
console.log();
|
|
291
|
+
console.log(boxTop());
|
|
292
|
+
console.log(boxLine(pc.bold(pc.white(' ** MDDD COVERAGE REPORT **'))));
|
|
293
|
+
console.log(boxBot());
|
|
294
|
+
console.log();
|
|
295
|
+
|
|
296
|
+
// ── SPECS ──
|
|
297
|
+
section('SPECS');
|
|
298
|
+
|
|
299
|
+
var coesoC = summary.coesoCount > 0 ? pc.green : pc.gray;
|
|
300
|
+
var caoticoC = summary.caoticoCount > 0 ? pc.red : pc.gray;
|
|
301
|
+
var unaudC = summary.unclassifiedCount > 0 ? pc.yellow : pc.gray;
|
|
302
|
+
|
|
303
|
+
console.log(boxLine(' ' + pc.bold(coesoC('COHESIVE')) + ' ' + pc.bold(coesoC(String(summary.coesoCount).padStart(4)))));
|
|
304
|
+
console.log(boxLine(' ' + pc.bold(caoticoC('CHAOTIC')) + ' ' + pc.bold(caoticoC(String(summary.caoticoCount).padStart(4)))));
|
|
305
|
+
console.log(boxLine(' ' + pc.bold(unaudC('UNAUDITED')) + ' ' + pc.bold(unaudC(String(summary.unclassifiedCount).padStart(4)))));
|
|
306
|
+
console.log(boxMid());
|
|
307
|
+
console.log(boxLine(pc.bold(' TOTAL: ' + String(summary.totalSpecs) + ' specs')));
|
|
308
|
+
|
|
309
|
+
console.log(boxBot());
|
|
310
|
+
console.log();
|
|
311
|
+
|
|
312
|
+
// ── TASKS ──
|
|
313
|
+
section('TASKS');
|
|
314
|
+
|
|
315
|
+
if (summary.totalTasks > 0) {
|
|
316
|
+
var pct = Math.round((summary.completedTasks / summary.totalTasks) * 100);
|
|
317
|
+
var barW = 22;
|
|
318
|
+
var filled = Math.round((summary.completedTasks / summary.totalTasks) * barW);
|
|
319
|
+
var bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barW - filled);
|
|
320
|
+
|
|
321
|
+
console.log(boxLine(' ' + pc.bold(String(summary.totalTasks).padStart(4)) + ' total ' + pc.green('done') + ' ' + pc.bold(pc.green(String(summary.completedTasks).padStart(4))) + ' ' + pc.yellow('pend') + ' ' + pc.bold(pc.yellow(String(summary.pendingTasks).padStart(4)))));
|
|
322
|
+
console.log(boxLine(' ' + bar + ' ' + pc.bold(String(pct) + '%')));
|
|
323
|
+
} else {
|
|
324
|
+
console.log(boxLine(pc.gray(' (no tasks defined)')));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log(boxBot());
|
|
328
|
+
console.log();
|
|
329
|
+
|
|
330
|
+
// ── CHANGES ──
|
|
331
|
+
section('VERSION CHANGES');
|
|
332
|
+
|
|
333
|
+
if (summary.totalChanges > 0) {
|
|
334
|
+
console.log(boxLine(pc.bold(' ' + String(summary.totalChanges) + ' total changes')));
|
|
335
|
+
|
|
336
|
+
var maj = summary.totalMajors > 0 ? pc.red(pc.bold('MAJOR ' + summary.totalMajors)) : pc.gray('MAJOR 0');
|
|
337
|
+
var min = summary.totalMinors > 0 ? pc.blue(pc.bold('MINOR ' + summary.totalMinors)) : pc.gray('MINOR 0');
|
|
338
|
+
var pat = summary.totalPatches > 0 ? pc.green(pc.bold('PATCH ' + summary.totalPatches)) : pc.gray('PATCH 0');
|
|
339
|
+
console.log(boxLine(' ' + maj + ' ' + pc.gray('|') + ' ' + min + ' ' + pc.gray('|') + ' ' + pat));
|
|
340
|
+
} else {
|
|
341
|
+
console.log(boxLine(pc.gray(' (no changes recorded)')));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log(boxBot());
|
|
345
|
+
console.log();
|
|
346
|
+
|
|
347
|
+
// ── MDDD IMPACT ──
|
|
348
|
+
var impactItems = [];
|
|
349
|
+
if (summary.totalDiscoveries > 0) impactItems.push({icon: '>>>', label: 'DISCOVERIES', value: summary.totalDiscoveries, color: pc.green});
|
|
350
|
+
if (summary.totalFixes > 0) impactItems.push({icon: '>>>', label: 'FIXES', value: summary.totalFixes, color: pc.yellow});
|
|
351
|
+
if (summary.totalImprovements > 0) impactItems.push({icon: '>>>', label: 'IMPROVEMENTS', value: summary.totalImprovements, color: pc.blue});
|
|
352
|
+
if (summary.totalDocumentation > 0) impactItems.push({icon: '>>>', label: 'DOCUMENTATION', value: summary.totalDocumentation, color: pc.cyan});
|
|
353
|
+
if (summary.totalRefactors > 0) impactItems.push({icon: '>>>', label: 'REFACTORS', value: summary.totalRefactors, color: pc.magenta});
|
|
354
|
+
|
|
355
|
+
section('MDDD IMPACT');
|
|
356
|
+
|
|
357
|
+
if (impactItems.length > 0) {
|
|
358
|
+
for (var i = 0; i < impactItems.length; i++) {
|
|
359
|
+
var it = impactItems[i];
|
|
360
|
+
var label = pc.bold(it.color(it.label.padEnd(16)));
|
|
361
|
+
var val = pc.bold(it.color(String(it.value)));
|
|
362
|
+
console.log(boxLine(' ' + it.icon + ' ' + label + ' ' + val));
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
console.log(boxLine(pc.gray(' (no impact metrics yet)')));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
console.log(boxBot());
|
|
369
|
+
console.log();
|
|
370
|
+
|
|
371
|
+
// ── CRITICAL POINTS ──
|
|
372
|
+
if (summary.criticalPoints.length > 0) {
|
|
373
|
+
var draftByDomain = new Map();
|
|
374
|
+
var caoticoList = [];
|
|
375
|
+
var pendingList = [];
|
|
376
|
+
|
|
377
|
+
for (var j = 0; j < summary.criticalPoints.length; j++) {
|
|
378
|
+
var pt = summary.criticalPoints[j];
|
|
379
|
+
if (pt.indexOf('CAOTICO') !== -1 || pt.indexOf('CAÓTICO') !== -1) caoticoList.push(pt);
|
|
380
|
+
else if (pt.indexOf('pending tasks') !== -1) pendingList.push(pt);
|
|
381
|
+
else if (pt.indexOf('DRAFT') !== -1) {
|
|
382
|
+
var pts = pt.split(' — ');
|
|
383
|
+
var dom = pts[0].split('/')[0];
|
|
384
|
+
if (!draftByDomain.has(dom)) draftByDomain.set(dom, []);
|
|
385
|
+
draftByDomain.get(dom).push(pts[0]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
section('CRITICAL POINTS');
|
|
390
|
+
|
|
391
|
+
for (var k = 0; k < caoticoList.length; k++) {
|
|
392
|
+
var cp = caoticoList[k].split(' — ');
|
|
393
|
+
console.log(boxLine(' ' + pc.red(pc.bold('!!! ' + (cp[0] || '')))));
|
|
394
|
+
if (cp[1]) console.log(boxLine(' ' + pc.red(cp[1])));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
var maxShow = 5;
|
|
398
|
+
var showItems = pendingList.slice(0, maxShow);
|
|
399
|
+
var moreCount = pendingList.length - maxShow;
|
|
400
|
+
for (var m = 0; m < showItems.length; m++) {
|
|
401
|
+
var pp = showItems[m].split(' — ');
|
|
402
|
+
var tm = pp[1] ? pp[1].match(/(\d+) pending/) : null;
|
|
403
|
+
var pendingNum = tm ? tm[1] : '0';
|
|
404
|
+
var filePath = pp[0] || '';
|
|
405
|
+
var maxPathLen = W - 22;
|
|
406
|
+
if (filePath.length > maxPathLen) filePath = '...' + filePath.slice(-(maxPathLen - 3));
|
|
407
|
+
console.log(boxLine(' ' + pc.yellow(pc.bold('PENDING ' + filePath)) + ' ' + pc.bold(pc.yellow(pendingNum))));
|
|
408
|
+
}
|
|
409
|
+
if (moreCount > 0) {
|
|
410
|
+
console.log(boxLine(pc.gray(' ... and ' + moreCount + ' more specs')));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
var draftCount = 0;
|
|
414
|
+
for (var d of draftByDomain.values()) draftCount += d.length;
|
|
415
|
+
if (draftCount > 0) {
|
|
416
|
+
var domainArr = [];
|
|
417
|
+
for (var entry of draftByDomain) {
|
|
418
|
+
domainArr.push({domain: entry[0], count: entry[1].length});
|
|
419
|
+
}
|
|
420
|
+
domainArr.sort(function(a, b) { return b.count - a.count; });
|
|
421
|
+
var domainStr = domainArr.map(function(e) {
|
|
422
|
+
return pc.bold(pc.cyan(e.domain)) + pc.gray('(' + e.count + ')');
|
|
423
|
+
}).join(' ');
|
|
424
|
+
console.log(boxLine(pc.bold(pc.cyan(' DRAFT: ' + String(draftCount) + ' specs'))));
|
|
425
|
+
// Truncate domain summary if too long
|
|
426
|
+
if (domainStr.length > W - 12) {
|
|
427
|
+
domainStr = domainStr.slice(0, W - 15) + '...';
|
|
428
|
+
}
|
|
429
|
+
console.log(boxLine(' ' + domainStr));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
console.log(boxBot());
|
|
433
|
+
console.log();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── Footer ───────────────────────────────────────
|
|
437
|
+
var versions = summary.specs.map(function(s) { return s.version; }).filter(function(v) { return v !== 'unknown'; });
|
|
438
|
+
var versionRange;
|
|
439
|
+
if (versions.length > 0) {
|
|
440
|
+
var sorted = versions.sort(function(a, b) {
|
|
441
|
+
var ap = a.split('.').map(Number);
|
|
442
|
+
var bp = b.split('.').map(Number);
|
|
443
|
+
if (ap[0] !== bp[0]) return ap[0] - bp[0];
|
|
444
|
+
if (ap[1] !== bp[1]) return ap[1] - bp[1];
|
|
445
|
+
return ap[2] - bp[2];
|
|
446
|
+
});
|
|
447
|
+
versionRange = 'v' + sorted[0] + ' -> v' + sorted[sorted.length - 1];
|
|
448
|
+
} else {
|
|
449
|
+
versionRange = 'no version data';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.log(pc.gray(' ' + '-'.repeat(W)));
|
|
453
|
+
console.log(pc.gray(' MDDD Protocol · ' + summary.specs.length + ' specs · ' + versionRange));
|
|
454
|
+
console.log(pc.gray(' Generated: ' + new Date().toISOString().split('T')[0]));
|
|
455
|
+
console.log();
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Executes the `md status` command.
|
|
459
|
+
* @param {{ findSpecs: (rootDir: string) => string[] }} specFinder
|
|
460
|
+
* @returns {Promise<DashboardSummary>}
|
|
461
|
+
*/
|
|
462
|
+
export async function execute(specFinder) {
|
|
463
|
+
const specs = specFinder.findSpecs(process.cwd());
|
|
464
|
+
|
|
465
|
+
if (specs.length === 0) {
|
|
466
|
+
const emptySummary = {
|
|
467
|
+
totalSpecs: 0,
|
|
468
|
+
coesoCount: 0,
|
|
469
|
+
caoticoCount: 0,
|
|
470
|
+
unclassifiedCount: 0,
|
|
471
|
+
totalTasks: 0,
|
|
472
|
+
completedTasks: 0,
|
|
473
|
+
pendingTasks: 0,
|
|
474
|
+
totalChanges: 0,
|
|
475
|
+
totalDiscoveries: 0,
|
|
476
|
+
totalFixes: 0,
|
|
477
|
+
totalImprovements: 0,
|
|
478
|
+
totalDocumentation: 0,
|
|
479
|
+
totalRefactors: 0,
|
|
480
|
+
totalMajors: 0,
|
|
481
|
+
totalMinors: 0,
|
|
482
|
+
totalPatches: 0,
|
|
483
|
+
criticalPoints: [],
|
|
484
|
+
specs: []
|
|
485
|
+
};
|
|
486
|
+
printDashboard(emptySummary);
|
|
487
|
+
return emptySummary;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Analyze each spec
|
|
491
|
+
const metricsList = specs.map(spec => analyzeSpec(spec));
|
|
492
|
+
|
|
493
|
+
// Aggregate into summary
|
|
494
|
+
/** @type {DashboardSummary} */
|
|
495
|
+
const summary = {
|
|
496
|
+
totalSpecs: metricsList.length,
|
|
497
|
+
coesoCount: metricsList.filter(m => m.classification === 'Coeso').length,
|
|
498
|
+
caoticoCount: metricsList.filter(m => m.classification === 'Caótico').length,
|
|
499
|
+
unclassifiedCount: metricsList.filter(m => !m.classification).length,
|
|
500
|
+
totalTasks: metricsList.reduce((acc, m) => acc + m.tasksTotal, 0),
|
|
501
|
+
completedTasks: metricsList.reduce((acc, m) => acc + m.tasksCompleted, 0),
|
|
502
|
+
pendingTasks: metricsList.reduce((acc, m) => acc + m.tasksPending, 0),
|
|
503
|
+
totalChanges: metricsList.reduce((acc, m) => acc + m.totalChanges, 0),
|
|
504
|
+
totalDiscoveries: metricsList.reduce((acc, m) => acc + m.changeTypes.discovery, 0),
|
|
505
|
+
totalFixes: metricsList.reduce((acc, m) => acc + m.changeTypes.fix, 0),
|
|
506
|
+
totalImprovements: metricsList.reduce((acc, m) => acc + m.changeTypes.improvement, 0),
|
|
507
|
+
totalDocumentation: metricsList.reduce((acc, m) => acc + m.changeTypes.documentation, 0),
|
|
508
|
+
totalRefactors: metricsList.reduce((acc, m) => acc + m.changeTypes.refactor, 0),
|
|
509
|
+
totalMajors: metricsList.reduce((acc, m) => acc + m.changeBreakdown.major, 0),
|
|
510
|
+
totalMinors: metricsList.reduce((acc, m) => acc + m.changeBreakdown.minor, 0),
|
|
511
|
+
totalPatches: metricsList.reduce((acc, m) => acc + m.changeBreakdown.patch, 0),
|
|
512
|
+
criticalPoints: [],
|
|
513
|
+
specs: metricsList
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// Build critical points
|
|
517
|
+
for (const m of metricsList) {
|
|
518
|
+
if (m.totalChanges === 0) {
|
|
519
|
+
summary.criticalPoints.push(`${m.relativePath} — No Audit History (0 changes)`);
|
|
520
|
+
}
|
|
521
|
+
if (m.tasksPending > 5) {
|
|
522
|
+
summary.criticalPoints.push(`${m.relativePath} — ⚠️ ${m.tasksPending} pending tasks`);
|
|
523
|
+
}
|
|
524
|
+
if (m.classification === 'Caótico') {
|
|
525
|
+
summary.criticalPoints.push(`${m.relativePath} — Classified as CAÓTICO`);
|
|
526
|
+
}
|
|
527
|
+
if (m.status === 'draft') {
|
|
528
|
+
summary.criticalPoints.push(`${m.relativePath} — Still in DRAFT status`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
printDashboard(summary);
|
|
533
|
+
return summary;
|
|
534
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# status — Specification
|
|
2
|
+
|
|
3
|
+
**SPEC_VERSION:** v1.0.0 — stable
|
|
4
|
+
**Classification:** Coeso
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
The `md status` command scans all `.spec.md` files in the project, extracts metrics from each spec's **Tasks** checklist and **Audit History** table, and generates a beautiful color-coded dashboard showing the MDDD protocol coverage: discoveries, fixes, improvements, documentation, refactors, task completion rates, and version evolution.
|
|
9
|
+
|
|
10
|
+
Reuses `SpecFinderService.findSpecs()` to discover specs — no custom file walking.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Behavioral Flow (Mermaid)
|
|
15
|
+
|
|
16
|
+
```mermaid
|
|
17
|
+
%% @spec-version v1.0.0
|
|
18
|
+
stateDiagram-v2
|
|
19
|
+
[*] --> FindSpecs: specFinder.findSpecs(process.cwd())
|
|
20
|
+
|
|
21
|
+
state FindSpecs {
|
|
22
|
+
[*] --> WalkTree: readdirSync + recursion
|
|
23
|
+
WalkTree --> CollectSpecs: filter *.spec.md
|
|
24
|
+
CollectSpecs --> [*]: return sorted string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
FindSpecs --> NoSpecs: specs.length === 0
|
|
28
|
+
NoSpecs --> PrintEmpty: pc.yellow warning
|
|
29
|
+
PrintEmpty --> [*]
|
|
30
|
+
|
|
31
|
+
FindSpecs --> AnalyzeLoop: for each spec
|
|
32
|
+
|
|
33
|
+
state AnalyzeSpec {
|
|
34
|
+
[*] --> ReadFile: read spec file
|
|
35
|
+
ReadFile --> ParseVersion: extract version
|
|
36
|
+
ParseVersion --> ParseStatus: detect draft or stable
|
|
37
|
+
ParseStatus --> ParseClassification: detect Coeso or Caótico
|
|
38
|
+
ParseClassification --> ParseTasks: count checklist items
|
|
39
|
+
ParseTasks --> ParseAudit: extract audit rows
|
|
40
|
+
ParseAudit --> ClassifyChanges: keyword analysis
|
|
41
|
+
ClassifyChanges --> [*]: return SpecMetrics
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
AnalyzeLoop --> AnalyzeSpec
|
|
45
|
+
AnalyzeSpec --> NextSpec: more specs remaining
|
|
46
|
+
NextSpec --> AnalyzeSpec
|
|
47
|
+
AnalyzeSpec --> Aggregate: all specs processed
|
|
48
|
+
|
|
49
|
+
state Aggregate {
|
|
50
|
+
[*] --> SumTasks: sum totals across specs
|
|
51
|
+
SumTasks --> SumChanges: sum MAJOR/MINOR/PATCH
|
|
52
|
+
SumChanges --> BuildCriticalPoints: detect anomalies
|
|
53
|
+
BuildCriticalPoints --> [*]: return DashboardSummary
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Aggregate --> PrintDashboard
|
|
57
|
+
|
|
58
|
+
state PrintDashboard {
|
|
59
|
+
[*] --> PrintHeader: render title box
|
|
60
|
+
PrintHeader --> PrintSpecs: coeso-caotico-unclassified
|
|
61
|
+
PrintSpecs --> PrintTasks: tasks with progress bar
|
|
62
|
+
PrintTasks --> PrintChanges: MAJOR-MINOR-PATCH
|
|
63
|
+
PrintChanges --> PrintImpact: discoveries-fixes-etc
|
|
64
|
+
PrintImpact --> PrintCritical: warnings highlighted
|
|
65
|
+
PrintCritical --> PrintFooter: timestamps and version
|
|
66
|
+
PrintFooter --> [*]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
PrintDashboard --> [*]: exit 0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Decision Matrix
|
|
75
|
+
|
|
76
|
+
### Primitive Factors
|
|
77
|
+
|
|
78
|
+
| Factor | Type | Allowed Values | Default |
|
|
79
|
+
| :--- | :--- | :--- | :--- |
|
|
80
|
+
| Specs found in project? | Binary | `✅ YES` / `❌ NO` | `❌ NO` |
|
|
81
|
+
| Has Tasks section? | Binary | `✅ YES` / `❌ NO` | `❌ NO` |
|
|
82
|
+
| Has Audit History table? | Binary | `✅ YES` / `❌ NO` | `❌ NO` |
|
|
83
|
+
| Has Coeso/Caótico classification? | Binary | `✅ YES` / `❌ NO` | `❌ NO` |
|
|
84
|
+
| Total specs > 0? | Binary | `✅ YES` / `❌ NO` | `❌ NO` |
|
|
85
|
+
|
|
86
|
+
### Resolution Table
|
|
87
|
+
|
|
88
|
+
| Specs found? | Has Tasks? | Has Audit? | Has Classif.? | Total > 0? | Proposed Action | Decision | Transition State |
|
|
89
|
+
| :---: | :---: | :---: | :---: | :---: | :--- | :---: | :--- |
|
|
90
|
+
| ❌ NO | - | - | - | ❌ NO | `PRINT_DASHBOARD` | ✅ ALLOW | `EMPTY` |
|
|
91
|
+
| ✅ YES | ❌ NO | ❌ NO | ❌ NO | ✅ YES | `PRINT_DASHBOARD` | ✅ ALLOW | `BASIC` |
|
|
92
|
+
| ✅ YES | ✅ YES | ❌ NO | ❌ NO | ✅ YES | `PRINT_DASHBOARD` | ✅ ALLOW | `TASKS_ONLY` |
|
|
93
|
+
| ✅ YES | ❌ NO | ✅ YES | ❌ NO | ✅ YES | `PRINT_DASHBOARD` | ✅ ALLOW | `AUDIT_ONLY` |
|
|
94
|
+
| ✅ YES | ✅ YES | ✅ YES | ❌ NO | ✅ YES | `PRINT_DASHBOARD` | ✅ ALLOW | `FULL` |
|
|
95
|
+
| ✅ YES | ✅ YES | ✅ YES | ✅ YES | ✅ YES | `PRINT_DASHBOARD` | ✅ ALLOW | `FULL_CLASSIFIED` |
|
|
96
|
+
|
|
97
|
+
> `-` = wildcard / any value matches.
|
|
98
|
+
|
|
99
|
+
### Resolution Rules (per MDDD protocol, section 3.3)
|
|
100
|
+
|
|
101
|
+
1. ALL columns must match the current system state.
|
|
102
|
+
2. If no row fully matches → `HaltWithConflict`.
|
|
103
|
+
3. If multiple rows match → `HaltWithConflict` (ambiguous).
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Tasks
|
|
108
|
+
|
|
109
|
+
Atomic checklist derived from the spec above:
|
|
110
|
+
|
|
111
|
+
- [x] Create `src/commands/status.js` with full analysis and dashboard logic
|
|
112
|
+
- [x] Create `src/commands/status.spec.md` (this file)
|
|
113
|
+
- [x] Edit `bin/cli.js` — register the `status` command
|
|
114
|
+
- [x] Edit `bin/cli.spec.md` — update topology, matrix, and audit history
|
|
115
|
+
- [x] Create `tests/commands/status.spec.js` with 5 test scenarios
|
|
116
|
+
- [x] Run tests: `node --test tests/commands/status.spec.js`
|
|
117
|
+
- [x] Run live: `node bin/cli.js status` and verify dashboard output
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Audit History
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary>Click to expand</summary>
|
|
125
|
+
|
|
126
|
+
| Date | Agent | Version | Change Summary |
|
|
127
|
+
| :--- | :--- | :---: | :--- |
|
|
128
|
+
| 2026-06-09 | Cline (md-edit) | v1.0.1 | **Fixed SPEC_VERSION header format** (added colon separator per template spec) and **added Classification: Coeso** field — resolves validation error where spec lacked required Coeso/Caótico classification that its own diagram and decision matrix reference. PATCH bump. |
|
|
129
|
+
| 2026-06-08 | Cline (md-edit) | v1.0.0 | **Spec created from template.** Behavioral flow covers the full pipeline: FindSpecs → AnalyzeLoop → Aggregate → PrintDashboard. Decision Matrix covers 5 primitive factors with 6 resolution rows. Tasks checklist mirrors implementation order. All 7 tasks completed and verified — 5/5 tests passing on `node --test tests/commands/status.spec.js`. Validated against 71-spec project (Appfy). Status promoted to **stable**. |
|
|
130
|
+
|
|
131
|
+
</details>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# FileSystemService — Specification
|
|
2
2
|
|
|
3
|
-
**SPEC_VERSION
|
|
3
|
+
**SPEC_VERSION:** v1.0.0 — stable
|
|
4
|
+
**Classification:** Coeso
|
|
4
5
|
|
|
5
6
|
## Overview
|
|
6
7
|
|
|
@@ -95,6 +96,7 @@ stateDiagram-v2
|
|
|
95
96
|
|
|
96
97
|
| Date | Agent | Version | Change Summary |
|
|
97
98
|
| :--- | :--- | :---: | :--- |
|
|
99
|
+
| 2026-06-09 | Cline (md-audit) | v1.0.1 | **Fixed SPEC_VERSION header format** (added colon separator per template spec) and **added Classification: Coeso** — aligns with the existing code quality (DI-ready, modular, testable). PATCH bump. |
|
|
98
100
|
| 2026-05-28 | Cline (md-audit) | v1.0.0 | **Spec created by md-audit.** Reverse-engineered from `src/services/FileSystemService.js` (38 lines). Code classified as **Clean / DI-ready**. All 7 methods documented with primitive factor analysis. No modifications to production code. |
|
|
99
101
|
|
|
100
102
|
</details>
|