refacil-sdd-ai 5.2.2 → 5.3.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/NOTICE.md +46 -0
- package/README.md +209 -42
- package/agents/auditor.md +46 -0
- package/agents/debugger.md +41 -1
- package/agents/implementer.md +76 -10
- package/agents/investigator.md +36 -0
- package/agents/proposer.md +46 -2
- package/agents/tester.md +45 -8
- package/agents/validator.md +67 -13
- package/bin/cli.js +428 -83
- package/bin/postinstall.js +20 -0
- package/lib/bus/broker.js +121 -3
- package/lib/bus/spawn.js +189 -121
- package/lib/check-review.js +102 -0
- package/lib/codegraph-telemetry.js +135 -0
- package/lib/codegraph.js +273 -0
- package/lib/commands/autopilot.js +120 -0
- package/lib/commands/bus.js +29 -36
- package/lib/commands/compact.js +185 -46
- package/lib/commands/read-spec.js +352 -0
- package/lib/commands/sdd.js +429 -44
- package/lib/compact-guidance.js +122 -77
- package/lib/config.js +136 -0
- package/lib/global-paths.js +56 -20
- package/lib/hooks.js +32 -4
- package/lib/ide-detection.js +1 -1
- package/lib/ignore-files.js +5 -1
- package/lib/installer.js +202 -19
- package/lib/kapso.js +241 -0
- package/lib/methodology-migration-pending.js +13 -0
- package/lib/open-browser.js +32 -0
- package/lib/opencode-migrate.js +148 -0
- package/lib/opencode-plugin/index.js +84 -104
- package/lib/opencode-plugin/rules.js +236 -0
- package/lib/project-root.js +154 -0
- package/lib/repo-ide-sync.js +5 -0
- package/lib/spec-reader/lang.js +72 -0
- package/lib/spec-reader/md-parser.js +299 -0
- package/lib/spec-reader/session.js +139 -0
- package/lib/spec-reader/ui/app.js +685 -0
- package/lib/spec-reader/ui/index.html +59 -0
- package/lib/spec-reader/ui/mixed-lang.js +200 -0
- package/lib/spec-reader/ui/model-cache.js +117 -0
- package/lib/spec-reader/ui/style.css +294 -0
- package/lib/spec-reader/ui/supertonic-helper.js +565 -0
- package/lib/spec-sync.js +258 -0
- package/lib/test-scope.js +713 -0
- package/lib/testing-policy-sync.js +14 -2
- package/package.json +6 -3
- package/skills/apply/SKILL.md +39 -64
- package/skills/archive/SKILL.md +74 -48
- package/skills/ask/SKILL.md +43 -8
- package/skills/autopilot/SKILL.md +476 -0
- package/skills/bug/SKILL.md +52 -53
- package/skills/explore/SKILL.md +48 -1
- package/skills/guide/SKILL.md +31 -13
- package/skills/inbox/SKILL.md +9 -0
- package/skills/join/SKILL.md +1 -1
- package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
- package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
- package/skills/prereqs/SKILL.md +1 -1
- package/skills/propose/SKILL.md +74 -19
- package/skills/read-spec/SKILL.md +76 -0
- package/skills/reply/SKILL.md +42 -9
- package/skills/review/SKILL.md +63 -25
- package/skills/review/checklist.md +2 -2
- package/skills/say/SKILL.md +40 -4
- package/skills/setup/SKILL.md +59 -5
- package/skills/setup/troubleshooting.md +11 -3
- package/skills/stats/SKILL.md +157 -0
- package/skills/test/SKILL.md +35 -10
- package/skills/up-code/SKILL.md +20 -13
- package/skills/update/SKILL.md +32 -1
- package/skills/verify/SKILL.md +78 -41
- package/templates/compact-guidance.md +10 -0
- package/templates/methodology-guide.md +5 -0
package/lib/commands/sdd.js
CHANGED
|
@@ -3,21 +3,9 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
-
const { loadBranchConfigWithSources, parseYaml, readConfigFile, SUPPORTED_LANGUAGES } = require('../config');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let dir = process.cwd();
|
|
10
|
-
const { root } = path.parse(dir);
|
|
11
|
-
while (dir !== root) {
|
|
12
|
-
if (fs.existsSync(path.join(dir, 'refacil-sdd')) || fs.existsSync(path.join(dir, '.git'))) {
|
|
13
|
-
return dir;
|
|
14
|
-
}
|
|
15
|
-
const parent = path.dirname(dir);
|
|
16
|
-
if (parent === dir) break;
|
|
17
|
-
dir = parent;
|
|
18
|
-
}
|
|
19
|
-
return process.cwd();
|
|
20
|
-
}
|
|
6
|
+
const { loadBranchConfigWithSources, parseYaml, readConfigFile, SUPPORTED_LANGUAGES, CODEGRAPH_MODES } = require('../config');
|
|
7
|
+
const { findProjectRoot } = require('../project-root');
|
|
8
|
+
const { collectSpecSourceFiles } = require('../spec-sync');
|
|
21
9
|
|
|
22
10
|
// --- Helpers ---
|
|
23
11
|
|
|
@@ -42,6 +30,11 @@ function parseArgs(argv) {
|
|
|
42
30
|
return args;
|
|
43
31
|
}
|
|
44
32
|
|
|
33
|
+
/** Bug-fix changes from refacil:bug use fix-* and only carry summary.md (no specs/). */
|
|
34
|
+
function isBugFixChangeName(name) {
|
|
35
|
+
return typeof name === 'string' && name.startsWith('fix-');
|
|
36
|
+
}
|
|
37
|
+
|
|
45
38
|
function validateChangeName(name) {
|
|
46
39
|
if (!name || name.trim() === '') {
|
|
47
40
|
return { valid: false, reason: 'El nombre del cambio no puede estar vacío.' };
|
|
@@ -64,6 +57,59 @@ function validateChangeName(name) {
|
|
|
64
57
|
return { valid: true };
|
|
65
58
|
}
|
|
66
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Looks up a change name inside the archive directory.
|
|
62
|
+
* Archive entry format: refacil-sdd/changes/archive/YYYY-MM-DD-<changeName>/
|
|
63
|
+
*
|
|
64
|
+
* Rules:
|
|
65
|
+
* - Matches by exact suffix `-<name>` (not substring).
|
|
66
|
+
* - Validates prefix matches /^\d{4}-\d{2}-\d{2}-/.
|
|
67
|
+
* - Returns most recent directory path (sorted descending by date prefix).
|
|
68
|
+
* - If two entries share the same suffix (same name, different dates) → returns most recent.
|
|
69
|
+
* - If the name is ambiguous (two entries with truly different names both match) → returns null and emits error.
|
|
70
|
+
* - Returns null if no match or archive dir doesn't exist.
|
|
71
|
+
*/
|
|
72
|
+
function resolveArchivedChangeName(projectRoot, name) {
|
|
73
|
+
if (!name || typeof name !== 'string') return null;
|
|
74
|
+
|
|
75
|
+
const archiveDir = path.join(projectRoot, 'refacil-sdd', 'changes', 'archive');
|
|
76
|
+
if (!fs.existsSync(archiveDir)) return null;
|
|
77
|
+
|
|
78
|
+
let entries;
|
|
79
|
+
try {
|
|
80
|
+
entries = fs.readdirSync(archiveDir, { withFileTypes: true })
|
|
81
|
+
.filter((e) => e.isDirectory())
|
|
82
|
+
.map((e) => e.name);
|
|
83
|
+
} catch (_) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Date prefix pattern
|
|
88
|
+
const datePrefix = /^\d{4}-\d{2}-\d{2}-/;
|
|
89
|
+
|
|
90
|
+
// Filter by exact suffix match AND valid date prefix
|
|
91
|
+
const suffix = `-${name}`;
|
|
92
|
+
const matches = entries.filter((entry) => {
|
|
93
|
+
if (!datePrefix.test(entry)) return false;
|
|
94
|
+
return entry.endsWith(suffix);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (matches.length === 0) return null;
|
|
98
|
+
|
|
99
|
+
// Check for ambiguity: extract the clean name (strip date prefix) and verify all match the same name
|
|
100
|
+
const cleanNames = new Set(matches.map((entry) => entry.replace(datePrefix, '')));
|
|
101
|
+
if (cleanNames.size > 1) {
|
|
102
|
+
// Multiple distinct clean names matched — truly ambiguous
|
|
103
|
+
console.error(`Nombre de cambio archivado ambiguo: '${name}'. Coincidencias: ${matches.join(', ')}`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Sort descending by date prefix so most recent is first
|
|
108
|
+
matches.sort((a, b) => b.localeCompare(a));
|
|
109
|
+
|
|
110
|
+
return path.join(archiveDir, matches[0]);
|
|
111
|
+
}
|
|
112
|
+
|
|
67
113
|
function resolveExistingChangeName(projectRoot, inputName) {
|
|
68
114
|
if (!inputName || typeof inputName !== 'string') {
|
|
69
115
|
return { ok: false, reason: 'El nombre del cambio no puede estar vacío.' };
|
|
@@ -179,6 +225,38 @@ function cmdNewChange(argv, projectRoot) {
|
|
|
179
225
|
console.log(`Cambio '${name}' creado en refacil-sdd/changes/${name}/`);
|
|
180
226
|
}
|
|
181
227
|
|
|
228
|
+
function cmdSyncSpec(argv, projectRoot) {
|
|
229
|
+
const args = parseArgs(argv);
|
|
230
|
+
const rawName = args._positional[0];
|
|
231
|
+
|
|
232
|
+
autoMigrateOpenspec(projectRoot);
|
|
233
|
+
const resolved = resolveExistingChangeName(projectRoot, rawName);
|
|
234
|
+
if (!resolved.ok) {
|
|
235
|
+
console.error(resolved.reason);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
const name = resolved.name;
|
|
239
|
+
const fromArchive = args['from-archive'] === true;
|
|
240
|
+
|
|
241
|
+
if (!fromArchive) {
|
|
242
|
+
const sourceDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
|
|
243
|
+
if (!fs.existsSync(sourceDir)) {
|
|
244
|
+
console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const { syncSpecToCatalog } = require('../spec-sync');
|
|
251
|
+
const result = syncSpecToCatalog(projectRoot, name, { fromArchive });
|
|
252
|
+
const rel = path.relative(projectRoot, result.specPath).replace(/\\/g, '/');
|
|
253
|
+
console.log(` Spec sincronizado: ${rel} (${result.criteriaCount} criterios, idioma: ${result.language})`);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(` Error sincronizando spec: ${err.message}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
182
260
|
function cmdArchive(argv, projectRoot) {
|
|
183
261
|
const args = parseArgs(argv);
|
|
184
262
|
const rawName = args._positional[0];
|
|
@@ -203,6 +281,20 @@ function cmdArchive(argv, projectRoot) {
|
|
|
203
281
|
process.exit(1);
|
|
204
282
|
}
|
|
205
283
|
|
|
284
|
+
if (!isBugFixChangeName(name)) {
|
|
285
|
+
try {
|
|
286
|
+
const { syncSpecToCatalog } = require('../spec-sync');
|
|
287
|
+
const result = syncSpecToCatalog(projectRoot, name);
|
|
288
|
+
const rel = path.relative(projectRoot, result.specPath).replace(/\\/g, '/');
|
|
289
|
+
console.log(` Spec sincronizado: ${rel} (idioma: ${result.language}, ${result.criteriaCount} criterios)`);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error(` Error sincronizando spec antes de archivar: ${err.message}`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
console.log(' Bug fix (fix-*): omitiendo sincronización de spec (use refacil:archive para documentar en refacil-sdd/specs/)');
|
|
296
|
+
}
|
|
297
|
+
|
|
206
298
|
const date = new Date().toISOString().slice(0, 10);
|
|
207
299
|
const archiveDir = path.join(projectRoot, 'refacil-sdd', 'changes', 'archive');
|
|
208
300
|
const destDir = path.join(archiveDir, `${date}-${name}`);
|
|
@@ -212,10 +304,6 @@ function cmdArchive(argv, projectRoot) {
|
|
|
212
304
|
process.exit(1);
|
|
213
305
|
}
|
|
214
306
|
|
|
215
|
-
// Delete memory.yaml before archiving (CA-18)
|
|
216
|
-
const memoryFile = path.join(sourceDir, 'memory.yaml');
|
|
217
|
-
if (fs.existsSync(memoryFile)) fs.unlinkSync(memoryFile);
|
|
218
|
-
|
|
219
307
|
fs.mkdirSync(archiveDir, { recursive: true });
|
|
220
308
|
fs.renameSync(sourceDir, destDir);
|
|
221
309
|
|
|
@@ -389,6 +477,7 @@ function cmdClearReviewFails(argv, projectRoot) {
|
|
|
389
477
|
function cmdList(argv, projectRoot) {
|
|
390
478
|
const args = parseArgs(argv);
|
|
391
479
|
const wantJson = args.json === true;
|
|
480
|
+
const includeArchived = args['include-archived'] === true;
|
|
392
481
|
|
|
393
482
|
autoMigrateOpenspec(projectRoot);
|
|
394
483
|
|
|
@@ -410,17 +499,55 @@ function cmdList(argv, projectRoot) {
|
|
|
410
499
|
return { name: e.name, reviewPassed };
|
|
411
500
|
});
|
|
412
501
|
|
|
502
|
+
// Build archived entries when --include-archived is requested
|
|
503
|
+
const archivedResult = [];
|
|
504
|
+
if (includeArchived) {
|
|
505
|
+
const archiveDir = path.join(changesDir, 'archive');
|
|
506
|
+
if (fs.existsSync(archiveDir)) {
|
|
507
|
+
const datePrefix = /^\d{4}-\d{2}-\d{2}-/;
|
|
508
|
+
try {
|
|
509
|
+
const archiveEntries = fs.readdirSync(archiveDir, { withFileTypes: true })
|
|
510
|
+
.filter((e) => e.isDirectory() && datePrefix.test(e.name));
|
|
511
|
+
for (const e of archiveEntries) {
|
|
512
|
+
const dateMatch = e.name.match(/^(\d{4}-\d{2}-\d{2})-/);
|
|
513
|
+
const archivedDate = dateMatch ? dateMatch[1] : null;
|
|
514
|
+
const cleanName = e.name.replace(datePrefix, '');
|
|
515
|
+
archivedResult.push({ name: cleanName, reviewPassed: null, archived: true, archivedDate });
|
|
516
|
+
}
|
|
517
|
+
// Sort descending by date prefix (most recent first)
|
|
518
|
+
archivedResult.sort((a, b) => {
|
|
519
|
+
const da = a.archivedDate || '';
|
|
520
|
+
const db = b.archivedDate || '';
|
|
521
|
+
return db.localeCompare(da);
|
|
522
|
+
});
|
|
523
|
+
} catch (_) {}
|
|
524
|
+
}
|
|
525
|
+
// If archive dir doesn't exist, gracefully produce empty list (exit 0)
|
|
526
|
+
}
|
|
527
|
+
|
|
413
528
|
if (wantJson) {
|
|
414
|
-
|
|
529
|
+
const combined = result.concat(archivedResult);
|
|
530
|
+
process.stdout.write(JSON.stringify(combined) + '\n');
|
|
415
531
|
} else {
|
|
416
|
-
if (result.length === 0) {
|
|
532
|
+
if (result.length === 0 && archivedResult.length === 0) {
|
|
417
533
|
console.log('Sin cambios activos.');
|
|
418
534
|
return;
|
|
419
535
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
536
|
+
if (result.length > 0) {
|
|
537
|
+
console.log('Cambios activos en refacil-sdd/changes/:');
|
|
538
|
+
for (const item of result) {
|
|
539
|
+
const badge = item.reviewPassed ? '[reviewed]' : '[pending-review]';
|
|
540
|
+
console.log(` ${item.name} ${badge}`);
|
|
541
|
+
}
|
|
542
|
+
} else if (!includeArchived) {
|
|
543
|
+
console.log('Sin cambios activos.');
|
|
544
|
+
}
|
|
545
|
+
if (includeArchived && archivedResult.length > 0) {
|
|
546
|
+
if (result.length > 0) console.log('');
|
|
547
|
+
console.log('Cambios archivados en refacil-sdd/changes/archive/:');
|
|
548
|
+
for (const item of archivedResult) {
|
|
549
|
+
console.log(` ${item.name} [archived] (${item.archivedDate || '?'})`);
|
|
550
|
+
}
|
|
424
551
|
}
|
|
425
552
|
}
|
|
426
553
|
}
|
|
@@ -454,16 +581,7 @@ function cmdStatus(argv, projectRoot) {
|
|
|
454
581
|
const hasDesign = fs.existsSync(path.join(changeDir, 'design.md'));
|
|
455
582
|
const hasTasks = fs.existsSync(path.join(changeDir, 'tasks.md'));
|
|
456
583
|
|
|
457
|
-
|
|
458
|
-
let hasSpecs = false;
|
|
459
|
-
const specsMd = path.join(changeDir, 'specs.md');
|
|
460
|
-
const specsDir = path.join(changeDir, 'specs');
|
|
461
|
-
if (fs.existsSync(specsMd)) {
|
|
462
|
-
hasSpecs = true;
|
|
463
|
-
} else if (fs.existsSync(specsDir) && fs.statSync(specsDir).isDirectory()) {
|
|
464
|
-
const mdFiles = fs.readdirSync(specsDir).filter((f) => f.endsWith('.md'));
|
|
465
|
-
hasSpecs = mdFiles.length > 0;
|
|
466
|
-
}
|
|
584
|
+
const hasSpecs = collectSpecSourceFiles(changeDir).length > 0;
|
|
467
585
|
|
|
468
586
|
const artifacts = {
|
|
469
587
|
proposal: hasProposal,
|
|
@@ -485,7 +603,7 @@ function cmdStatus(argv, projectRoot) {
|
|
|
485
603
|
const reviewPassed = fs.existsSync(path.join(changeDir, '.review-passed'));
|
|
486
604
|
|
|
487
605
|
const ready = {
|
|
488
|
-
forApply: artifacts.proposal && artifacts.tasks,
|
|
606
|
+
forApply: artifacts.proposal && artifacts.design && artifacts.tasks && artifacts.specs,
|
|
489
607
|
forArchive: reviewPassed && taskStats.total > 0 && taskStats.pending === 0,
|
|
490
608
|
};
|
|
491
609
|
|
|
@@ -653,11 +771,12 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
653
771
|
const rawBaseBranch = args['base-branch'];
|
|
654
772
|
const rawProtectedBranches = args['protected-branches'];
|
|
655
773
|
const rawArtifactLanguage = args['artifact-language'];
|
|
774
|
+
const rawCodegraph = args['codegraph'];
|
|
656
775
|
|
|
657
776
|
// CR-03: no flags provided
|
|
658
|
-
if (rawBaseBranch === undefined && rawProtectedBranches === undefined && rawArtifactLanguage === undefined) {
|
|
659
|
-
console.error('Uso: refacil-sdd-ai sdd write-config [--global] [--base-branch <branch>] [--protected-branches <csv>] [--artifact-language <language>]');
|
|
660
|
-
console.error('Debe especificar al menos --base-branch, --protected-branches
|
|
777
|
+
if (rawBaseBranch === undefined && rawProtectedBranches === undefined && rawArtifactLanguage === undefined && rawCodegraph === undefined) {
|
|
778
|
+
console.error('Uso: refacil-sdd-ai sdd write-config [--global] [--base-branch <branch>] [--protected-branches <csv>] [--artifact-language <language>] [--codegraph <mode>]');
|
|
779
|
+
console.error('Debe especificar al menos --base-branch, --protected-branches, --artifact-language o --codegraph.');
|
|
661
780
|
process.exit(1);
|
|
662
781
|
}
|
|
663
782
|
|
|
@@ -689,6 +808,18 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
689
808
|
}
|
|
690
809
|
}
|
|
691
810
|
|
|
811
|
+
// --codegraph: must be a valid CODEGRAPH_MODES value
|
|
812
|
+
if (rawCodegraph !== undefined) {
|
|
813
|
+
if (typeof rawCodegraph !== 'string' || rawCodegraph.trim() === '') {
|
|
814
|
+
console.error('Error: --codegraph no puede estar vacío.');
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
if (!CODEGRAPH_MODES.includes(rawCodegraph.trim())) {
|
|
818
|
+
console.error(`Error: --codegraph "${rawCodegraph.trim()}" no es un modo válido. Valores válidos: ${CODEGRAPH_MODES.join(', ')}.`);
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
692
823
|
const targetPath = isGlobal
|
|
693
824
|
? path.join(os.homedir(), '.refacil-sdd-ai', 'config.yaml')
|
|
694
825
|
: path.join(projectRoot, 'refacil-sdd', 'config.yaml');
|
|
@@ -707,6 +838,9 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
707
838
|
if (rawArtifactLanguage !== undefined) {
|
|
708
839
|
merged.artifactLanguage = rawArtifactLanguage.trim();
|
|
709
840
|
}
|
|
841
|
+
if (rawCodegraph !== undefined) {
|
|
842
|
+
merged.codegraphMode = rawCodegraph.trim();
|
|
843
|
+
}
|
|
710
844
|
|
|
711
845
|
// CA-03: no-op when all provided keys already match existing config (semantic comparison)
|
|
712
846
|
const isNoOp = Object.keys(existing).length > 0 &&
|
|
@@ -714,7 +848,8 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
714
848
|
(protectedBranchesList === undefined ||
|
|
715
849
|
(Array.isArray(existing.protectedBranches) &&
|
|
716
850
|
JSON.stringify(existing.protectedBranches.slice().sort()) === JSON.stringify(protectedBranchesList.slice().sort()))) &&
|
|
717
|
-
(rawArtifactLanguage === undefined || existing.artifactLanguage === rawArtifactLanguage.trim())
|
|
851
|
+
(rawArtifactLanguage === undefined || existing.artifactLanguage === rawArtifactLanguage.trim()) &&
|
|
852
|
+
(rawCodegraph === undefined || existing.codegraphMode === rawCodegraph.trim());
|
|
718
853
|
if (isNoOp) {
|
|
719
854
|
console.log(`Sin cambios: ${targetPath} ya tiene los valores indicados.`);
|
|
720
855
|
process.exit(0);
|
|
@@ -730,14 +865,234 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
730
865
|
console.log(`Configuración escrita en ${targetPath} (nivel: ${level})`);
|
|
731
866
|
}
|
|
732
867
|
|
|
868
|
+
function cmdTestScope(argv, projectRoot) {
|
|
869
|
+
const args = parseArgs(argv);
|
|
870
|
+
const wantJson = args.json === true;
|
|
871
|
+
|
|
872
|
+
const filesRaw = args.files || '';
|
|
873
|
+
const stackHint = args.stack || undefined;
|
|
874
|
+
const baselineCmd = args.baseline || '';
|
|
875
|
+
// Use the already-resolved projectRoot from handleSdd (via findProjectRoot()) so
|
|
876
|
+
// the CLI works correctly when invoked from a subdirectory within the monorepo.
|
|
877
|
+
const root = projectRoot || process.cwd();
|
|
878
|
+
|
|
879
|
+
// Parse CSV files — empty string or missing → empty array (CR-04: never fail on empty)
|
|
880
|
+
const files = filesRaw
|
|
881
|
+
? filesRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
882
|
+
: [];
|
|
883
|
+
|
|
884
|
+
const { testScope } = require('../test-scope');
|
|
885
|
+
const result = testScope({ files, stack: stackHint, baseline: baselineCmd, projectRoot: root });
|
|
886
|
+
|
|
887
|
+
if (wantJson) {
|
|
888
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
889
|
+
} else {
|
|
890
|
+
console.log(`testCommand: ${result.testCommand}`);
|
|
891
|
+
console.log(`files: ${result.files.join(', ') || '(none)'}`);
|
|
892
|
+
console.log(`fallback: ${result.fallback}`);
|
|
893
|
+
if (result.fallbackReason) console.log(`fallbackReason: ${result.fallbackReason}`);
|
|
894
|
+
}
|
|
895
|
+
// Always exit 0 (CR-04)
|
|
896
|
+
process.exit(0);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function cmdStats(argv, projectRoot) {
|
|
900
|
+
const args = parseArgs(argv);
|
|
901
|
+
const rawName = args._positional[0];
|
|
902
|
+
const wantJson = args.json === true;
|
|
903
|
+
|
|
904
|
+
if (!rawName) {
|
|
905
|
+
console.error('Uso: refacil-sdd-ai sdd stats <nombre-cambio> [--json]');
|
|
906
|
+
process.exit(1);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
autoMigrateOpenspec(projectRoot);
|
|
910
|
+
const resolved = resolveExistingChangeName(projectRoot, rawName);
|
|
911
|
+
if (!resolved.ok) {
|
|
912
|
+
console.error(resolved.reason);
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
const name = resolved.name;
|
|
916
|
+
|
|
917
|
+
let changeDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
|
|
918
|
+
let isArchived = false;
|
|
919
|
+
let archivedDate = null;
|
|
920
|
+
|
|
921
|
+
if (!fs.existsSync(changeDir)) {
|
|
922
|
+
// Fallback: try to find in archive
|
|
923
|
+
const archivedPath = resolveArchivedChangeName(projectRoot, name);
|
|
924
|
+
if (archivedPath) {
|
|
925
|
+
changeDir = archivedPath;
|
|
926
|
+
isArchived = true;
|
|
927
|
+
// Extract YYYY-MM-DD from the directory basename
|
|
928
|
+
const baseName = path.basename(archivedPath);
|
|
929
|
+
const dateMatch = baseName.match(/^(\d{4}-\d{2}-\d{2})-/);
|
|
930
|
+
archivedDate = dateMatch ? dateMatch[1] : null;
|
|
931
|
+
} else {
|
|
932
|
+
console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/ ni en el archivo.`);
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// --- Read memory.yaml ---
|
|
938
|
+
const memoryPath = path.join(changeDir, 'memory.yaml');
|
|
939
|
+
let memory = {};
|
|
940
|
+
if (fs.existsSync(memoryPath)) {
|
|
941
|
+
try {
|
|
942
|
+
memory = parseYaml(fs.readFileSync(memoryPath, 'utf8')) || {};
|
|
943
|
+
} catch (_) {
|
|
944
|
+
memory = {};
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const testCommand = memory.testCommand || memory.commandsRun || null;
|
|
948
|
+
const lastStep = memory.lastStep || null;
|
|
949
|
+
const criteriaRun = Array.isArray(memory.criteriaRun) ? memory.criteriaRun : [];
|
|
950
|
+
|
|
951
|
+
// --- Read .review-passed ---
|
|
952
|
+
const reviewPassedPath = path.join(changeDir, '.review-passed');
|
|
953
|
+
let reviewDate = null;
|
|
954
|
+
let reviewVerdict = null;
|
|
955
|
+
let reviewFailCount = 0;
|
|
956
|
+
if (fs.existsSync(reviewPassedPath)) {
|
|
957
|
+
try {
|
|
958
|
+
const rp = JSON.parse(fs.readFileSync(reviewPassedPath, 'utf8'));
|
|
959
|
+
reviewDate = rp.date || null;
|
|
960
|
+
reviewVerdict = rp.verdict || null;
|
|
961
|
+
reviewFailCount = rp.failCount || 0;
|
|
962
|
+
} catch (_) {}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// --- Determine change start date from proposal.md mtime (fallback: summary.md) ---
|
|
966
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
967
|
+
let startDate = null;
|
|
968
|
+
if (fs.existsSync(proposalPath)) {
|
|
969
|
+
try {
|
|
970
|
+
const stat = fs.statSync(proposalPath);
|
|
971
|
+
startDate = stat.mtime;
|
|
972
|
+
} catch (_) {}
|
|
973
|
+
}
|
|
974
|
+
if (!startDate) {
|
|
975
|
+
const summaryPath = path.join(changeDir, 'summary.md');
|
|
976
|
+
if (fs.existsSync(summaryPath)) {
|
|
977
|
+
try {
|
|
978
|
+
const stat = fs.statSync(summaryPath);
|
|
979
|
+
startDate = stat.mtime;
|
|
980
|
+
} catch (_) {}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// --- Read compact.log filtered by change period ---
|
|
985
|
+
let compactStats = { totalRewrites: 0, totalSaved: 0, totalAlreadyCompact: 0, totalAlreadyCompactPotential: 0, totalEvents: 0 };
|
|
986
|
+
try {
|
|
987
|
+
const compactTelemetry = require('../compact/telemetry');
|
|
988
|
+
const allCompact = compactTelemetry.readLog ? compactTelemetry.readLog() : [];
|
|
989
|
+
const filtered = startDate ? allCompact.filter((e) => e.ts && new Date(e.ts) >= startDate) : allCompact;
|
|
990
|
+
let totalRewrites = 0, totalSaved = 0, totalAlreadyCompact = 0, totalAlreadyCompactPotential = 0;
|
|
991
|
+
for (const e of filtered) {
|
|
992
|
+
const evType = e.eventType || 'hook_rewrite';
|
|
993
|
+
if (evType === 'already_compact') {
|
|
994
|
+
totalAlreadyCompact++;
|
|
995
|
+
totalAlreadyCompactPotential += e.savedTokensEst || 0;
|
|
996
|
+
} else {
|
|
997
|
+
totalRewrites++;
|
|
998
|
+
totalSaved += e.savedTokensEst || 0;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
compactStats = { totalRewrites, totalSaved, totalAlreadyCompact, totalAlreadyCompactPotential, totalEvents: filtered.length };
|
|
1002
|
+
} catch (_) {}
|
|
1003
|
+
|
|
1004
|
+
// --- Read codegraph.log filtered by change period ---
|
|
1005
|
+
let codegraphStats = { totalEvents: 0, totalToolCalls: 0, totalTokensSaved: 0 };
|
|
1006
|
+
try {
|
|
1007
|
+
const cgTelemetry = require('../codegraph-telemetry');
|
|
1008
|
+
const allCg = cgTelemetry.readLog ? cgTelemetry.readLog() : [];
|
|
1009
|
+
const filtered = startDate ? allCg.filter((e) => e.ts && new Date(e.ts) >= startDate) : allCg;
|
|
1010
|
+
let totalEvents = 0, totalToolCalls = 0, totalTokensSaved = 0;
|
|
1011
|
+
for (const e of filtered) {
|
|
1012
|
+
totalEvents++;
|
|
1013
|
+
totalToolCalls += e.toolCallsCount || 0;
|
|
1014
|
+
totalTokensSaved += e.estimatedTokensSaved || 0;
|
|
1015
|
+
}
|
|
1016
|
+
codegraphStats = { totalEvents, totalToolCalls, totalTokensSaved };
|
|
1017
|
+
} catch (_) {}
|
|
1018
|
+
|
|
1019
|
+
// --- Build stats object ---
|
|
1020
|
+
const statsObj = {
|
|
1021
|
+
changeName: name,
|
|
1022
|
+
isArchived,
|
|
1023
|
+
archivedDate,
|
|
1024
|
+
startDate: startDate ? startDate.toISOString() : null,
|
|
1025
|
+
memory: {
|
|
1026
|
+
testCommand: testCommand || null,
|
|
1027
|
+
lastStep: lastStep || null,
|
|
1028
|
+
criteriaRun,
|
|
1029
|
+
},
|
|
1030
|
+
review: {
|
|
1031
|
+
passed: reviewDate !== null,
|
|
1032
|
+
verdict: reviewVerdict,
|
|
1033
|
+
date: reviewDate,
|
|
1034
|
+
failCount: reviewFailCount,
|
|
1035
|
+
},
|
|
1036
|
+
compact: {
|
|
1037
|
+
eventsInPeriod: compactStats.totalEvents,
|
|
1038
|
+
rewrites: compactStats.totalRewrites,
|
|
1039
|
+
estimatedTokensSavedByRewrites: compactStats.totalSaved,
|
|
1040
|
+
alreadyCompact: compactStats.totalAlreadyCompact,
|
|
1041
|
+
estimatedTokensSavedAlreadyCompact: compactStats.totalAlreadyCompactPotential,
|
|
1042
|
+
},
|
|
1043
|
+
codegraph: {
|
|
1044
|
+
eventsInPeriod: codegraphStats.totalEvents,
|
|
1045
|
+
totalToolCalls: codegraphStats.totalToolCalls,
|
|
1046
|
+
estimatedTokensSaved: codegraphStats.totalTokensSaved,
|
|
1047
|
+
},
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
if (wantJson) {
|
|
1051
|
+
process.stdout.write(JSON.stringify(statsObj) + '\n');
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// --- Tabular output ---
|
|
1056
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
1057
|
+
const noDataLabel = isArchived ? '(archivado)' : '(sin datos)';
|
|
1058
|
+
console.log('');
|
|
1059
|
+
console.log(` Stats del cambio: ${name}${isArchived && archivedDate ? ` [archivado el ${archivedDate}]` : ""}`);
|
|
1060
|
+
if (startDate) console.log(` Inicio estimado: ${startDate.toISOString().slice(0, 10)}`);
|
|
1061
|
+
console.log('');
|
|
1062
|
+
console.log(' Memory');
|
|
1063
|
+
console.log(` ${pad('lastStep', 20)} ${lastStep || noDataLabel}`);
|
|
1064
|
+
console.log(` ${pad('criteriaRun', 20)} ${criteriaRun.length > 0 ? criteriaRun.join(', ') : noDataLabel}`);
|
|
1065
|
+
console.log(` ${pad('testCommand', 20)} ${testCommand || noDataLabel}`);
|
|
1066
|
+
console.log('');
|
|
1067
|
+
console.log(' Review');
|
|
1068
|
+
console.log(` ${pad('passed', 20)} ${reviewDate !== null ? 'Si' : 'No'}`);
|
|
1069
|
+
if (reviewVerdict) console.log(` ${pad('verdict', 20)} ${reviewVerdict}`);
|
|
1070
|
+
if (reviewDate) console.log(` ${pad('date', 20)} ${reviewDate}`);
|
|
1071
|
+
if (reviewDate) console.log(` ${pad('failCount', 20)} ${reviewFailCount}`);
|
|
1072
|
+
console.log('');
|
|
1073
|
+
console.log(' Compact telemetry (periodo del cambio)');
|
|
1074
|
+
console.log(` ${pad('eventos', 20)} ${compactStats.totalEvents}`);
|
|
1075
|
+
console.log(` ${pad('rewrites', 20)} ${compactStats.totalRewrites}`);
|
|
1076
|
+
console.log(` ${pad('tokens ahorrados', 20)} ${compactStats.totalSaved}`);
|
|
1077
|
+
console.log('');
|
|
1078
|
+
console.log(' CodeGraph telemetry (periodo del cambio)');
|
|
1079
|
+
console.log(` ${pad('eventos', 20)} ${codegraphStats.totalEvents}`);
|
|
1080
|
+
console.log(` ${pad('tool calls', 20)} ${codegraphStats.totalToolCalls}`);
|
|
1081
|
+
console.log(` ${pad('tokens ahorrados', 20)} ${codegraphStats.totalTokensSaved}`);
|
|
1082
|
+
console.log('');
|
|
1083
|
+
}
|
|
1084
|
+
|
|
733
1085
|
function sddHelp() {
|
|
734
1086
|
console.log(`
|
|
735
1087
|
refacil-sdd-ai sdd — Gestión de artefactos SDD-AI
|
|
736
1088
|
|
|
737
1089
|
Subcomandos:
|
|
738
1090
|
sdd new-change <nombre> Crea un nuevo cambio con los 4 artefactos scaffold
|
|
739
|
-
sdd
|
|
740
|
-
|
|
1091
|
+
sdd sync-spec <nombre> Sincroniza specs del change a refacil-sdd/specs/<nombre>/spec.md
|
|
1092
|
+
(mismo idioma que los artefactos; sin traducción)
|
|
1093
|
+
[--from-archive] Lee el change desde refacil-sdd/changes/archive/*-<nombre>/
|
|
1094
|
+
sdd archive <nombre> Archiva un cambio (sync-spec automático + mueve a archive/)
|
|
1095
|
+
sdd list [--json] [--include-archived] Lista cambios activos con estado de review
|
|
741
1096
|
sdd status <nombre> [--json] Muestra estado de artefactos y tasks de un cambio
|
|
742
1097
|
sdd mark-reviewed <nombre> Escribe .review-passed con veredicto y resumen
|
|
743
1098
|
--verdict <v> Veredicto (ej: approved, approved-with-notes, rejected)
|
|
@@ -769,11 +1124,20 @@ function sddHelp() {
|
|
|
769
1124
|
[--base-branch <branch>] Rama base para nuevos cambios
|
|
770
1125
|
[--protected-branches <csv>] Ramas protegidas (separadas por coma)
|
|
771
1126
|
[--artifact-language <language>] Idioma para los artefactos SDD generados (english, spanish)
|
|
1127
|
+
sdd stats <nombre> [--json] Muestra estadísticas del cambio: memoria, review,
|
|
1128
|
+
compact telemetry y CodeGraph en el periodo del cambio
|
|
1129
|
+
sdd test-scope Resolves scoped test files for the given source files
|
|
1130
|
+
--files <csv> Comma-separated source file paths to scope tests for
|
|
1131
|
+
[--stack <name>] Stack hint (node, python, go, rust, java, dotnet)
|
|
1132
|
+
[--baseline <cmd>] Fallback test command when no test files are found
|
|
1133
|
+
[--json] Output result as JSON (testCommand, files, fallback, fallbackReason)
|
|
1134
|
+
Always exits 0.
|
|
772
1135
|
|
|
773
1136
|
Notas:
|
|
774
1137
|
- Los nombres de cambio deben empezar con minúscula y usar solo [a-z0-9-]
|
|
775
1138
|
- Si existe openspec/ y no existe refacil-sdd/, se migra automáticamente
|
|
776
|
-
- sdd archive
|
|
1139
|
+
- sdd archive ejecuta sync-spec antes de mover (preserva idioma de CA/CR del change)
|
|
1140
|
+
- sdd archive mueve el cambio completo (incluyendo memory.yaml si existe) al directorio archive/
|
|
777
1141
|
`);
|
|
778
1142
|
}
|
|
779
1143
|
|
|
@@ -790,6 +1154,9 @@ function handleSdd(sub, argv, projectRoot) {
|
|
|
790
1154
|
case 'archive':
|
|
791
1155
|
cmdArchive(args, root);
|
|
792
1156
|
break;
|
|
1157
|
+
case 'sync-spec':
|
|
1158
|
+
cmdSyncSpec(args, root);
|
|
1159
|
+
break;
|
|
793
1160
|
case 'list':
|
|
794
1161
|
cmdList(args, root);
|
|
795
1162
|
break;
|
|
@@ -823,10 +1190,28 @@ function handleSdd(sub, argv, projectRoot) {
|
|
|
823
1190
|
case 'write-config':
|
|
824
1191
|
cmdWriteConfig(args, root);
|
|
825
1192
|
break;
|
|
1193
|
+
case 'stats':
|
|
1194
|
+
cmdStats(args, root);
|
|
1195
|
+
break;
|
|
1196
|
+
case 'test-scope':
|
|
1197
|
+
cmdTestScope(args, root);
|
|
1198
|
+
break;
|
|
826
1199
|
default:
|
|
827
1200
|
sddHelp();
|
|
828
1201
|
process.exit(1);
|
|
829
1202
|
}
|
|
830
1203
|
}
|
|
831
1204
|
|
|
832
|
-
module.exports = {
|
|
1205
|
+
module.exports = {
|
|
1206
|
+
handleSdd,
|
|
1207
|
+
parseArgs,
|
|
1208
|
+
autoMigrateOpenspec,
|
|
1209
|
+
validateChangeName,
|
|
1210
|
+
isBugFixChangeName,
|
|
1211
|
+
resolveExistingChangeName,
|
|
1212
|
+
resolveArchivedChangeName,
|
|
1213
|
+
findProjectRoot,
|
|
1214
|
+
cmdWriteConfig,
|
|
1215
|
+
cmdStats,
|
|
1216
|
+
cmdTestScope,
|
|
1217
|
+
};
|