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.
Files changed (76) hide show
  1. package/NOTICE.md +46 -0
  2. package/README.md +209 -42
  3. package/agents/auditor.md +46 -0
  4. package/agents/debugger.md +41 -1
  5. package/agents/implementer.md +76 -10
  6. package/agents/investigator.md +36 -0
  7. package/agents/proposer.md +46 -2
  8. package/agents/tester.md +45 -8
  9. package/agents/validator.md +67 -13
  10. package/bin/cli.js +428 -83
  11. package/bin/postinstall.js +20 -0
  12. package/lib/bus/broker.js +121 -3
  13. package/lib/bus/spawn.js +189 -121
  14. package/lib/check-review.js +102 -0
  15. package/lib/codegraph-telemetry.js +135 -0
  16. package/lib/codegraph.js +273 -0
  17. package/lib/commands/autopilot.js +120 -0
  18. package/lib/commands/bus.js +29 -36
  19. package/lib/commands/compact.js +185 -46
  20. package/lib/commands/read-spec.js +352 -0
  21. package/lib/commands/sdd.js +429 -44
  22. package/lib/compact-guidance.js +122 -77
  23. package/lib/config.js +136 -0
  24. package/lib/global-paths.js +56 -20
  25. package/lib/hooks.js +32 -4
  26. package/lib/ide-detection.js +1 -1
  27. package/lib/ignore-files.js +5 -1
  28. package/lib/installer.js +202 -19
  29. package/lib/kapso.js +241 -0
  30. package/lib/methodology-migration-pending.js +13 -0
  31. package/lib/open-browser.js +32 -0
  32. package/lib/opencode-migrate.js +148 -0
  33. package/lib/opencode-plugin/index.js +84 -104
  34. package/lib/opencode-plugin/rules.js +236 -0
  35. package/lib/project-root.js +154 -0
  36. package/lib/repo-ide-sync.js +5 -0
  37. package/lib/spec-reader/lang.js +72 -0
  38. package/lib/spec-reader/md-parser.js +299 -0
  39. package/lib/spec-reader/session.js +139 -0
  40. package/lib/spec-reader/ui/app.js +685 -0
  41. package/lib/spec-reader/ui/index.html +59 -0
  42. package/lib/spec-reader/ui/mixed-lang.js +200 -0
  43. package/lib/spec-reader/ui/model-cache.js +117 -0
  44. package/lib/spec-reader/ui/style.css +294 -0
  45. package/lib/spec-reader/ui/supertonic-helper.js +565 -0
  46. package/lib/spec-sync.js +258 -0
  47. package/lib/test-scope.js +713 -0
  48. package/lib/testing-policy-sync.js +14 -2
  49. package/package.json +6 -3
  50. package/skills/apply/SKILL.md +39 -64
  51. package/skills/archive/SKILL.md +74 -48
  52. package/skills/ask/SKILL.md +43 -8
  53. package/skills/autopilot/SKILL.md +476 -0
  54. package/skills/bug/SKILL.md +52 -53
  55. package/skills/explore/SKILL.md +48 -1
  56. package/skills/guide/SKILL.md +31 -13
  57. package/skills/inbox/SKILL.md +9 -0
  58. package/skills/join/SKILL.md +1 -1
  59. package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
  60. package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
  61. package/skills/prereqs/SKILL.md +1 -1
  62. package/skills/propose/SKILL.md +74 -19
  63. package/skills/read-spec/SKILL.md +76 -0
  64. package/skills/reply/SKILL.md +42 -9
  65. package/skills/review/SKILL.md +63 -25
  66. package/skills/review/checklist.md +2 -2
  67. package/skills/say/SKILL.md +40 -4
  68. package/skills/setup/SKILL.md +59 -5
  69. package/skills/setup/troubleshooting.md +11 -3
  70. package/skills/stats/SKILL.md +157 -0
  71. package/skills/test/SKILL.md +35 -10
  72. package/skills/up-code/SKILL.md +20 -13
  73. package/skills/update/SKILL.md +32 -1
  74. package/skills/verify/SKILL.md +78 -41
  75. package/templates/compact-guidance.md +10 -0
  76. package/templates/methodology-guide.md +5 -0
@@ -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
- function findProjectRoot() {
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
- process.stdout.write(JSON.stringify(result) + '\n');
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
- console.log('Cambios activos en refacil-sdd/changes/:');
421
- for (const item of result) {
422
- const badge = item.reviewPassed ? '[reviewed]' : '[pending-review]';
423
- console.log(` ${item.name} ${badge}`);
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
- // specs: specs.md existe OR specs/ dir con al menos un .md
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 o --artifact-language.');
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 archive <nombre> Archiva un cambio completado a refacil-sdd/changes/archive/
740
- sdd list [--json] Lista cambios activos con estado de review
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 elimina memory.yaml automáticamente antes de mover el cambio
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 = { handleSdd, parseArgs, autoMigrateOpenspec, validateChangeName, resolveExistingChangeName, findProjectRoot, cmdWriteConfig };
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
+ };