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.
@@ -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: v1.0.0 — stable**
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>
@@ -21,7 +21,7 @@ export class InitService {
21
21
  * @returns {Promise<void>}
22
22
  */
23
23
  async createSystemPrompt(promptContent) {
24
- await this.#fs.writeFile('system_prompt.md', promptContent);
24
+ await this.#fs.writeFile('AGENTS.md', promptContent);
25
25
  }
26
26
 
27
27
  /**