refacil-sdd-ai 5.2.3 → 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 +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 +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 +26 -4
- package/lib/ide-detection.js +1 -1
- package/lib/ignore-files.js +5 -1
- package/lib/installer.js +195 -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 +5 -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/compact.js
CHANGED
|
@@ -1,77 +1,210 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const compactTelemetry = require('../compact/telemetry');
|
|
4
|
+
const codegraphTelemetry = require('../codegraph-telemetry');
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const USD_PER_MTOK = 3;
|
|
7
|
+
|
|
8
|
+
function formatUsd(tokens) {
|
|
9
|
+
return ((tokens / 1_000_000) * USD_PER_MTOK).toFixed(2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatK(tokens) {
|
|
13
|
+
return (tokens / 1000).toFixed(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseCompactArgs(argv) {
|
|
17
|
+
const args = { _positional: [] };
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const token = argv[i];
|
|
20
|
+
if (!token) continue;
|
|
21
|
+
if (!token.startsWith('--')) {
|
|
22
|
+
args._positional.push(token);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const key = token.slice(2);
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
if (next === undefined || next.startsWith('--')) {
|
|
28
|
+
args[key] = true;
|
|
29
|
+
} else {
|
|
30
|
+
args[key] = next;
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
10
33
|
}
|
|
34
|
+
return args;
|
|
35
|
+
}
|
|
11
36
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
function parseBool(value, defaultValue = false) {
|
|
38
|
+
if (value === undefined || value === null) return defaultValue;
|
|
39
|
+
if (value === true) return true;
|
|
40
|
+
const s = String(value).toLowerCase();
|
|
41
|
+
if (s === 'true' || s === '1' || s === 'yes') return true;
|
|
42
|
+
if (s === 'false' || s === '0' || s === 'no') return false;
|
|
43
|
+
return defaultValue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function showCodegraphSection(cg) {
|
|
47
|
+
console.log(`\n CodeGraph — estimated savings by sub-agent\n`);
|
|
48
|
+
const sortedSkills = Object.entries(cg.bySkill)
|
|
49
|
+
.filter(([, data]) => data.withGraph.events > 0)
|
|
50
|
+
.sort((a, b) => b[1].withGraph.tokensSaved - a[1].withGraph.tokensSaved);
|
|
51
|
+
|
|
52
|
+
if (sortedSkills.length > 0) {
|
|
53
|
+
console.log(' With CodeGraph:');
|
|
54
|
+
for (const [skillId, data] of sortedSkills) {
|
|
55
|
+
const kTokens = formatK(data.withGraph.tokensSaved);
|
|
27
56
|
console.log(
|
|
28
|
-
` ${
|
|
57
|
+
` ${skillId.padEnd(18)} ${String(data.withGraph.events).padStart(5)} sessions ` +
|
|
58
|
+
`${String(data.withGraph.toolCalls).padStart(5)} tool calls ~${kTokens.padStart(7)}k tokens saved`,
|
|
29
59
|
);
|
|
30
60
|
}
|
|
31
|
-
const
|
|
32
|
-
const
|
|
61
|
+
const totalCgK = formatK(cg.totalTokensSaved);
|
|
62
|
+
const totalCgUsd = formatUsd(cg.totalTokensSaved);
|
|
33
63
|
console.log(` ${'-'.repeat(62)}`);
|
|
34
64
|
console.log(
|
|
35
|
-
` ${'Total
|
|
65
|
+
` ${'Total CodeGraph'.padEnd(18)} ${String(cg.totalEvents).padStart(5)} events ` +
|
|
66
|
+
`${String(cg.totalToolCalls).padStart(5)} tool calls ~${totalCgK.padStart(7)}k tokens (~$${totalCgUsd} USD)`,
|
|
36
67
|
);
|
|
37
|
-
console.log('');
|
|
38
68
|
}
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
const withoutGraph = Object.entries(cg.bySkill).filter(
|
|
71
|
+
([, data]) => data.withoutGraph.events > 0,
|
|
72
|
+
);
|
|
73
|
+
if (withoutGraph.length > 0) {
|
|
74
|
+
console.log('\n Sessions without graph (baseline):');
|
|
75
|
+
for (const [skillId, data] of withoutGraph) {
|
|
44
76
|
console.log(
|
|
45
|
-
` ${
|
|
77
|
+
` ${skillId.padEnd(18)} ${String(data.withoutGraph.events).padStart(5)} sessions`,
|
|
46
78
|
);
|
|
47
79
|
}
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`\n Log: ${codegraphTelemetry.LOG_PATH}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function showCompactStats() {
|
|
86
|
+
const s = compactTelemetry.stats();
|
|
87
|
+
const cg = codegraphTelemetry.stats();
|
|
88
|
+
const hasCompact = s.totalEvents > 0;
|
|
89
|
+
const hasCg = cg.totalEvents > 0;
|
|
90
|
+
|
|
91
|
+
if (!hasCompact && !hasCg) {
|
|
92
|
+
console.log('\n No hay eventos registrados todavia.');
|
|
93
|
+
console.log(' - Bash con hook compact-bash → compact.log');
|
|
94
|
+
console.log(
|
|
95
|
+
' - Tras sub-agentes: refacil-sdd-ai compact log-codegraph-event --skill <id> --has-graph true|false --tool-calls N --tokens N\n',
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (hasCompact) {
|
|
101
|
+
const sortedRewrites = Object.entries(s.byRule)
|
|
102
|
+
.filter(([, data]) => data.rewriteCount > 0)
|
|
103
|
+
.sort((a, b) => b[1].rewriteSaved - a[1].rewriteSaved);
|
|
104
|
+
const sortedAlreadyCompact = Object.entries(s.byRule)
|
|
105
|
+
.filter(([, data]) => data.alreadyCompactCount > 0)
|
|
106
|
+
.sort((a, b) => b[1].alreadyCompactPotential - a[1].alreadyCompactPotential);
|
|
107
|
+
|
|
108
|
+
console.log(`\n compact-bash stats\n`);
|
|
109
|
+
console.log(` Rewrites por hook: ${s.totalRewrites}`);
|
|
110
|
+
console.log(` Comandos ya compactos detectados (skill/agente): ${s.totalAlreadyCompact}\n`);
|
|
111
|
+
|
|
112
|
+
if (sortedRewrites.length > 0) {
|
|
113
|
+
console.log(' Ahorro aplicado por hook (rewrite):');
|
|
114
|
+
for (const [id, data] of sortedRewrites) {
|
|
115
|
+
const kTokens = formatK(data.rewriteSaved);
|
|
116
|
+
console.log(
|
|
117
|
+
` ${id.padEnd(18)} ${String(data.rewriteCount).padStart(6)} rewrites ~${kTokens.padStart(7)}k tokens`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const totalHookK = formatK(s.totalSaved);
|
|
121
|
+
const hookUsd = formatUsd(s.totalSaved);
|
|
122
|
+
console.log(` ${'-'.repeat(62)}`);
|
|
123
|
+
console.log(
|
|
124
|
+
` ${'Total hook'.padEnd(18)} ${String(s.totalRewrites).padStart(6)} rewrites ~${totalHookK.padStart(7)}k tokens (~$${hookUsd} USD)`,
|
|
125
|
+
);
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (sortedAlreadyCompact.length > 0) {
|
|
130
|
+
console.log(' Ahorro potencial ya capturado por skill/agente (sin rewrite):');
|
|
131
|
+
for (const [id, data] of sortedAlreadyCompact) {
|
|
132
|
+
const kTokens = formatK(data.alreadyCompactPotential);
|
|
133
|
+
console.log(
|
|
134
|
+
` ${id.padEnd(18)} ${String(data.alreadyCompactCount).padStart(6)} eventos ~${kTokens.padStart(7)}k tokens potenciales`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const totalAgentK = formatK(s.totalAlreadyCompactPotential);
|
|
138
|
+
const agentUsd = formatUsd(s.totalAlreadyCompactPotential);
|
|
139
|
+
console.log(` ${'-'.repeat(62)}`);
|
|
140
|
+
console.log(
|
|
141
|
+
` ${'Total skill'.padEnd(18)} ${String(s.totalAlreadyCompact).padStart(6)} eventos ~${totalAgentK.padStart(7)}k tokens (~$${agentUsd} USD)`,
|
|
142
|
+
);
|
|
143
|
+
console.log('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const compactK = formatK(s.totalObservedPotential);
|
|
147
|
+
const compactUsd = formatUsd(s.totalObservedPotential);
|
|
50
148
|
console.log(` ${'-'.repeat(62)}`);
|
|
51
149
|
console.log(
|
|
52
|
-
` ${'Total
|
|
150
|
+
` ${'Total compact'.padEnd(18)} ${String(s.totalEvents).padStart(6)} eventos ~${compactK.padStart(7)}k tokens (~$${compactUsd} USD, Sonnet input)`,
|
|
53
151
|
);
|
|
54
|
-
console.log(
|
|
152
|
+
console.log(`\n Log: ${compactTelemetry.LOG_PATH}`);
|
|
153
|
+
if (compactTelemetry.isDisabled()) {
|
|
154
|
+
console.log(' Estado: DESHABILITADO (no se registran nuevos eventos)');
|
|
155
|
+
}
|
|
55
156
|
}
|
|
56
157
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
console.log('
|
|
158
|
+
if (hasCg) {
|
|
159
|
+
showCodegraphSection(cg);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const grandTotal = s.totalObservedPotential + cg.totalTokensSaved;
|
|
163
|
+
if (hasCompact && hasCg) {
|
|
164
|
+
const grandK = formatK(grandTotal);
|
|
165
|
+
const grandUsd = formatUsd(grandTotal);
|
|
166
|
+
console.log(`\n ${'-'.repeat(62)}`);
|
|
167
|
+
console.log(
|
|
168
|
+
` ${'Total combinado'.padEnd(18)} compact + CodeGraph ~${grandK.padStart(7)}k tokens (~$${grandUsd} USD, Sonnet input)`,
|
|
169
|
+
);
|
|
170
|
+
} else if (!hasCompact && hasCg && cg.totalTokensSaved > 0) {
|
|
171
|
+
const grandK = formatK(grandTotal);
|
|
172
|
+
const grandUsd = formatUsd(grandTotal);
|
|
173
|
+
console.log(`\n ${'-'.repeat(62)}`);
|
|
174
|
+
console.log(
|
|
175
|
+
` ${'Total observado'.padEnd(18)} ${String(cg.totalEvents).padStart(6)} eventos ~${grandK.padStart(7)}k tokens (~$${grandUsd} USD, Sonnet input)`,
|
|
176
|
+
);
|
|
66
177
|
}
|
|
178
|
+
|
|
67
179
|
console.log('');
|
|
68
180
|
}
|
|
69
181
|
|
|
70
|
-
function
|
|
182
|
+
function cmdLogCodegraphEvent(argv) {
|
|
183
|
+
const args = parseCompactArgs(argv);
|
|
184
|
+
const skillId = args.skill || args._positional[0];
|
|
185
|
+
if (!skillId) {
|
|
186
|
+
console.error(
|
|
187
|
+
' Uso: refacil-sdd-ai compact log-codegraph-event --skill <id> --has-graph true|false [--tool-calls N] [--tokens N]',
|
|
188
|
+
);
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const hasGraph = parseBool(args['has-graph'], false);
|
|
194
|
+
const toolCalls = Math.max(0, parseInt(args['tool-calls'] || '0', 10) || 0);
|
|
195
|
+
const tokens = Math.max(0, parseInt(args.tokens || '0', 10) || 0);
|
|
196
|
+
|
|
197
|
+
codegraphTelemetry.logEvent(skillId, hasGraph, toolCalls, tokens);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function handleCompact(sub, argv = []) {
|
|
71
201
|
switch (sub) {
|
|
72
202
|
case 'stats':
|
|
73
203
|
showCompactStats();
|
|
74
204
|
break;
|
|
205
|
+
case 'log-codegraph-event':
|
|
206
|
+
cmdLogCodegraphEvent(argv);
|
|
207
|
+
break;
|
|
75
208
|
case 'disable':
|
|
76
209
|
compactTelemetry.disable();
|
|
77
210
|
console.log(' compact-bash deshabilitado. Reactiva con: refacil-sdd-ai compact enable');
|
|
@@ -84,9 +217,15 @@ function handleCompact(sub) {
|
|
|
84
217
|
compactTelemetry.clearLog();
|
|
85
218
|
console.log(' compact.log limpiado.');
|
|
86
219
|
break;
|
|
220
|
+
case 'codegraph-clear-log':
|
|
221
|
+
codegraphTelemetry.clearLog();
|
|
222
|
+
console.log(' codegraph.log limpiado.');
|
|
223
|
+
break;
|
|
87
224
|
default:
|
|
88
|
-
console.log(
|
|
225
|
+
console.log(
|
|
226
|
+
'Uso: refacil-sdd-ai compact <stats|log-codegraph-event|disable|enable|clear-log|codegraph-clear-log>',
|
|
227
|
+
);
|
|
89
228
|
}
|
|
90
229
|
}
|
|
91
230
|
|
|
92
|
-
module.exports = { handleCompact };
|
|
231
|
+
module.exports = { handleCompact, showCompactStats, cmdLogCodegraphEvent };
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { findProjectRoot } = require('./sdd');
|
|
6
|
+
const { parseFile, extractArtifactLanguageMeta } = require('../spec-reader/md-parser');
|
|
7
|
+
const { artifactLanguageToTtsCode } = require('../spec-reader/lang');
|
|
8
|
+
const { writeSession, writeFolderSession } = require('../spec-reader/session');
|
|
9
|
+
const busSpawn = require('../bus/spawn');
|
|
10
|
+
const { openInBrowser } = require('../open-browser');
|
|
11
|
+
const {
|
|
12
|
+
resolveDefaultTtsLang,
|
|
13
|
+
isValidTtsLang,
|
|
14
|
+
detectTtsLangFromContent,
|
|
15
|
+
DEFAULT_TTS_LANG,
|
|
16
|
+
} = require('../spec-reader/lang');
|
|
17
|
+
const { loadBranchConfigWithSources } = require('../config');
|
|
18
|
+
|
|
19
|
+
const DEFAULT_LANG = DEFAULT_TTS_LANG;
|
|
20
|
+
const DEFAULT_VOICE = 'M3';
|
|
21
|
+
const DEFAULT_SPEED = 1;
|
|
22
|
+
const VALID_VOICES = new Set(['M1', 'M2', 'M3', 'M4', 'M5', 'F1', 'F2', 'F3', 'F4', 'F5']);
|
|
23
|
+
|
|
24
|
+
function parseReadSpecArgs(argv) {
|
|
25
|
+
const args = {};
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
const token = argv[i];
|
|
28
|
+
if (!token || !token.startsWith('--')) continue;
|
|
29
|
+
const key = token.slice(2);
|
|
30
|
+
const next = argv[i + 1];
|
|
31
|
+
if (next === undefined || next.startsWith('--')) {
|
|
32
|
+
args[key] = true;
|
|
33
|
+
} else {
|
|
34
|
+
args[key] = next;
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return args;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function printHelp() {
|
|
42
|
+
console.log(`
|
|
43
|
+
read-spec — Listen to a Markdown spec in the browser (on-device TTS)
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
refacil-sdd-ai read-spec --file <path> [options]
|
|
47
|
+
refacil-sdd-ai read-spec --change <changeName> [--select <file.md>] [options]
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--file <path> Markdown file (must be inside project root)
|
|
51
|
+
--change <changeName> Load all SDD artifacts for a change folder
|
|
52
|
+
--select <file.md> Pre-select a specific file in folder mode (default: proposal.md)
|
|
53
|
+
--lang <code> TTS language (default: SDD artifactLanguage → es/en)
|
|
54
|
+
--voice <id> Voice style M1–M5 or F1–F5 (default: ${DEFAULT_VOICE})
|
|
55
|
+
--speed <n> Speech speed 0.9–1.5 (default: ${DEFAULT_SPEED})
|
|
56
|
+
--help Show this help
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} fileArg
|
|
62
|
+
* @param {string} projectRoot
|
|
63
|
+
* @returns {string} absolute path
|
|
64
|
+
*/
|
|
65
|
+
function resolveFileWithinProject(fileArg, projectRoot) {
|
|
66
|
+
const resolved = path.resolve(projectRoot, fileArg);
|
|
67
|
+
const rootNorm = path.resolve(projectRoot) + path.sep;
|
|
68
|
+
if (!resolved.startsWith(rootNorm) && resolved !== path.resolve(projectRoot)) {
|
|
69
|
+
throw new Error('Security: --file must resolve inside the project root');
|
|
70
|
+
}
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Canonical ordering for SDD artifact files in a change folder.
|
|
76
|
+
* Files not in this list are appended alphabetically after.
|
|
77
|
+
*/
|
|
78
|
+
const FOLDER_FILE_ORDER = ['proposal.md', 'design.md', 'tasks.md', 'specs.md'];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Sort .md filenames: canonical order first, then remaining alphabetically.
|
|
82
|
+
* @param {string[]} names
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
*/
|
|
85
|
+
function sortFolderFiles(names) {
|
|
86
|
+
const canonical = FOLDER_FILE_ORDER.filter((n) => names.includes(n));
|
|
87
|
+
const rest = names
|
|
88
|
+
.filter((n) => !FOLDER_FILE_ORDER.includes(n))
|
|
89
|
+
.sort((a, b) => a.localeCompare(b));
|
|
90
|
+
return [...canonical, ...rest];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string[]} argv
|
|
95
|
+
* @param {string} packageRoot
|
|
96
|
+
* @param {string} [projectRootOverride]
|
|
97
|
+
*/
|
|
98
|
+
async function handleReadSpec(argv, packageRoot, projectRootOverride) {
|
|
99
|
+
const args = parseReadSpecArgs(argv || []);
|
|
100
|
+
if (args.help) {
|
|
101
|
+
printHelp();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Mutual exclusivity: --change and --file cannot be used together
|
|
106
|
+
if (args.change && args.file) {
|
|
107
|
+
console.error(' Error: --change y --file son mutuamente excluyentes. Usa solo uno de los dos.');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Neither --change nor --file provided
|
|
112
|
+
if (!args.change && !args.file) {
|
|
113
|
+
console.error(' Error: --file is required');
|
|
114
|
+
printHelp();
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const projectRoot = projectRootOverride || findProjectRoot();
|
|
119
|
+
|
|
120
|
+
// -----------------------------------------------------------------------
|
|
121
|
+
// FOLDER MODE: --change <changeName>
|
|
122
|
+
// -----------------------------------------------------------------------
|
|
123
|
+
if (args.change) {
|
|
124
|
+
const changeName = String(args.change);
|
|
125
|
+
const changeFolder = path.join(projectRoot, 'refacil-sdd', 'changes', changeName);
|
|
126
|
+
|
|
127
|
+
const changesRoot = path.join(projectRoot, 'refacil-sdd', 'changes') + path.sep;
|
|
128
|
+
if (!changeFolder.startsWith(changesRoot)) {
|
|
129
|
+
console.error(' Error: --change must be a simple folder name (no path separators).');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(changeFolder)) {
|
|
134
|
+
console.error(` Error: no se encontró la carpeta de cambio '${changeName}' en refacil-sdd/changes/.`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Collect .md files in the change folder
|
|
139
|
+
let allEntries;
|
|
140
|
+
try {
|
|
141
|
+
allEntries = fs.readdirSync(changeFolder);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(` Error: could not read change folder: ${err.message}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const mdFiles = allEntries.filter((n) => n.endsWith('.md'));
|
|
147
|
+
|
|
148
|
+
if (mdFiles.length === 0) {
|
|
149
|
+
console.error(` Error: la carpeta '${changeName}' no contiene archivos Markdown.`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const sortedFiles = sortFolderFiles(mdFiles);
|
|
154
|
+
|
|
155
|
+
// Validate --select if provided
|
|
156
|
+
const selectArg = typeof args.select === 'string' ? args.select : 'proposal.md';
|
|
157
|
+
const resolvedSelect = sortedFiles.includes(selectArg) ? selectArg : null;
|
|
158
|
+
if (typeof args.select === 'string' && !resolvedSelect) {
|
|
159
|
+
console.error(` Error: el archivo '${args.select}' no existe en la carpeta de cambio '${changeName}'.`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
if (!resolvedSelect && typeof args.select !== 'string') {
|
|
163
|
+
console.warn(` Note: proposal.md not found in '${changeName}', opening '${sortedFiles[0]}' instead.`);
|
|
164
|
+
}
|
|
165
|
+
const selectedFile = resolvedSelect || sortedFiles[0];
|
|
166
|
+
|
|
167
|
+
const voice = typeof args.voice === 'string' ? args.voice.toUpperCase() : DEFAULT_VOICE;
|
|
168
|
+
const speedRaw = args.speed !== undefined ? parseFloat(args.speed) : DEFAULT_SPEED;
|
|
169
|
+
|
|
170
|
+
if (!VALID_VOICES.has(voice)) {
|
|
171
|
+
console.error(` Error: invalid --voice "${args.voice}". Use M1–M5 or F1–F5`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
if (Number.isNaN(speedRaw) || speedRaw < 0.9 || speedRaw > 1.5) {
|
|
175
|
+
console.error(' Error: --speed must be between 0.9 and 1.5');
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { artifactLanguage } = loadBranchConfigWithSources(projectRoot);
|
|
180
|
+
let resolvedLang = typeof args.lang === 'string' ? args.lang : null;
|
|
181
|
+
|
|
182
|
+
// Parse each file and collect sections
|
|
183
|
+
const files = [];
|
|
184
|
+
const allContents = [];
|
|
185
|
+
for (const name of sortedFiles) {
|
|
186
|
+
const absPath = path.join(changeFolder, name);
|
|
187
|
+
const relPath = path.relative(projectRoot, absPath).replace(/\\/g, '/');
|
|
188
|
+
let sections;
|
|
189
|
+
let fileContent;
|
|
190
|
+
try {
|
|
191
|
+
fileContent = fs.readFileSync(absPath, 'utf8');
|
|
192
|
+
sections = parseFile(absPath);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error(` Error parsing ${name}: ${err.message}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
// Explicit meta comment takes priority (first file that has one wins)
|
|
198
|
+
if (!resolvedLang) {
|
|
199
|
+
const metaFromFile = extractArtifactLanguageMeta(fileContent);
|
|
200
|
+
if (metaFromFile) {
|
|
201
|
+
resolvedLang = artifactLanguageToTtsCode(metaFromFile);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
allContents.push(fileContent);
|
|
205
|
+
files.push({ name, relPath, sections });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!resolvedLang) {
|
|
209
|
+
// Content-based detection: inspect heading patterns across all files
|
|
210
|
+
resolvedLang = detectTtsLangFromContent(allContents.join('\n'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!resolvedLang) {
|
|
214
|
+
resolvedLang = resolveDefaultTtsLang(projectRoot);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!isValidTtsLang(resolvedLang)) {
|
|
218
|
+
console.error(` Error: invalid --lang "${resolvedLang}". Use a Supertonic code (e.g. es, en).`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const relativeChangeFolder = path.relative(projectRoot, changeFolder).replace(/\\/g, '/');
|
|
223
|
+
|
|
224
|
+
const sessionId = writeFolderSession({
|
|
225
|
+
files,
|
|
226
|
+
selectedFile,
|
|
227
|
+
meta: {
|
|
228
|
+
lang: resolvedLang,
|
|
229
|
+
voice,
|
|
230
|
+
speed: speedRaw,
|
|
231
|
+
changeName,
|
|
232
|
+
artifactLanguage,
|
|
233
|
+
},
|
|
234
|
+
sourcePath: relativeChangeFolder,
|
|
235
|
+
createdAt: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
let info;
|
|
239
|
+
try {
|
|
240
|
+
({ info } = await busSpawn.ensureBroker(packageRoot));
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error(` Error: could not start broker: ${err.message}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const url = `http://127.0.0.1:${info.port}/read-spec?id=${sessionId}`;
|
|
247
|
+
console.log(` read-spec session: ${sessionId}`);
|
|
248
|
+
console.log(` Open: ${url}`);
|
|
249
|
+
|
|
250
|
+
const opened = openInBrowser(url);
|
|
251
|
+
if (!opened) {
|
|
252
|
+
console.log(' (could not open browser automatically — open the URL above manually)');
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -----------------------------------------------------------------------
|
|
258
|
+
// FILE MODE: --file <path>
|
|
259
|
+
// -----------------------------------------------------------------------
|
|
260
|
+
let filePath;
|
|
261
|
+
try {
|
|
262
|
+
filePath = resolveFileWithinProject(args.file, projectRoot);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(` Error: ${err.message}`);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!fs.existsSync(filePath)) {
|
|
269
|
+
console.error(` Error: file not found: ${args.file}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (path.extname(filePath).toLowerCase() !== '.md') {
|
|
274
|
+
console.error(' Error: --file must be a .md Markdown file');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const voice = typeof args.voice === 'string' ? args.voice.toUpperCase() : DEFAULT_VOICE;
|
|
279
|
+
const speedRaw = args.speed !== undefined ? parseFloat(args.speed) : DEFAULT_SPEED;
|
|
280
|
+
|
|
281
|
+
if (!VALID_VOICES.has(voice)) {
|
|
282
|
+
console.error(` Error: invalid --voice "${args.voice}". Use M1–M5 or F1–F5`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
if (Number.isNaN(speedRaw) || speedRaw < 0.9 || speedRaw > 1.5) {
|
|
286
|
+
console.error(' Error: --speed must be between 0.9 and 1.5');
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let sections;
|
|
291
|
+
let fileContent;
|
|
292
|
+
try {
|
|
293
|
+
fileContent = fs.readFileSync(filePath, 'utf8');
|
|
294
|
+
sections = parseFile(filePath);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error(` Error parsing Markdown: ${err.message}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const relativeSource = path.relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
301
|
+
const { artifactLanguage } = loadBranchConfigWithSources(projectRoot);
|
|
302
|
+
const metaFromFile = extractArtifactLanguageMeta(fileContent);
|
|
303
|
+
const resolvedLang = typeof args.lang === 'string'
|
|
304
|
+
? args.lang
|
|
305
|
+
: (metaFromFile
|
|
306
|
+
? artifactLanguageToTtsCode(metaFromFile)
|
|
307
|
+
: (detectTtsLangFromContent(fileContent) || resolveDefaultTtsLang(projectRoot)));
|
|
308
|
+
if (!isValidTtsLang(resolvedLang)) {
|
|
309
|
+
console.error(` Error: invalid --lang "${resolvedLang}". Use a Supertonic code (e.g. es, en).`);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const sessionId = writeSession({
|
|
314
|
+
sections,
|
|
315
|
+
meta: {
|
|
316
|
+
lang: resolvedLang,
|
|
317
|
+
voice,
|
|
318
|
+
speed: speedRaw,
|
|
319
|
+
artifactLanguage: metaFromFile || artifactLanguage,
|
|
320
|
+
},
|
|
321
|
+
sourcePath: relativeSource,
|
|
322
|
+
createdAt: new Date().toISOString(),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
let info;
|
|
326
|
+
try {
|
|
327
|
+
({ info } = await busSpawn.ensureBroker(packageRoot));
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.error(` Error: could not start broker: ${err.message}`);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const url = `http://127.0.0.1:${info.port}/read-spec?id=${sessionId}`;
|
|
334
|
+
console.log(` read-spec session: ${sessionId}`);
|
|
335
|
+
console.log(` Open: ${url}`);
|
|
336
|
+
|
|
337
|
+
const opened = openInBrowser(url);
|
|
338
|
+
if (!opened) {
|
|
339
|
+
console.log(' (could not open browser automatically — open the URL above manually)');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = {
|
|
344
|
+
handleReadSpec,
|
|
345
|
+
parseReadSpecArgs,
|
|
346
|
+
sortFolderFiles,
|
|
347
|
+
FOLDER_FILE_ORDER,
|
|
348
|
+
printHelp,
|
|
349
|
+
DEFAULT_LANG,
|
|
350
|
+
DEFAULT_VOICE,
|
|
351
|
+
DEFAULT_SPEED,
|
|
352
|
+
};
|