refacil-sdd-ai 5.2.3 → 5.3.1
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 +210 -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 +56 -2
- package/agents/tester.md +45 -8
- package/agents/validator.md +67 -13
- package/bin/cli.js +396 -84
- 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 +600 -47
- package/lib/compact-guidance.js +122 -77
- package/lib/config.js +136 -0
- package/lib/global-paths.js +56 -20
- package/lib/hooks.js +26 -4
- package/lib/ide-detection.js +1 -1
- package/lib/ignore-files.js +5 -1
- package/lib/installer.js +196 -19
- package/lib/kapso.js +308 -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 +5 -3
- package/skills/apply/SKILL.md +50 -65
- package/skills/archive/SKILL.md +84 -50
- package/skills/ask/SKILL.md +43 -8
- package/skills/autopilot/SKILL.md +505 -0
- package/skills/bug/SKILL.md +52 -53
- package/skills/explore/SKILL.md +48 -1
- package/skills/guide/SKILL.md +35 -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 +82 -19
- package/skills/read-spec/SKILL.md +76 -0
- package/skills/reply/SKILL.md +42 -9
- package/skills/review/SKILL.md +71 -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 +160 -0
- package/skills/status/SKILL.md +116 -0
- package/skills/test/SKILL.md +38 -11
- package/skills/up-code/SKILL.md +20 -13
- package/skills/update/SKILL.md +32 -1
- package/skills/verify/SKILL.md +85 -40
- 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.' };
|
|
@@ -137,6 +183,88 @@ function serializeMemoryYaml(obj) {
|
|
|
137
183
|
return lines.join('\n') + '\n';
|
|
138
184
|
}
|
|
139
185
|
|
|
186
|
+
// --- State tracking ---
|
|
187
|
+
|
|
188
|
+
const VALID_STATES = [
|
|
189
|
+
'proposed',
|
|
190
|
+
'approved',
|
|
191
|
+
'apply-in-progress',
|
|
192
|
+
'applied',
|
|
193
|
+
'tested',
|
|
194
|
+
'verified',
|
|
195
|
+
'reviewed',
|
|
196
|
+
'archived',
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Infers the current state of a change from its artifacts when no explicit
|
|
201
|
+
* currentState is stored in memory. Retrocompatible — never writes to disk.
|
|
202
|
+
*
|
|
203
|
+
* Priority (descending):
|
|
204
|
+
* 1. .review-passed exists → 'reviewed'
|
|
205
|
+
* 2. lastStep === 'verify' or 'verified' → 'verified'
|
|
206
|
+
* 3. lastStep === 'test' → 'tested'
|
|
207
|
+
* 4. lastStep === 'apply' → 'applied'
|
|
208
|
+
* 5. tasks.md has at least one [x] and lastStep is absent → 'apply-in-progress'
|
|
209
|
+
* 6. only proposal.md exists → 'proposed'
|
|
210
|
+
* 7. none of the above → 'unknown'
|
|
211
|
+
*
|
|
212
|
+
* NOTE — 'approved' is intentionally absent from this table.
|
|
213
|
+
* There is no artifact signal that distinguishes an approved proposal from a
|
|
214
|
+
* merely proposed one: both have proposal.md, specs.md, design.md, tasks.md.
|
|
215
|
+
* 'approved' is set exclusively via `set-memory --state approved --actor propose-skill`
|
|
216
|
+
* when the human approves the artifacts in /refacil:propose Step 4.
|
|
217
|
+
* Inference can only detect 'proposed' (artifact exists but no explicit state recorded).
|
|
218
|
+
*
|
|
219
|
+
* @param {string} changeDir Absolute path to the change directory.
|
|
220
|
+
* @param {object} memory Parsed memory.yaml object (may be empty).
|
|
221
|
+
* @returns {{ currentState: string, inferred: boolean }}
|
|
222
|
+
*/
|
|
223
|
+
function inferCurrentState(changeDir, memory) {
|
|
224
|
+
// 1. .review-passed
|
|
225
|
+
if (fs.existsSync(path.join(changeDir, '.review-passed'))) {
|
|
226
|
+
return { currentState: 'reviewed', inferred: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const lastStep = memory.lastStep || null;
|
|
230
|
+
|
|
231
|
+
// 2. lastStep verify/verified
|
|
232
|
+
if (lastStep === 'verify' || lastStep === 'verified') {
|
|
233
|
+
return { currentState: 'verified', inferred: true };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 3. lastStep test
|
|
237
|
+
if (lastStep === 'test') {
|
|
238
|
+
return { currentState: 'tested', inferred: true };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 4. lastStep apply
|
|
242
|
+
if (lastStep === 'apply') {
|
|
243
|
+
return { currentState: 'applied', inferred: true };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 5. tasks.md has at least one [x] and lastStep absent
|
|
247
|
+
const tasksPath = path.join(changeDir, 'tasks.md');
|
|
248
|
+
if (!lastStep && fs.existsSync(tasksPath)) {
|
|
249
|
+
try {
|
|
250
|
+
const tasksContent = fs.readFileSync(tasksPath, 'utf8');
|
|
251
|
+
if (/^- \[x\]/m.test(tasksContent)) {
|
|
252
|
+
return { currentState: 'apply-in-progress', inferred: true };
|
|
253
|
+
}
|
|
254
|
+
} catch (_) {
|
|
255
|
+
// Unexpected read error (e.g. permission denied): swallow and fall through
|
|
256
|
+
// to the proposal.md check so inference still produces a useful result.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 6. only proposal.md exists (no design, tasks, specs)
|
|
261
|
+
if (fs.existsSync(path.join(changeDir, 'proposal.md'))) {
|
|
262
|
+
return { currentState: 'proposed', inferred: true };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { currentState: 'unknown', inferred: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
140
268
|
// --- Subcomandos ---
|
|
141
269
|
|
|
142
270
|
function cmdValidateName(argv) {
|
|
@@ -179,6 +307,38 @@ function cmdNewChange(argv, projectRoot) {
|
|
|
179
307
|
console.log(`Cambio '${name}' creado en refacil-sdd/changes/${name}/`);
|
|
180
308
|
}
|
|
181
309
|
|
|
310
|
+
function cmdSyncSpec(argv, projectRoot) {
|
|
311
|
+
const args = parseArgs(argv);
|
|
312
|
+
const rawName = args._positional[0];
|
|
313
|
+
|
|
314
|
+
autoMigrateOpenspec(projectRoot);
|
|
315
|
+
const resolved = resolveExistingChangeName(projectRoot, rawName);
|
|
316
|
+
if (!resolved.ok) {
|
|
317
|
+
console.error(resolved.reason);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
const name = resolved.name;
|
|
321
|
+
const fromArchive = args['from-archive'] === true;
|
|
322
|
+
|
|
323
|
+
if (!fromArchive) {
|
|
324
|
+
const sourceDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
|
|
325
|
+
if (!fs.existsSync(sourceDir)) {
|
|
326
|
+
console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/`);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const { syncSpecToCatalog } = require('../spec-sync');
|
|
333
|
+
const result = syncSpecToCatalog(projectRoot, name, { fromArchive });
|
|
334
|
+
const rel = path.relative(projectRoot, result.specPath).replace(/\\/g, '/');
|
|
335
|
+
console.log(` Spec sincronizado: ${rel} (${result.criteriaCount} criterios, idioma: ${result.language})`);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error(` Error sincronizando spec: ${err.message}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
182
342
|
function cmdArchive(argv, projectRoot) {
|
|
183
343
|
const args = parseArgs(argv);
|
|
184
344
|
const rawName = args._positional[0];
|
|
@@ -203,6 +363,20 @@ function cmdArchive(argv, projectRoot) {
|
|
|
203
363
|
process.exit(1);
|
|
204
364
|
}
|
|
205
365
|
|
|
366
|
+
if (!isBugFixChangeName(name)) {
|
|
367
|
+
try {
|
|
368
|
+
const { syncSpecToCatalog } = require('../spec-sync');
|
|
369
|
+
const result = syncSpecToCatalog(projectRoot, name);
|
|
370
|
+
const rel = path.relative(projectRoot, result.specPath).replace(/\\/g, '/');
|
|
371
|
+
console.log(` Spec sincronizado: ${rel} (idioma: ${result.language}, ${result.criteriaCount} criterios)`);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.error(` Error sincronizando spec antes de archivar: ${err.message}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
console.log(' Bug fix (fix-*): omitiendo sincronización de spec (use refacil:archive para documentar en refacil-sdd/specs/)');
|
|
378
|
+
}
|
|
379
|
+
|
|
206
380
|
const date = new Date().toISOString().slice(0, 10);
|
|
207
381
|
const archiveDir = path.join(projectRoot, 'refacil-sdd', 'changes', 'archive');
|
|
208
382
|
const destDir = path.join(archiveDir, `${date}-${name}`);
|
|
@@ -212,10 +386,6 @@ function cmdArchive(argv, projectRoot) {
|
|
|
212
386
|
process.exit(1);
|
|
213
387
|
}
|
|
214
388
|
|
|
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
389
|
fs.mkdirSync(archiveDir, { recursive: true });
|
|
220
390
|
fs.renameSync(sourceDir, destDir);
|
|
221
391
|
|
|
@@ -232,7 +402,7 @@ function cmdSetMemory(argv, projectRoot) {
|
|
|
232
402
|
const rawName = args._positional[0];
|
|
233
403
|
|
|
234
404
|
if (!rawName) {
|
|
235
|
-
console.error('Uso: refacil-sdd-ai sdd set-memory <nombre-cambio> [--last-step <value>] [--stack-detected <value>] [--touched-files <csv>] [--commands-run <value>] [--criteria-run <csv>]');
|
|
405
|
+
console.error('Uso: refacil-sdd-ai sdd set-memory <nombre-cambio> [--last-step <value>] [--stack-detected <value>] [--touched-files <csv>] [--commands-run <value>] [--criteria-run <csv>] [--state <estado>] [--actor <nombre>]');
|
|
236
406
|
process.exit(1);
|
|
237
407
|
}
|
|
238
408
|
|
|
@@ -252,10 +422,30 @@ function cmdSetMemory(argv, projectRoot) {
|
|
|
252
422
|
process.exit(1);
|
|
253
423
|
}
|
|
254
424
|
|
|
425
|
+
// Validate --state before touching any file (CR-01: exit 1 without modifying file)
|
|
426
|
+
if (args['state'] !== undefined) {
|
|
427
|
+
const stateValue = args['state'];
|
|
428
|
+
// CR-02: empty string or bare flag (parsed as boolean true) → explicit empty-state error
|
|
429
|
+
if (stateValue === '' || stateValue === true) {
|
|
430
|
+
console.error('set-memory: el estado no puede estar vacío');
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
if (!VALID_STATES.includes(stateValue)) {
|
|
434
|
+
console.error(`set-memory: estado inválido '${stateValue}'. Estados válidos: ${VALID_STATES.join(', ')}`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Validate --actor: pipe character would corrupt the stateHistory pipe format
|
|
440
|
+
if (args['actor'] !== undefined && String(args['actor']).includes('|')) {
|
|
441
|
+
console.error("set-memory: --actor no puede contener el carácter '|'");
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
255
445
|
// Require at least one field flag
|
|
256
|
-
const knownFlags = ['last-step', 'stack-detected', 'touched-files', 'commands-run', 'criteria-run'];
|
|
446
|
+
const knownFlags = ['last-step', 'stack-detected', 'touched-files', 'commands-run', 'criteria-run', 'state', 'actor'];
|
|
257
447
|
if (!knownFlags.some((f) => args[f] !== undefined)) {
|
|
258
|
-
console.error('set-memory: debe especificar al menos un campo (--last-step, --stack-detected, --touched-files, --commands-run, --criteria-run)');
|
|
448
|
+
console.error('set-memory: debe especificar al menos un campo (--last-step, --stack-detected, --touched-files, --commands-run, --criteria-run, --state, --actor)');
|
|
259
449
|
process.exit(1);
|
|
260
450
|
}
|
|
261
451
|
|
|
@@ -282,6 +472,18 @@ function cmdSetMemory(argv, projectRoot) {
|
|
|
282
472
|
existing['criteriaRun'] = args['criteria-run'].split(',').map((s) => s.trim()).filter(Boolean);
|
|
283
473
|
}
|
|
284
474
|
|
|
475
|
+
// State tracking (T-01)
|
|
476
|
+
if (args['state'] !== undefined) {
|
|
477
|
+
const actor = args['actor'] !== undefined ? String(args['actor']) : 'cli';
|
|
478
|
+
existing['currentState'] = args['state'];
|
|
479
|
+
const iso = new Date().toISOString();
|
|
480
|
+
const entry = `${args['state']}|${iso}|${actor}`;
|
|
481
|
+
if (!Array.isArray(existing['stateHistory'])) {
|
|
482
|
+
existing['stateHistory'] = [];
|
|
483
|
+
}
|
|
484
|
+
existing['stateHistory'].push(entry);
|
|
485
|
+
}
|
|
486
|
+
|
|
285
487
|
fs.writeFileSync(memoryPath, serializeMemoryYaml(existing), 'utf8');
|
|
286
488
|
console.log(`memory.yaml actualizado para '${name}'`);
|
|
287
489
|
}
|
|
@@ -389,6 +591,7 @@ function cmdClearReviewFails(argv, projectRoot) {
|
|
|
389
591
|
function cmdList(argv, projectRoot) {
|
|
390
592
|
const args = parseArgs(argv);
|
|
391
593
|
const wantJson = args.json === true;
|
|
594
|
+
const includeArchived = args['include-archived'] === true;
|
|
392
595
|
|
|
393
596
|
autoMigrateOpenspec(projectRoot);
|
|
394
597
|
|
|
@@ -410,17 +613,55 @@ function cmdList(argv, projectRoot) {
|
|
|
410
613
|
return { name: e.name, reviewPassed };
|
|
411
614
|
});
|
|
412
615
|
|
|
616
|
+
// Build archived entries when --include-archived is requested
|
|
617
|
+
const archivedResult = [];
|
|
618
|
+
if (includeArchived) {
|
|
619
|
+
const archiveDir = path.join(changesDir, 'archive');
|
|
620
|
+
if (fs.existsSync(archiveDir)) {
|
|
621
|
+
const datePrefix = /^\d{4}-\d{2}-\d{2}-/;
|
|
622
|
+
try {
|
|
623
|
+
const archiveEntries = fs.readdirSync(archiveDir, { withFileTypes: true })
|
|
624
|
+
.filter((e) => e.isDirectory() && datePrefix.test(e.name));
|
|
625
|
+
for (const e of archiveEntries) {
|
|
626
|
+
const dateMatch = e.name.match(/^(\d{4}-\d{2}-\d{2})-/);
|
|
627
|
+
const archivedDate = dateMatch ? dateMatch[1] : null;
|
|
628
|
+
const cleanName = e.name.replace(datePrefix, '');
|
|
629
|
+
archivedResult.push({ name: cleanName, reviewPassed: null, archived: true, archivedDate });
|
|
630
|
+
}
|
|
631
|
+
// Sort descending by date prefix (most recent first)
|
|
632
|
+
archivedResult.sort((a, b) => {
|
|
633
|
+
const da = a.archivedDate || '';
|
|
634
|
+
const db = b.archivedDate || '';
|
|
635
|
+
return db.localeCompare(da);
|
|
636
|
+
});
|
|
637
|
+
} catch (_) {}
|
|
638
|
+
}
|
|
639
|
+
// If archive dir doesn't exist, gracefully produce empty list (exit 0)
|
|
640
|
+
}
|
|
641
|
+
|
|
413
642
|
if (wantJson) {
|
|
414
|
-
|
|
643
|
+
const combined = result.concat(archivedResult);
|
|
644
|
+
process.stdout.write(JSON.stringify(combined) + '\n');
|
|
415
645
|
} else {
|
|
416
|
-
if (result.length === 0) {
|
|
646
|
+
if (result.length === 0 && archivedResult.length === 0) {
|
|
417
647
|
console.log('Sin cambios activos.');
|
|
418
648
|
return;
|
|
419
649
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
650
|
+
if (result.length > 0) {
|
|
651
|
+
console.log('Cambios activos en refacil-sdd/changes/:');
|
|
652
|
+
for (const item of result) {
|
|
653
|
+
const badge = item.reviewPassed ? '[reviewed]' : '[pending-review]';
|
|
654
|
+
console.log(` ${item.name} ${badge}`);
|
|
655
|
+
}
|
|
656
|
+
} else if (!includeArchived) {
|
|
657
|
+
console.log('Sin cambios activos.');
|
|
658
|
+
}
|
|
659
|
+
if (includeArchived && archivedResult.length > 0) {
|
|
660
|
+
if (result.length > 0) console.log('');
|
|
661
|
+
console.log('Cambios archivados en refacil-sdd/changes/archive/:');
|
|
662
|
+
for (const item of archivedResult) {
|
|
663
|
+
console.log(` ${item.name} [archived] (${item.archivedDate || '?'})`);
|
|
664
|
+
}
|
|
424
665
|
}
|
|
425
666
|
}
|
|
426
667
|
}
|
|
@@ -454,16 +695,7 @@ function cmdStatus(argv, projectRoot) {
|
|
|
454
695
|
const hasDesign = fs.existsSync(path.join(changeDir, 'design.md'));
|
|
455
696
|
const hasTasks = fs.existsSync(path.join(changeDir, 'tasks.md'));
|
|
456
697
|
|
|
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
|
-
}
|
|
698
|
+
const hasSpecs = collectSpecSourceFiles(changeDir).length > 0;
|
|
467
699
|
|
|
468
700
|
const artifacts = {
|
|
469
701
|
proposal: hasProposal,
|
|
@@ -485,16 +717,46 @@ function cmdStatus(argv, projectRoot) {
|
|
|
485
717
|
const reviewPassed = fs.existsSync(path.join(changeDir, '.review-passed'));
|
|
486
718
|
|
|
487
719
|
const ready = {
|
|
488
|
-
forApply: artifacts.proposal && artifacts.tasks,
|
|
720
|
+
forApply: artifacts.proposal && artifacts.design && artifacts.tasks && artifacts.specs,
|
|
489
721
|
forArchive: reviewPassed && taskStats.total > 0 && taskStats.pending === 0,
|
|
490
722
|
};
|
|
491
723
|
|
|
724
|
+
// Read memory for currentState / lastStep / touchedFiles
|
|
725
|
+
const memoryPath = path.join(changeDir, 'memory.yaml');
|
|
726
|
+
let memory = {};
|
|
727
|
+
if (fs.existsSync(memoryPath)) {
|
|
728
|
+
try {
|
|
729
|
+
memory = parseYaml(fs.readFileSync(memoryPath, 'utf8')) || {};
|
|
730
|
+
} catch (_) {
|
|
731
|
+
memory = {};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Resolve currentState: explicit from memory or inferred
|
|
736
|
+
let currentState = null;
|
|
737
|
+
let stateInferred = false;
|
|
738
|
+
if (memory.currentState) {
|
|
739
|
+
currentState = memory.currentState;
|
|
740
|
+
stateInferred = false;
|
|
741
|
+
} else {
|
|
742
|
+
const inferred = inferCurrentState(changeDir, memory);
|
|
743
|
+
currentState = inferred.currentState;
|
|
744
|
+
stateInferred = inferred.inferred;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const lastStep = memory.lastStep || null;
|
|
748
|
+
const touchedFiles = Array.isArray(memory.touchedFiles) ? memory.touchedFiles : [];
|
|
749
|
+
|
|
492
750
|
const status = {
|
|
493
751
|
name,
|
|
494
752
|
artifacts,
|
|
495
753
|
tasks: taskStats,
|
|
496
754
|
reviewPassed,
|
|
497
755
|
ready,
|
|
756
|
+
currentState,
|
|
757
|
+
stateInferred,
|
|
758
|
+
lastStep,
|
|
759
|
+
touchedFiles,
|
|
498
760
|
};
|
|
499
761
|
|
|
500
762
|
if (wantJson) {
|
|
@@ -514,6 +776,11 @@ function cmdStatus(argv, projectRoot) {
|
|
|
514
776
|
console.log('');
|
|
515
777
|
console.log(` Review aprobado: ${reviewPassed ? 'Si' : 'No'}`);
|
|
516
778
|
console.log('');
|
|
779
|
+
console.log(' Estado:');
|
|
780
|
+
console.log(` currentState: ${currentState}${stateInferred ? ' (inferido)' : ''}`);
|
|
781
|
+
if (lastStep) console.log(` lastStep: ${lastStep}`);
|
|
782
|
+
if (touchedFiles.length > 0) console.log(` touchedFiles: ${touchedFiles.join(', ')}`);
|
|
783
|
+
console.log('');
|
|
517
784
|
console.log(' Listo para:');
|
|
518
785
|
console.log(` apply: ${ready.forApply ? 'Si' : 'No'}`);
|
|
519
786
|
console.log(` archive: ${ready.forArchive ? 'Si' : 'No'}`);
|
|
@@ -653,11 +920,12 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
653
920
|
const rawBaseBranch = args['base-branch'];
|
|
654
921
|
const rawProtectedBranches = args['protected-branches'];
|
|
655
922
|
const rawArtifactLanguage = args['artifact-language'];
|
|
923
|
+
const rawCodegraph = args['codegraph'];
|
|
656
924
|
|
|
657
925
|
// 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
|
|
926
|
+
if (rawBaseBranch === undefined && rawProtectedBranches === undefined && rawArtifactLanguage === undefined && rawCodegraph === undefined) {
|
|
927
|
+
console.error('Uso: refacil-sdd-ai sdd write-config [--global] [--base-branch <branch>] [--protected-branches <csv>] [--artifact-language <language>] [--codegraph <mode>]');
|
|
928
|
+
console.error('Debe especificar al menos --base-branch, --protected-branches, --artifact-language o --codegraph.');
|
|
661
929
|
process.exit(1);
|
|
662
930
|
}
|
|
663
931
|
|
|
@@ -689,6 +957,18 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
689
957
|
}
|
|
690
958
|
}
|
|
691
959
|
|
|
960
|
+
// --codegraph: must be a valid CODEGRAPH_MODES value
|
|
961
|
+
if (rawCodegraph !== undefined) {
|
|
962
|
+
if (typeof rawCodegraph !== 'string' || rawCodegraph.trim() === '') {
|
|
963
|
+
console.error('Error: --codegraph no puede estar vacío.');
|
|
964
|
+
process.exit(1);
|
|
965
|
+
}
|
|
966
|
+
if (!CODEGRAPH_MODES.includes(rawCodegraph.trim())) {
|
|
967
|
+
console.error(`Error: --codegraph "${rawCodegraph.trim()}" no es un modo válido. Valores válidos: ${CODEGRAPH_MODES.join(', ')}.`);
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
692
972
|
const targetPath = isGlobal
|
|
693
973
|
? path.join(os.homedir(), '.refacil-sdd-ai', 'config.yaml')
|
|
694
974
|
: path.join(projectRoot, 'refacil-sdd', 'config.yaml');
|
|
@@ -707,6 +987,9 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
707
987
|
if (rawArtifactLanguage !== undefined) {
|
|
708
988
|
merged.artifactLanguage = rawArtifactLanguage.trim();
|
|
709
989
|
}
|
|
990
|
+
if (rawCodegraph !== undefined) {
|
|
991
|
+
merged.codegraphMode = rawCodegraph.trim();
|
|
992
|
+
}
|
|
710
993
|
|
|
711
994
|
// CA-03: no-op when all provided keys already match existing config (semantic comparison)
|
|
712
995
|
const isNoOp = Object.keys(existing).length > 0 &&
|
|
@@ -714,7 +997,8 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
714
997
|
(protectedBranchesList === undefined ||
|
|
715
998
|
(Array.isArray(existing.protectedBranches) &&
|
|
716
999
|
JSON.stringify(existing.protectedBranches.slice().sort()) === JSON.stringify(protectedBranchesList.slice().sort()))) &&
|
|
717
|
-
(rawArtifactLanguage === undefined || existing.artifactLanguage === rawArtifactLanguage.trim())
|
|
1000
|
+
(rawArtifactLanguage === undefined || existing.artifactLanguage === rawArtifactLanguage.trim()) &&
|
|
1001
|
+
(rawCodegraph === undefined || existing.codegraphMode === rawCodegraph.trim());
|
|
718
1002
|
if (isNoOp) {
|
|
719
1003
|
console.log(`Sin cambios: ${targetPath} ya tiene los valores indicados.`);
|
|
720
1004
|
process.exit(0);
|
|
@@ -730,14 +1014,249 @@ function cmdWriteConfig(argv, projectRoot) {
|
|
|
730
1014
|
console.log(`Configuración escrita en ${targetPath} (nivel: ${level})`);
|
|
731
1015
|
}
|
|
732
1016
|
|
|
1017
|
+
function cmdTestScope(argv, projectRoot) {
|
|
1018
|
+
const args = parseArgs(argv);
|
|
1019
|
+
const wantJson = args.json === true;
|
|
1020
|
+
|
|
1021
|
+
const filesRaw = args.files || '';
|
|
1022
|
+
const stackHint = args.stack || undefined;
|
|
1023
|
+
const baselineCmd = args.baseline || '';
|
|
1024
|
+
// Use the already-resolved projectRoot from handleSdd (via findProjectRoot()) so
|
|
1025
|
+
// the CLI works correctly when invoked from a subdirectory within the monorepo.
|
|
1026
|
+
const root = projectRoot || process.cwd();
|
|
1027
|
+
|
|
1028
|
+
// Parse CSV files — empty string or missing → empty array (CR-04: never fail on empty)
|
|
1029
|
+
const files = filesRaw
|
|
1030
|
+
? filesRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
1031
|
+
: [];
|
|
1032
|
+
|
|
1033
|
+
const { testScope } = require('../test-scope');
|
|
1034
|
+
const result = testScope({ files, stack: stackHint, baseline: baselineCmd, projectRoot: root });
|
|
1035
|
+
|
|
1036
|
+
if (wantJson) {
|
|
1037
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
1038
|
+
} else {
|
|
1039
|
+
console.log(`testCommand: ${result.testCommand}`);
|
|
1040
|
+
console.log(`files: ${result.files.join(', ') || '(none)'}`);
|
|
1041
|
+
console.log(`fallback: ${result.fallback}`);
|
|
1042
|
+
if (result.fallbackReason) console.log(`fallbackReason: ${result.fallbackReason}`);
|
|
1043
|
+
}
|
|
1044
|
+
// Always exit 0 (CR-04)
|
|
1045
|
+
process.exit(0);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function cmdStats(argv, projectRoot) {
|
|
1049
|
+
const args = parseArgs(argv);
|
|
1050
|
+
const rawName = args._positional[0];
|
|
1051
|
+
const wantJson = args.json === true;
|
|
1052
|
+
|
|
1053
|
+
if (!rawName) {
|
|
1054
|
+
console.error('Uso: refacil-sdd-ai sdd stats <nombre-cambio> [--json]');
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
autoMigrateOpenspec(projectRoot);
|
|
1059
|
+
const resolved = resolveExistingChangeName(projectRoot, rawName);
|
|
1060
|
+
if (!resolved.ok) {
|
|
1061
|
+
console.error(resolved.reason);
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
const name = resolved.name;
|
|
1065
|
+
|
|
1066
|
+
let changeDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
|
|
1067
|
+
let isArchived = false;
|
|
1068
|
+
let archivedDate = null;
|
|
1069
|
+
|
|
1070
|
+
if (!fs.existsSync(changeDir)) {
|
|
1071
|
+
// Fallback: try to find in archive
|
|
1072
|
+
const archivedPath = resolveArchivedChangeName(projectRoot, name);
|
|
1073
|
+
if (archivedPath) {
|
|
1074
|
+
changeDir = archivedPath;
|
|
1075
|
+
isArchived = true;
|
|
1076
|
+
// Extract YYYY-MM-DD from the directory basename
|
|
1077
|
+
const baseName = path.basename(archivedPath);
|
|
1078
|
+
const dateMatch = baseName.match(/^(\d{4}-\d{2}-\d{2})-/);
|
|
1079
|
+
archivedDate = dateMatch ? dateMatch[1] : null;
|
|
1080
|
+
} else {
|
|
1081
|
+
console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/ ni en el archivo.`);
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// --- Read memory.yaml ---
|
|
1087
|
+
const memoryPath = path.join(changeDir, 'memory.yaml');
|
|
1088
|
+
let memory = {};
|
|
1089
|
+
if (fs.existsSync(memoryPath)) {
|
|
1090
|
+
try {
|
|
1091
|
+
memory = parseYaml(fs.readFileSync(memoryPath, 'utf8')) || {};
|
|
1092
|
+
} catch (_) {
|
|
1093
|
+
memory = {};
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const testCommand = memory.testCommand || memory.commandsRun || null;
|
|
1097
|
+
const lastStep = memory.lastStep || null;
|
|
1098
|
+
const criteriaRun = Array.isArray(memory.criteriaRun) ? memory.criteriaRun : [];
|
|
1099
|
+
|
|
1100
|
+
// Resolve currentState for stats
|
|
1101
|
+
let statsCurrentState = null;
|
|
1102
|
+
let statsStateInferred = false;
|
|
1103
|
+
if (memory.currentState) {
|
|
1104
|
+
statsCurrentState = memory.currentState;
|
|
1105
|
+
statsStateInferred = false;
|
|
1106
|
+
} else {
|
|
1107
|
+
const inferred = inferCurrentState(changeDir, memory);
|
|
1108
|
+
statsCurrentState = inferred.currentState;
|
|
1109
|
+
statsStateInferred = inferred.inferred;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// --- Read .review-passed ---
|
|
1113
|
+
const reviewPassedPath = path.join(changeDir, '.review-passed');
|
|
1114
|
+
let reviewDate = null;
|
|
1115
|
+
let reviewVerdict = null;
|
|
1116
|
+
let reviewFailCount = 0;
|
|
1117
|
+
if (fs.existsSync(reviewPassedPath)) {
|
|
1118
|
+
try {
|
|
1119
|
+
const rp = JSON.parse(fs.readFileSync(reviewPassedPath, 'utf8'));
|
|
1120
|
+
reviewDate = rp.date || null;
|
|
1121
|
+
reviewVerdict = rp.verdict || null;
|
|
1122
|
+
reviewFailCount = rp.failCount || 0;
|
|
1123
|
+
} catch (_) {}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// --- Determine change start date from proposal.md mtime (fallback: summary.md) ---
|
|
1127
|
+
const proposalPath = path.join(changeDir, 'proposal.md');
|
|
1128
|
+
let startDate = null;
|
|
1129
|
+
if (fs.existsSync(proposalPath)) {
|
|
1130
|
+
try {
|
|
1131
|
+
const stat = fs.statSync(proposalPath);
|
|
1132
|
+
startDate = stat.mtime;
|
|
1133
|
+
} catch (_) {}
|
|
1134
|
+
}
|
|
1135
|
+
if (!startDate) {
|
|
1136
|
+
const summaryPath = path.join(changeDir, 'summary.md');
|
|
1137
|
+
if (fs.existsSync(summaryPath)) {
|
|
1138
|
+
try {
|
|
1139
|
+
const stat = fs.statSync(summaryPath);
|
|
1140
|
+
startDate = stat.mtime;
|
|
1141
|
+
} catch (_) {}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// --- Read compact.log filtered by change period ---
|
|
1146
|
+
let compactStats = { totalRewrites: 0, totalSaved: 0, totalAlreadyCompact: 0, totalAlreadyCompactPotential: 0, totalEvents: 0 };
|
|
1147
|
+
try {
|
|
1148
|
+
const compactTelemetry = require('../compact/telemetry');
|
|
1149
|
+
const allCompact = compactTelemetry.readLog ? compactTelemetry.readLog() : [];
|
|
1150
|
+
const filtered = startDate ? allCompact.filter((e) => e.ts && new Date(e.ts) >= startDate) : allCompact;
|
|
1151
|
+
let totalRewrites = 0, totalSaved = 0, totalAlreadyCompact = 0, totalAlreadyCompactPotential = 0;
|
|
1152
|
+
for (const e of filtered) {
|
|
1153
|
+
const evType = e.eventType || 'hook_rewrite';
|
|
1154
|
+
if (evType === 'already_compact') {
|
|
1155
|
+
totalAlreadyCompact++;
|
|
1156
|
+
totalAlreadyCompactPotential += e.savedTokensEst || 0;
|
|
1157
|
+
} else {
|
|
1158
|
+
totalRewrites++;
|
|
1159
|
+
totalSaved += e.savedTokensEst || 0;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
compactStats = { totalRewrites, totalSaved, totalAlreadyCompact, totalAlreadyCompactPotential, totalEvents: filtered.length };
|
|
1163
|
+
} catch (_) {}
|
|
1164
|
+
|
|
1165
|
+
// --- Read codegraph.log filtered by change period ---
|
|
1166
|
+
let codegraphStats = { totalEvents: 0, totalToolCalls: 0, totalTokensSaved: 0 };
|
|
1167
|
+
try {
|
|
1168
|
+
const cgTelemetry = require('../codegraph-telemetry');
|
|
1169
|
+
const allCg = cgTelemetry.readLog ? cgTelemetry.readLog() : [];
|
|
1170
|
+
const filtered = startDate ? allCg.filter((e) => e.ts && new Date(e.ts) >= startDate) : allCg;
|
|
1171
|
+
let totalEvents = 0, totalToolCalls = 0, totalTokensSaved = 0;
|
|
1172
|
+
for (const e of filtered) {
|
|
1173
|
+
totalEvents++;
|
|
1174
|
+
totalToolCalls += e.toolCallsCount || 0;
|
|
1175
|
+
totalTokensSaved += e.estimatedTokensSaved || 0;
|
|
1176
|
+
}
|
|
1177
|
+
codegraphStats = { totalEvents, totalToolCalls, totalTokensSaved };
|
|
1178
|
+
} catch (_) {}
|
|
1179
|
+
|
|
1180
|
+
// --- Build stats object ---
|
|
1181
|
+
const statsObj = {
|
|
1182
|
+
changeName: name,
|
|
1183
|
+
isArchived,
|
|
1184
|
+
archivedDate,
|
|
1185
|
+
startDate: startDate ? startDate.toISOString() : null,
|
|
1186
|
+
memory: {
|
|
1187
|
+
testCommand: testCommand || null,
|
|
1188
|
+
lastStep: lastStep || null,
|
|
1189
|
+
criteriaRun,
|
|
1190
|
+
currentState: statsCurrentState,
|
|
1191
|
+
stateInferred: statsStateInferred,
|
|
1192
|
+
},
|
|
1193
|
+
review: {
|
|
1194
|
+
passed: reviewDate !== null,
|
|
1195
|
+
verdict: reviewVerdict,
|
|
1196
|
+
date: reviewDate,
|
|
1197
|
+
failCount: reviewFailCount,
|
|
1198
|
+
},
|
|
1199
|
+
compact: {
|
|
1200
|
+
eventsInPeriod: compactStats.totalEvents,
|
|
1201
|
+
rewrites: compactStats.totalRewrites,
|
|
1202
|
+
estimatedTokensSavedByRewrites: compactStats.totalSaved,
|
|
1203
|
+
alreadyCompact: compactStats.totalAlreadyCompact,
|
|
1204
|
+
estimatedTokensSavedAlreadyCompact: compactStats.totalAlreadyCompactPotential,
|
|
1205
|
+
},
|
|
1206
|
+
codegraph: {
|
|
1207
|
+
eventsInPeriod: codegraphStats.totalEvents,
|
|
1208
|
+
totalToolCalls: codegraphStats.totalToolCalls,
|
|
1209
|
+
estimatedTokensSaved: codegraphStats.totalTokensSaved,
|
|
1210
|
+
},
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
if (wantJson) {
|
|
1214
|
+
process.stdout.write(JSON.stringify(statsObj) + '\n');
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// --- Tabular output ---
|
|
1219
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
1220
|
+
const noDataLabel = isArchived ? '(archivado)' : '(sin datos)';
|
|
1221
|
+
console.log('');
|
|
1222
|
+
console.log(` Stats del cambio: ${name}${isArchived && archivedDate ? ` [archivado el ${archivedDate}]` : ""}`);
|
|
1223
|
+
if (startDate) console.log(` Inicio estimado: ${startDate.toISOString().slice(0, 10)}`);
|
|
1224
|
+
console.log('');
|
|
1225
|
+
console.log(' Memory');
|
|
1226
|
+
console.log(` ${pad('currentState', 20)} ${statsCurrentState || noDataLabel}${statsStateInferred ? ' (inferido)' : ''}`);
|
|
1227
|
+
console.log(` ${pad('lastStep', 20)} ${lastStep || noDataLabel}`);
|
|
1228
|
+
console.log(` ${pad('criteriaRun', 20)} ${criteriaRun.length > 0 ? criteriaRun.join(', ') : noDataLabel}`);
|
|
1229
|
+
console.log(` ${pad('testCommand', 20)} ${testCommand || noDataLabel}`);
|
|
1230
|
+
console.log('');
|
|
1231
|
+
console.log(' Review');
|
|
1232
|
+
console.log(` ${pad('passed', 20)} ${reviewDate !== null ? 'Si' : 'No'}`);
|
|
1233
|
+
if (reviewVerdict) console.log(` ${pad('verdict', 20)} ${reviewVerdict}`);
|
|
1234
|
+
if (reviewDate) console.log(` ${pad('date', 20)} ${reviewDate}`);
|
|
1235
|
+
if (reviewDate) console.log(` ${pad('failCount', 20)} ${reviewFailCount}`);
|
|
1236
|
+
console.log('');
|
|
1237
|
+
console.log(' Compact telemetry (periodo del cambio)');
|
|
1238
|
+
console.log(` ${pad('eventos', 20)} ${compactStats.totalEvents}`);
|
|
1239
|
+
console.log(` ${pad('rewrites', 20)} ${compactStats.totalRewrites}`);
|
|
1240
|
+
console.log(` ${pad('tokens ahorrados', 20)} ${compactStats.totalSaved}`);
|
|
1241
|
+
console.log('');
|
|
1242
|
+
console.log(' CodeGraph telemetry (periodo del cambio)');
|
|
1243
|
+
console.log(` ${pad('eventos', 20)} ${codegraphStats.totalEvents}`);
|
|
1244
|
+
console.log(` ${pad('tool calls', 20)} ${codegraphStats.totalToolCalls}`);
|
|
1245
|
+
console.log(` ${pad('tokens ahorrados', 20)} ${codegraphStats.totalTokensSaved}`);
|
|
1246
|
+
console.log('');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
733
1249
|
function sddHelp() {
|
|
734
1250
|
console.log(`
|
|
735
1251
|
refacil-sdd-ai sdd — Gestión de artefactos SDD-AI
|
|
736
1252
|
|
|
737
1253
|
Subcomandos:
|
|
738
1254
|
sdd new-change <nombre> Crea un nuevo cambio con los 4 artefactos scaffold
|
|
739
|
-
sdd
|
|
740
|
-
|
|
1255
|
+
sdd sync-spec <nombre> Sincroniza specs del change a refacil-sdd/specs/<nombre>/spec.md
|
|
1256
|
+
(mismo idioma que los artefactos; sin traducción)
|
|
1257
|
+
[--from-archive] Lee el change desde refacil-sdd/changes/archive/*-<nombre>/
|
|
1258
|
+
sdd archive <nombre> Archiva un cambio (sync-spec automático + mueve a archive/)
|
|
1259
|
+
sdd list [--json] [--include-archived] Lista cambios activos con estado de review
|
|
741
1260
|
sdd status <nombre> [--json] Muestra estado de artefactos y tasks de un cambio
|
|
742
1261
|
sdd mark-reviewed <nombre> Escribe .review-passed con veredicto y resumen
|
|
743
1262
|
--verdict <v> Veredicto (ej: approved, approved-with-notes, rejected)
|
|
@@ -755,6 +1274,8 @@ function sddHelp() {
|
|
|
755
1274
|
[--touched-files <csv>] Archivos modificados (separados por coma)
|
|
756
1275
|
[--commands-run <value>] Comando de test ejecutado
|
|
757
1276
|
[--criteria-run <csv>] Criterios CA/CR ejecutados (separados por coma)
|
|
1277
|
+
[--state <estado>] Estado del ciclo SDD-AI (proposed, approved, apply-in-progress, applied, tested, verified, reviewed, archived)
|
|
1278
|
+
[--actor <nombre>] Actor que origina el cambio de estado (default: cli)
|
|
758
1279
|
sdd get-memory <nombre> Lee memory.yaml del cambio
|
|
759
1280
|
[--json] Salida en JSON (por defecto: YAML raw)
|
|
760
1281
|
sdd set-review-fails <nombre> Escribe .review-last-fails.json con archivos fallidos
|
|
@@ -769,11 +1290,20 @@ function sddHelp() {
|
|
|
769
1290
|
[--base-branch <branch>] Rama base para nuevos cambios
|
|
770
1291
|
[--protected-branches <csv>] Ramas protegidas (separadas por coma)
|
|
771
1292
|
[--artifact-language <language>] Idioma para los artefactos SDD generados (english, spanish)
|
|
1293
|
+
sdd stats <nombre> [--json] Muestra estadísticas del cambio: memoria (incluyendo currentState),
|
|
1294
|
+
review, compact telemetry y CodeGraph en el periodo del cambio
|
|
1295
|
+
sdd test-scope Resolves scoped test files for the given source files
|
|
1296
|
+
--files <csv> Comma-separated source file paths to scope tests for
|
|
1297
|
+
[--stack <name>] Stack hint (node, python, go, rust, java, dotnet)
|
|
1298
|
+
[--baseline <cmd>] Fallback test command when no test files are found
|
|
1299
|
+
[--json] Output result as JSON (testCommand, files, fallback, fallbackReason)
|
|
1300
|
+
Always exits 0.
|
|
772
1301
|
|
|
773
1302
|
Notas:
|
|
774
1303
|
- Los nombres de cambio deben empezar con minúscula y usar solo [a-z0-9-]
|
|
775
1304
|
- Si existe openspec/ y no existe refacil-sdd/, se migra automáticamente
|
|
776
|
-
- sdd archive
|
|
1305
|
+
- sdd archive ejecuta sync-spec antes de mover (preserva idioma de CA/CR del change)
|
|
1306
|
+
- sdd archive mueve el cambio completo (incluyendo memory.yaml si existe) al directorio archive/
|
|
777
1307
|
`);
|
|
778
1308
|
}
|
|
779
1309
|
|
|
@@ -790,6 +1320,9 @@ function handleSdd(sub, argv, projectRoot) {
|
|
|
790
1320
|
case 'archive':
|
|
791
1321
|
cmdArchive(args, root);
|
|
792
1322
|
break;
|
|
1323
|
+
case 'sync-spec':
|
|
1324
|
+
cmdSyncSpec(args, root);
|
|
1325
|
+
break;
|
|
793
1326
|
case 'list':
|
|
794
1327
|
cmdList(args, root);
|
|
795
1328
|
break;
|
|
@@ -823,10 +1356,30 @@ function handleSdd(sub, argv, projectRoot) {
|
|
|
823
1356
|
case 'write-config':
|
|
824
1357
|
cmdWriteConfig(args, root);
|
|
825
1358
|
break;
|
|
1359
|
+
case 'stats':
|
|
1360
|
+
cmdStats(args, root);
|
|
1361
|
+
break;
|
|
1362
|
+
case 'test-scope':
|
|
1363
|
+
cmdTestScope(args, root);
|
|
1364
|
+
break;
|
|
826
1365
|
default:
|
|
827
1366
|
sddHelp();
|
|
828
1367
|
process.exit(1);
|
|
829
1368
|
}
|
|
830
1369
|
}
|
|
831
1370
|
|
|
832
|
-
module.exports = {
|
|
1371
|
+
module.exports = {
|
|
1372
|
+
handleSdd,
|
|
1373
|
+
parseArgs,
|
|
1374
|
+
autoMigrateOpenspec,
|
|
1375
|
+
validateChangeName,
|
|
1376
|
+
isBugFixChangeName,
|
|
1377
|
+
resolveExistingChangeName,
|
|
1378
|
+
resolveArchivedChangeName,
|
|
1379
|
+
findProjectRoot,
|
|
1380
|
+
cmdWriteConfig,
|
|
1381
|
+
cmdStats,
|
|
1382
|
+
cmdTestScope,
|
|
1383
|
+
VALID_STATES,
|
|
1384
|
+
inferCurrentState,
|
|
1385
|
+
};
|