@nac3/forge-cli 0.2.0-alpha.2 → 0.2.0-alpha.20
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/dist/bin/yf.d.ts.map +1 -1
- package/dist/bin/yf.js +25 -0
- package/dist/bin/yf.js.map +1 -1
- package/dist/chat/claude.d.ts +22 -15
- package/dist/chat/claude.d.ts.map +1 -1
- package/dist/chat/claude.js +75 -22
- package/dist/chat/claude.js.map +1 -1
- package/dist/chat/panel.d.ts.map +1 -1
- package/dist/chat/panel.js +90 -3
- package/dist/chat/panel.js.map +1 -1
- package/dist/chat/server.js +498 -32
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/tools/audit_consumers.d.ts +66 -0
- package/dist/chat/tools/audit_consumers.d.ts.map +1 -0
- package/dist/chat/tools/audit_consumers.js +231 -0
- package/dist/chat/tools/audit_consumers.js.map +1 -0
- package/dist/chat/tools/git.js +4 -4
- package/dist/chat/tools/github.js +3 -3
- package/dist/chat/tools/lifecycle.js +3 -3
- package/dist/chat/tools/manual.js +1 -1
- package/dist/chat/tools/reader.js +8 -8
- package/dist/chat/tools/workflow.d.ts +45 -0
- package/dist/chat/tools/workflow.d.ts.map +1 -0
- package/dist/chat/tools/workflow.js +404 -0
- package/dist/chat/tools/workflow.js.map +1 -0
- package/dist/chat/tools.d.ts.map +1 -1
- package/dist/chat/tools.js +23 -4
- package/dist/chat/tools.js.map +1 -1
- package/dist/commands/approve.d.ts +32 -0
- package/dist/commands/approve.d.ts.map +1 -0
- package/dist/commands/approve.js +198 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/block.d.ts +28 -0
- package/dist/commands/block.d.ts.map +1 -0
- package/dist/commands/block.js +189 -0
- package/dist/commands/block.js.map +1 -0
- package/dist/commands/bootstrap.d.ts +35 -0
- package/dist/commands/bootstrap.d.ts.map +1 -0
- package/dist/commands/bootstrap.js +205 -0
- package/dist/commands/bootstrap.js.map +1 -0
- package/dist/commands/chat.d.ts +3 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +46 -1
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/clarify.d.ts +30 -0
- package/dist/commands/clarify.d.ts.map +1 -0
- package/dist/commands/clarify.js +671 -0
- package/dist/commands/clarify.js.map +1 -0
- package/dist/commands/discover.d.ts +30 -0
- package/dist/commands/discover.d.ts.map +1 -0
- package/dist/commands/discover.js +178 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/doctor.js +94 -42
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/keys_setup.d.ts +53 -0
- package/dist/commands/keys_setup.d.ts.map +1 -0
- package/dist/commands/keys_setup.js +487 -0
- package/dist/commands/keys_setup.js.map +1 -0
- package/dist/commands/legacy-audit.d.ts +34 -0
- package/dist/commands/legacy-audit.d.ts.map +1 -0
- package/dist/commands/legacy-audit.js +270 -0
- package/dist/commands/legacy-audit.js.map +1 -0
- package/dist/commands/license.d.ts.map +1 -1
- package/dist/commands/license.js +41 -0
- package/dist/commands/license.js.map +1 -1
- package/dist/commands/operate.d.ts +22 -0
- package/dist/commands/operate.d.ts.map +1 -0
- package/dist/commands/operate.js +483 -0
- package/dist/commands/operate.js.map +1 -0
- package/dist/commands/spec.d.ts +38 -0
- package/dist/commands/spec.d.ts.map +1 -0
- package/dist/commands/spec.js +256 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/triage.d.ts +34 -0
- package/dist/commands/triage.d.ts.map +1 -0
- package/dist/commands/triage.js +228 -0
- package/dist/commands/triage.js.map +1 -0
- package/dist/commands/vault-inventory.d.ts +30 -0
- package/dist/commands/vault-inventory.d.ts.map +1 -0
- package/dist/commands/vault-inventory.js +214 -0
- package/dist/commands/vault-inventory.js.map +1 -0
- package/dist/commands/vault.d.ts.map +1 -1
- package/dist/commands/vault.js +5 -0
- package/dist/commands/vault.js.map +1 -1
- package/dist/commands/voice.js +1 -1
- package/dist/commands/voice.js.map +1 -1
- package/dist/commands/workflow-coverage.d.ts +30 -0
- package/dist/commands/workflow-coverage.d.ts.map +1 -0
- package/dist/commands/workflow-coverage.js +138 -0
- package/dist/commands/workflow-coverage.js.map +1 -0
- package/dist/core/keys_envelope.d.ts +13 -0
- package/dist/core/keys_envelope.d.ts.map +1 -1
- package/dist/core/keys_envelope.js.map +1 -1
- package/dist/deploy/adapter.d.ts +93 -0
- package/dist/deploy/adapter.d.ts.map +1 -0
- package/dist/deploy/adapter.js +42 -0
- package/dist/deploy/adapter.js.map +1 -0
- package/dist/deploy/aws_adapter.d.ts +28 -0
- package/dist/deploy/aws_adapter.d.ts.map +1 -0
- package/dist/deploy/aws_adapter.js +65 -0
- package/dist/deploy/aws_adapter.js.map +1 -0
- package/dist/deploy/cloudflare.d.ts +24 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -0
- package/dist/deploy/cloudflare.js +169 -0
- package/dist/deploy/cloudflare.js.map +1 -0
- package/dist/license/hito4_client.d.ts +17 -1
- package/dist/license/hito4_client.d.ts.map +1 -1
- package/dist/license/hito4_client.js +71 -10
- package/dist/license/hito4_client.js.map +1 -1
- package/dist/license/index.d.ts.map +1 -1
- package/dist/license/index.js +7 -0
- package/dist/license/index.js.map +1 -1
- package/dist/license/sync.d.ts +54 -0
- package/dist/license/sync.d.ts.map +1 -0
- package/dist/license/sync.js +131 -0
- package/dist/license/sync.js.map +1 -0
- package/dist/telemetry/usage.d.ts +67 -0
- package/dist/telemetry/usage.d.ts.map +1 -0
- package/dist/telemetry/usage.js +208 -0
- package/dist/telemetry/usage.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/voice/intents.d.ts +1 -1
- package/dist/voice/intents.js +0 -0
- package/dist/voice/providers/google.d.ts.map +1 -1
- package/dist/voice/providers/google.js +157 -27
- package/dist/voice/providers/google.js.map +1 -1
- package/dist/voice/router.d.ts +10 -0
- package/dist/voice/router.d.ts.map +1 -1
- package/dist/voice/router.js +39 -20
- package/dist/voice/router.js.map +1 -1
- package/dist/voice/types.d.ts +5 -2
- package/dist/voice/types.d.ts.map +1 -1
- package/dist/voice/types.js.map +1 -1
- package/dist/workflow/state.d.ts +190 -0
- package/dist/workflow/state.d.ts.map +1 -0
- package/dist/workflow/state.js +119 -0
- package/dist/workflow/state.js.map +1 -0
- package/package.json +13 -15
- package/templates/nextjs-app/README.md +48 -0
- package/templates/nextjs-app/next.config.js +8 -0
- package/templates/nextjs-app/package.json +33 -0
- package/templates/nextjs-app/src/app/globals.css +43 -0
- package/templates/nextjs-app/src/app/layout.tsx +29 -0
- package/templates/nextjs-app/src/app/page.tsx +63 -0
- package/templates/nextjs-app/src/nac/manifest.ts +36 -0
- package/templates/nextjs-app/tsconfig.json +21 -0
- package/templates/nextjs-app/yujin.forge.json +11 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yf clarify <round>` -- Phase II of PRODUCT_WORKFLOW.md
|
|
3
|
+
* (steps 4 through 9).
|
|
4
|
+
*
|
|
5
|
+
* Six rounds of structured clarification:
|
|
6
|
+
*
|
|
7
|
+
* functional step 4: user stories, MVP slice, personas
|
|
8
|
+
* quantitative step 5: volumetria, paralelismo, complejidad
|
|
9
|
+
* structural step 6: persistencia, middleware, lifecycle, workers
|
|
10
|
+
* governance step 7: compliance, audit, retention, RBAC, DR
|
|
11
|
+
* operational step 8: i18n, a11y, cost, monitoring, SLA
|
|
12
|
+
* blocks step 9: descompose system + dep graph + MVP path
|
|
13
|
+
*
|
|
14
|
+
* Each round writes a markdown doc to
|
|
15
|
+
* docs/clarification/0X_<round>.md inside the user's project,
|
|
16
|
+
* and a structured summary to workflow.clarification.<round>.
|
|
17
|
+
*
|
|
18
|
+
* The 6 rounds run in order. Forge refuses to run round N+1
|
|
19
|
+
* until round N is complete. Skip is allowed only when triage
|
|
20
|
+
* tier === simple AND the round is not blocks (which everyone
|
|
21
|
+
* needs).
|
|
22
|
+
*
|
|
23
|
+
* ASCII-only.
|
|
24
|
+
*/
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { promises as fs } from 'node:fs';
|
|
27
|
+
import prompts from 'prompts';
|
|
28
|
+
import { c, header } from '../ui/colors.js';
|
|
29
|
+
import { readWorkflow, patchWorkflowGroup, } from '../workflow/state.js';
|
|
30
|
+
const ROUND_NUMBER = {
|
|
31
|
+
functional: '04',
|
|
32
|
+
quantitative: '05',
|
|
33
|
+
structural: '06',
|
|
34
|
+
governance: '07',
|
|
35
|
+
operational: '08',
|
|
36
|
+
blocks: '09',
|
|
37
|
+
};
|
|
38
|
+
const ROUND_STATE_KEY = {
|
|
39
|
+
functional: 'functional',
|
|
40
|
+
quantitative: 'nfr_quantitative',
|
|
41
|
+
structural: 'nfr_structural',
|
|
42
|
+
governance: 'nfr_governance',
|
|
43
|
+
operational: 'nfr_operational',
|
|
44
|
+
blocks: 'blocks',
|
|
45
|
+
};
|
|
46
|
+
const PRE_REQ = {
|
|
47
|
+
functional: null,
|
|
48
|
+
quantitative: 'functional',
|
|
49
|
+
structural: 'quantitative',
|
|
50
|
+
governance: 'structural',
|
|
51
|
+
operational: 'governance',
|
|
52
|
+
blocks: 'operational',
|
|
53
|
+
};
|
|
54
|
+
async function ensureClarificationDir(projectRoot) {
|
|
55
|
+
const dir = path.join(projectRoot, 'docs', 'clarification');
|
|
56
|
+
await fs.mkdir(dir, { recursive: true });
|
|
57
|
+
return dir;
|
|
58
|
+
}
|
|
59
|
+
async function writeRoundDoc(projectRoot, round, title, bodyMd) {
|
|
60
|
+
const dir = await ensureClarificationDir(projectRoot);
|
|
61
|
+
const filename = ROUND_NUMBER[round] + '_' + round + '.md';
|
|
62
|
+
const fullPath = path.join(dir, filename);
|
|
63
|
+
const stamp = new Date().toISOString();
|
|
64
|
+
const md = `# ${title}\n\n*Generated by \`yf clarify ${round}\` on ${stamp}.*\n\n${bodyMd}\n`;
|
|
65
|
+
await fs.writeFile(fullPath, md, 'utf-8');
|
|
66
|
+
return path.relative(projectRoot, fullPath);
|
|
67
|
+
}
|
|
68
|
+
/* =========================================================================
|
|
69
|
+
* Round 4 -- functional
|
|
70
|
+
* ========================================================================= */
|
|
71
|
+
async function runFunctional(projectRoot) {
|
|
72
|
+
console.log(c.dim('Ronda 4/6 -- relevamiento funcional. ~10 preguntas.'));
|
|
73
|
+
console.log('');
|
|
74
|
+
const a = await prompts([
|
|
75
|
+
{ type: 'text', name: 'vision', message: 'En 1-2 frases: cual es la vision del producto?' },
|
|
76
|
+
{ type: 'text', name: 'pain_solved', message: 'Que dolor concreto resuelve hoy?' },
|
|
77
|
+
{ type: 'text', name: 'personas', message: 'Quien lo usa? (separados por coma; ej: "PM, dev junior, ops")' },
|
|
78
|
+
{ type: 'text', name: 'flagship_features', message: 'Top 3 killer features (separadas por coma):' },
|
|
79
|
+
{ type: 'text', name: 'mvp_slice', message: 'El slice mas chico que justifica un launch (1 frase):' },
|
|
80
|
+
{ type: 'text', name: 'success_metrics', message: 'Que metric mide exito? (ej: "DAU > 100", "checkout > 80%")' },
|
|
81
|
+
{ type: 'text', name: 'out_of_scope', message: 'Cosas explicitamente fuera de scope (separadas por coma):' },
|
|
82
|
+
{ type: 'select', name: 'launch_horizon', message: 'Horizonte de lanzamiento:',
|
|
83
|
+
choices: [
|
|
84
|
+
{ title: 'Esta semana', value: 'this_week' },
|
|
85
|
+
{ title: 'Este mes', value: 'this_month' },
|
|
86
|
+
{ title: 'Este trimestre', value: 'quarter' },
|
|
87
|
+
{ title: 'No urgente', value: 'no_rush' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{ type: 'select', name: 'comparable_product', message: 'Producto comparable que ya conoces:',
|
|
91
|
+
choices: [
|
|
92
|
+
{ title: 'Otro SaaS similar (decime cual)', value: 'saas' },
|
|
93
|
+
{ title: 'Una herramienta interna', value: 'internal' },
|
|
94
|
+
{ title: 'No hay comparable directo', value: 'novel' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{ type: 'text', name: 'comparable_name', message: 'Nombre del comparable (vacio si "novel"):', initial: '' },
|
|
98
|
+
]);
|
|
99
|
+
if (!a.vision) {
|
|
100
|
+
console.log(c.warn('Cancelado.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const bodyMd = [
|
|
104
|
+
'## Vision', a.vision, '',
|
|
105
|
+
'## Pain solved', a.pain_solved, '',
|
|
106
|
+
'## Personas', a.personas.split(',').map((p) => '- ' + p.trim()).join('\n'), '',
|
|
107
|
+
'## Killer features', a.flagship_features.split(',').map((f) => '- ' + f.trim()).join('\n'), '',
|
|
108
|
+
'## MVP slice', a.mvp_slice, '',
|
|
109
|
+
'## Success metrics', a.success_metrics, '',
|
|
110
|
+
'## Out of scope', a.out_of_scope ? a.out_of_scope.split(',').map((o) => '- ' + o.trim()).join('\n') : '_none declared_', '',
|
|
111
|
+
'## Launch horizon', '- ' + a.launch_horizon, '',
|
|
112
|
+
'## Comparable product', '- type: ' + a.comparable_product, '- name: ' + (a.comparable_name || '(none)'), '',
|
|
113
|
+
].join('\n');
|
|
114
|
+
const docPath = await writeRoundDoc(projectRoot, 'functional', 'Functional Clarification (step 4)', bodyMd);
|
|
115
|
+
await patchWorkflowGroup(projectRoot, 'clarification', {
|
|
116
|
+
functional: {
|
|
117
|
+
done_at: new Date().toISOString(),
|
|
118
|
+
summary: a.vision,
|
|
119
|
+
doc_path: docPath,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(c.success('Round functional persistido: ') + c.brand(docPath));
|
|
124
|
+
console.log(c.dim('Siguiente paso: ') + c.code('yf clarify quantitative'));
|
|
125
|
+
}
|
|
126
|
+
/* =========================================================================
|
|
127
|
+
* Round 5 -- quantitative NFR
|
|
128
|
+
* ========================================================================= */
|
|
129
|
+
async function runQuantitative(projectRoot) {
|
|
130
|
+
console.log(c.dim('Ronda 5/6 -- NFR cuantitativas. Estas respuestas DIRECTAMENTE'));
|
|
131
|
+
console.log(c.dim('disparan decisiones de arquitectura.'));
|
|
132
|
+
console.log('');
|
|
133
|
+
const a = await prompts([
|
|
134
|
+
{ type: 'number', name: 'users_launch', message: 'Usuarios concurrentes esperados AL lanzamiento:', initial: 10, min: 0 },
|
|
135
|
+
{ type: 'number', name: 'users_12m', message: 'Usuarios concurrentes esperados a 12 meses:', initial: 100, min: 0 },
|
|
136
|
+
{ type: 'number', name: 'rps_peak', message: 'Requests/segundo en pico esperado:', initial: 5, min: 0 },
|
|
137
|
+
{ type: 'select', name: 'data_volume', message: 'Volumen de datos (tabla mas grande):',
|
|
138
|
+
choices: [
|
|
139
|
+
{ title: '< 100k filas', value: 'small' },
|
|
140
|
+
{ title: '100k a 10M filas', value: 'medium' },
|
|
141
|
+
{ title: '10M a 1B filas', value: 'large' },
|
|
142
|
+
{ title: '> 1B filas', value: 'huge' },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
{ type: 'select', name: 'data_growth', message: 'Crecimiento esperado:',
|
|
146
|
+
choices: [
|
|
147
|
+
{ title: 'Lineal y modesto (< 1 GB/mes)', value: 'slow' },
|
|
148
|
+
{ title: 'Moderado (1-50 GB/mes)', value: 'medium' },
|
|
149
|
+
{ title: 'Rapido (> 50 GB/mes)', value: 'fast' },
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{ type: 'select', name: 'sync_async', message: 'Operaciones predominantes:',
|
|
153
|
+
choices: [
|
|
154
|
+
{ title: 'Sincronicas (responder al request)', value: 'sync' },
|
|
155
|
+
{ title: 'Asincronicas / background', value: 'async' },
|
|
156
|
+
{ title: 'Mix balanceado', value: 'mixed' },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
{ type: 'select', name: 'realtime', message: 'Necesidad de tiempo real:',
|
|
160
|
+
choices: [
|
|
161
|
+
{ title: 'No, request-response basta', value: 'none' },
|
|
162
|
+
{ title: 'Notificaciones push esporadicas', value: 'push' },
|
|
163
|
+
{ title: 'Chat / colab en vivo (websockets)', value: 'websocket' },
|
|
164
|
+
{ title: 'Streaming continuo (audio/video)', value: 'stream' },
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
{ type: 'select', name: 'batch', message: 'Batch / procesos nocturnos:',
|
|
168
|
+
choices: [
|
|
169
|
+
{ title: 'Ninguno', value: 'none' },
|
|
170
|
+
{ title: 'ETL ocasional', value: 'occasional' },
|
|
171
|
+
{ title: 'Nightly heavy (>1h)', value: 'heavy' },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
{ type: 'number', name: 'process_steps', message: 'Flujo de negocio mas largo: cuantos pasos tiene?', initial: 3, min: 1 },
|
|
175
|
+
{ type: 'number', name: 'process_decisions', message: 'Cuantas decisiones / ramificaciones tiene?', initial: 1, min: 0 },
|
|
176
|
+
{ type: 'number', name: 'process_actors', message: 'Cuantos actores intervienen?', initial: 1, min: 1 },
|
|
177
|
+
]);
|
|
178
|
+
if (a.users_launch === undefined) {
|
|
179
|
+
console.log(c.warn('Cancelado.'));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
/* Derive arch decisions per PRODUCT_WORKFLOW sec 5 table. */
|
|
183
|
+
const decisions = [];
|
|
184
|
+
if (a.process_steps > 5 && a.process_decisions >= 2) {
|
|
185
|
+
decisions.push('**Workflow engine RECOMENDADO** -- proceso con > 5 pasos + ramificaciones. Sugerencia: temporal / inngest / xstate.');
|
|
186
|
+
}
|
|
187
|
+
if (a.process_actors >= 3) {
|
|
188
|
+
decisions.push('**Object lifecycle states explicito** -- multiples actores tocando entidades. Sugerencia: state machine + audit log.');
|
|
189
|
+
}
|
|
190
|
+
if (a.batch !== 'none' || a.realtime === 'stream') {
|
|
191
|
+
decisions.push('**Workers async** -- presencia de batch o streaming. Sugerencia: bull / sidekiq / temporal.');
|
|
192
|
+
}
|
|
193
|
+
if (a.batch === 'heavy' || a.batch === 'occasional') {
|
|
194
|
+
decisions.push('**Cron jobs** -- batch nocturno. Sugerencia: cron + idempotent jobs.');
|
|
195
|
+
}
|
|
196
|
+
if (a.realtime === 'websocket' || a.realtime === 'stream') {
|
|
197
|
+
decisions.push('**Real-time delivery** -- websockets/SSE/pusher.');
|
|
198
|
+
}
|
|
199
|
+
const bodyMd = [
|
|
200
|
+
'## Volumetria',
|
|
201
|
+
'- Usuarios concurrentes lanzamiento: ' + a.users_launch,
|
|
202
|
+
'- Usuarios concurrentes 12m: ' + a.users_12m,
|
|
203
|
+
'- Pico RPS: ' + a.rps_peak,
|
|
204
|
+
'- Volumen de datos: ' + a.data_volume,
|
|
205
|
+
'- Crecimiento: ' + a.data_growth,
|
|
206
|
+
'',
|
|
207
|
+
'## Paralelismo',
|
|
208
|
+
'- Predominantes: ' + a.sync_async,
|
|
209
|
+
'- Real-time: ' + a.realtime,
|
|
210
|
+
'- Batch: ' + a.batch,
|
|
211
|
+
'',
|
|
212
|
+
'## Complejidad de procesos',
|
|
213
|
+
'- Pasos del flujo mas largo: ' + a.process_steps,
|
|
214
|
+
'- Decisiones / ramificaciones: ' + a.process_decisions,
|
|
215
|
+
'- Actores: ' + a.process_actors,
|
|
216
|
+
'',
|
|
217
|
+
'## Decisiones derivadas (proponer al usuario)',
|
|
218
|
+
decisions.length > 0 ? decisions.map((d) => '- ' + d).join('\n') : '_ninguna especifica disparada por la volumetria_',
|
|
219
|
+
].join('\n');
|
|
220
|
+
const docPath = await writeRoundDoc(projectRoot, 'quantitative', 'NFR Cuantitativas (step 5)', bodyMd);
|
|
221
|
+
await patchWorkflowGroup(projectRoot, 'clarification', {
|
|
222
|
+
nfr_quantitative: {
|
|
223
|
+
done_at: new Date().toISOString(),
|
|
224
|
+
summary: `${a.users_12m} users / ${a.rps_peak} rps / ${a.realtime} / ${a.batch}`,
|
|
225
|
+
doc_path: docPath,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log(c.success('Round quantitative persistido: ') + c.brand(docPath));
|
|
230
|
+
if (decisions.length > 0) {
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(c.warn('Decisiones arch derivadas:'));
|
|
233
|
+
for (const d of decisions)
|
|
234
|
+
console.log(' - ' + d.replace(/\*\*/g, ''));
|
|
235
|
+
}
|
|
236
|
+
console.log('');
|
|
237
|
+
console.log(c.dim('Siguiente paso: ') + c.code('yf clarify structural'));
|
|
238
|
+
}
|
|
239
|
+
/* =========================================================================
|
|
240
|
+
* Round 6 -- structural NFR
|
|
241
|
+
* ========================================================================= */
|
|
242
|
+
async function runStructural(projectRoot) {
|
|
243
|
+
console.log(c.dim('Ronda 6/6 -- NFR estructurales (persistencia + middleware + lifecycle).'));
|
|
244
|
+
console.log('');
|
|
245
|
+
const a = await prompts([
|
|
246
|
+
{ type: 'select', name: 'persistence', message: 'Modelo de persistencia primario:',
|
|
247
|
+
choices: [
|
|
248
|
+
{ title: 'SQL (relacional, ACID)', value: 'sql' },
|
|
249
|
+
{ title: 'NoSQL (document, eventual)', value: 'nosql' },
|
|
250
|
+
{ title: 'Hibrido SQL + NoSQL', value: 'hybrid' },
|
|
251
|
+
{ title: 'File storage (no DB)', value: 'files' },
|
|
252
|
+
{ title: 'In-memory only', value: 'memory' },
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
{ type: 'select', name: 'orm', message: 'Capa de acceso a la DB:',
|
|
256
|
+
choices: [
|
|
257
|
+
{ title: 'ORM estricto (Prisma / Drizzle / TypeORM)', value: 'orm' },
|
|
258
|
+
{ title: 'Query builder (Kysely / Knex)', value: 'query_builder' },
|
|
259
|
+
{ title: 'SQL crudo / driver directo', value: 'raw_sql' },
|
|
260
|
+
{ title: 'No aplica', value: 'none' },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
{ type: 'select', name: 'consistency', message: 'Modelo de consistencia:',
|
|
264
|
+
choices: [
|
|
265
|
+
{ title: 'ACID estricto', value: 'acid' },
|
|
266
|
+
{ title: 'Eventual consistency aceptada', value: 'eventual' },
|
|
267
|
+
{ title: 'No aplica (no hay BD)', value: 'none' },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
{ type: 'multiselect', name: 'middleware', message: 'Middleware central (multiselect con espacio):',
|
|
271
|
+
choices: [
|
|
272
|
+
{ title: 'auth (JWT / session)', value: 'auth' },
|
|
273
|
+
{ title: 'logging estructurado', value: 'logging' },
|
|
274
|
+
{ title: 'rate limiting', value: 'rate_limit' },
|
|
275
|
+
{ title: 'caching layer', value: 'cache' },
|
|
276
|
+
{ title: 'CORS', value: 'cors' },
|
|
277
|
+
{ title: 'i18n', value: 'i18n' },
|
|
278
|
+
{ title: 'request validation (zod)', value: 'validation' },
|
|
279
|
+
],
|
|
280
|
+
hint: '- Space to select, Enter to submit',
|
|
281
|
+
},
|
|
282
|
+
{ type: 'select', name: 'cache', message: 'Estrategia de cache:',
|
|
283
|
+
choices: [
|
|
284
|
+
{ title: 'Sin cache', value: 'none' },
|
|
285
|
+
{ title: 'In-memory (LRU local)', value: 'memory' },
|
|
286
|
+
{ title: 'Redis', value: 'redis' },
|
|
287
|
+
{ title: 'Edge / CDN', value: 'edge' },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
{ type: 'select', name: 'queue', message: 'Cola de mensajes:',
|
|
291
|
+
choices: [
|
|
292
|
+
{ title: 'Sin cola', value: 'none' },
|
|
293
|
+
{ title: 'Redis (BullMQ)', value: 'redis' },
|
|
294
|
+
{ title: 'AWS SQS', value: 'sqs' },
|
|
295
|
+
{ title: 'RabbitMQ', value: 'rabbit' },
|
|
296
|
+
{ title: 'NATS', value: 'nats' },
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
{ type: 'select', name: 'file_storage', message: 'Almacenamiento de archivos:',
|
|
300
|
+
choices: [
|
|
301
|
+
{ title: 'No aplica', value: 'none' },
|
|
302
|
+
{ title: 'S3-compatible (R2 / S3 / GCS)', value: 's3' },
|
|
303
|
+
{ title: 'Local filesystem', value: 'local' },
|
|
304
|
+
{ title: 'Hibrido', value: 'hybrid' },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
if (!a.persistence) {
|
|
309
|
+
console.log(c.warn('Cancelado.'));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const bodyMd = [
|
|
313
|
+
'## Persistencia',
|
|
314
|
+
'- Modelo: ' + a.persistence,
|
|
315
|
+
'- Capa: ' + a.orm,
|
|
316
|
+
'- Consistencia: ' + a.consistency,
|
|
317
|
+
'',
|
|
318
|
+
'## Middleware central',
|
|
319
|
+
Array.isArray(a.middleware) && a.middleware.length > 0
|
|
320
|
+
? a.middleware.map((m) => '- ' + m).join('\n')
|
|
321
|
+
: '_ninguno seleccionado_',
|
|
322
|
+
'',
|
|
323
|
+
'## Cache + colas + storage',
|
|
324
|
+
'- Cache strategy: ' + a.cache,
|
|
325
|
+
'- Queue: ' + a.queue,
|
|
326
|
+
'- File storage: ' + a.file_storage,
|
|
327
|
+
].join('\n');
|
|
328
|
+
const docPath = await writeRoundDoc(projectRoot, 'structural', 'NFR Estructurales (step 6)', bodyMd);
|
|
329
|
+
await patchWorkflowGroup(projectRoot, 'clarification', {
|
|
330
|
+
nfr_structural: {
|
|
331
|
+
done_at: new Date().toISOString(),
|
|
332
|
+
summary: `${a.persistence} / ${a.orm} / ${a.cache} cache / ${a.queue} queue`,
|
|
333
|
+
doc_path: docPath,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
console.log('');
|
|
337
|
+
console.log(c.success('Round structural persistido: ') + c.brand(docPath));
|
|
338
|
+
console.log(c.dim('Siguiente paso: ') + c.code('yf clarify governance'));
|
|
339
|
+
}
|
|
340
|
+
/* =========================================================================
|
|
341
|
+
* Round 7 -- governance NFR
|
|
342
|
+
* ========================================================================= */
|
|
343
|
+
async function runGovernance(projectRoot) {
|
|
344
|
+
console.log(c.dim('Ronda 7 -- NFR de gobernanza (compliance + auditoria + retencion).'));
|
|
345
|
+
console.log('');
|
|
346
|
+
const a = await prompts([
|
|
347
|
+
{ type: 'multiselect', name: 'compliance', message: 'Compliance que aplica (multiselect):',
|
|
348
|
+
choices: [
|
|
349
|
+
{ title: 'GDPR (EU users)', value: 'gdpr' },
|
|
350
|
+
{ title: 'HIPAA (US health)', value: 'hipaa' },
|
|
351
|
+
{ title: 'PCI-DSS (card payments)', value: 'pci' },
|
|
352
|
+
{ title: 'SOC 2 (B2B SaaS)', value: 'soc2' },
|
|
353
|
+
{ title: 'LGPD (Brasil)', value: 'lgpd' },
|
|
354
|
+
{ title: 'Ninguna por ahora', value: 'none' },
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
{ type: 'select', name: 'audit_level', message: 'Nivel de auditoria:',
|
|
358
|
+
choices: [
|
|
359
|
+
{ title: 'Logs basicos (errores + access)', value: 'basic' },
|
|
360
|
+
{ title: 'Append-only audit log de toda operacion sensible', value: 'append' },
|
|
361
|
+
{ title: 'Audit log + signed entries', value: 'signed' },
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
{ type: 'text', name: 'retention_default', message: 'Retencion default de datos del usuario (ej: "1 year", "7 years", "forever"):' },
|
|
365
|
+
{ type: 'select', name: 'rbac', message: 'Modelo de roles + permisos:',
|
|
366
|
+
choices: [
|
|
367
|
+
{ title: 'Single-user (no roles)', value: 'single' },
|
|
368
|
+
{ title: 'Single-tenant + roles', value: 'single_rbac' },
|
|
369
|
+
{ title: 'Multi-tenant B2B + RBAC', value: 'multi_rbac' },
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
{ type: 'select', name: 'backup', message: 'Frecuencia de backups:',
|
|
373
|
+
choices: [
|
|
374
|
+
{ title: 'Sin backup automatico', value: 'none' },
|
|
375
|
+
{ title: 'Daily', value: 'daily' },
|
|
376
|
+
{ title: 'Hourly + daily', value: 'hourly' },
|
|
377
|
+
{ title: 'Continuous (PITR)', value: 'continuous' },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
{ type: 'text', name: 'rto_rpo', message: 'RTO / RPO target (ej: "RTO 4h / RPO 1h", o "no aplica"):', initial: 'no aplica' },
|
|
381
|
+
{ type: 'confirm', name: 'pii_redact', message: 'Hay PII en logs/transcripts que hay que redactar automatico?', initial: false },
|
|
382
|
+
{ type: 'confirm', name: 'encrypt_at_rest', message: 'Encryption at rest obligatorio?', initial: true },
|
|
383
|
+
]);
|
|
384
|
+
if (!a.compliance) {
|
|
385
|
+
console.log(c.warn('Cancelado.'));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const decisions = [];
|
|
389
|
+
if (Array.isArray(a.compliance) && a.compliance.some((c) => ['gdpr', 'hipaa', 'soc2', 'lgpd'].includes(c))) {
|
|
390
|
+
decisions.push('Audit log estructural REQUERIDO (append-only + immutable storage)');
|
|
391
|
+
}
|
|
392
|
+
if (Array.isArray(a.compliance) && (a.compliance.includes('gdpr') || a.compliance.includes('lgpd'))) {
|
|
393
|
+
decisions.push('PII redaction automatica en logs + chat (regex middleware)');
|
|
394
|
+
decisions.push('Right to erasure endpoint (DELETE /user/me) + workflow');
|
|
395
|
+
}
|
|
396
|
+
if (a.rbac === 'multi_rbac') {
|
|
397
|
+
decisions.push('Multi-tenancy: tenant_id en cada fila + RLS / app-level isolation');
|
|
398
|
+
}
|
|
399
|
+
if (a.backup !== 'none') {
|
|
400
|
+
decisions.push('Backup automatico ' + a.backup + ' + cross-region');
|
|
401
|
+
}
|
|
402
|
+
const bodyMd = [
|
|
403
|
+
'## Compliance',
|
|
404
|
+
Array.isArray(a.compliance) && a.compliance.length > 0
|
|
405
|
+
? a.compliance.map((c) => '- ' + c).join('\n')
|
|
406
|
+
: '- none',
|
|
407
|
+
'',
|
|
408
|
+
'## Audit + retention',
|
|
409
|
+
'- Nivel: ' + a.audit_level,
|
|
410
|
+
'- Retencion default: ' + a.retention_default,
|
|
411
|
+
'',
|
|
412
|
+
'## Acceso + multi-tenancy',
|
|
413
|
+
'- RBAC: ' + a.rbac,
|
|
414
|
+
'',
|
|
415
|
+
'## Backup + DR',
|
|
416
|
+
'- Frecuencia: ' + a.backup,
|
|
417
|
+
'- RTO/RPO: ' + a.rto_rpo,
|
|
418
|
+
'',
|
|
419
|
+
'## Seguridad',
|
|
420
|
+
'- PII redaction: ' + (a.pii_redact ? 'si' : 'no'),
|
|
421
|
+
'- Encryption at rest: ' + (a.encrypt_at_rest ? 'si' : 'no'),
|
|
422
|
+
'',
|
|
423
|
+
'## Decisiones derivadas',
|
|
424
|
+
decisions.length > 0 ? decisions.map((d) => '- ' + d).join('\n') : '_ninguna disparada_',
|
|
425
|
+
].join('\n');
|
|
426
|
+
const docPath = await writeRoundDoc(projectRoot, 'governance', 'NFR Gobernanza (step 7)', bodyMd);
|
|
427
|
+
await patchWorkflowGroup(projectRoot, 'clarification', {
|
|
428
|
+
nfr_governance: {
|
|
429
|
+
done_at: new Date().toISOString(),
|
|
430
|
+
summary: `compliance: ${(Array.isArray(a.compliance) ? a.compliance.join(',') : 'none')} / rbac: ${a.rbac} / backup: ${a.backup}`,
|
|
431
|
+
doc_path: docPath,
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
console.log('');
|
|
435
|
+
console.log(c.success('Round governance persistido: ') + c.brand(docPath));
|
|
436
|
+
console.log(c.dim('Siguiente paso: ') + c.code('yf clarify operational'));
|
|
437
|
+
}
|
|
438
|
+
/* =========================================================================
|
|
439
|
+
* Round 8 -- operational NFR
|
|
440
|
+
* ========================================================================= */
|
|
441
|
+
async function runOperational(projectRoot) {
|
|
442
|
+
console.log(c.dim('Ronda 8 -- NFR operacionales (i18n + a11y + costos + monitoring + SLA).'));
|
|
443
|
+
console.log('');
|
|
444
|
+
const a = await prompts([
|
|
445
|
+
{ type: 'multiselect', name: 'locales', message: 'Locales a soportar al lanzamiento (multiselect):',
|
|
446
|
+
choices: [
|
|
447
|
+
{ title: 'Espanol', value: 'es' }, { title: 'English', value: 'en' },
|
|
448
|
+
{ title: 'Portugues', value: 'pt' }, { title: 'Francais', value: 'fr' },
|
|
449
|
+
{ title: 'Italiano', value: 'it' }, { title: 'Deutsch', value: 'de' },
|
|
450
|
+
{ title: 'Japones', value: 'ja' }, { title: 'Chino', value: 'zh' },
|
|
451
|
+
{ title: 'Hindi', value: 'hi' }, { title: 'Arabe (RTL)', value: 'ar' },
|
|
452
|
+
],
|
|
453
|
+
},
|
|
454
|
+
{ type: 'select', name: 'a11y', message: 'Nivel de accesibilidad target:',
|
|
455
|
+
choices: [
|
|
456
|
+
{ title: 'WCAG AA (recomendado)', value: 'aa' },
|
|
457
|
+
{ title: 'WCAG AAA (estricto)', value: 'aaa' },
|
|
458
|
+
{ title: 'ADIP completo (voz + STT + TTS para usuarios)', value: 'adip' },
|
|
459
|
+
{ title: 'No es prioridad ahora', value: 'none' },
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
{ type: 'number', name: 'budget_monthly', message: 'Presupuesto mensual estimado (USD, hosting + APIs):', initial: 50, min: 0 },
|
|
463
|
+
{ type: 'multiselect', name: 'monitoring', message: 'Stack de monitoring:',
|
|
464
|
+
choices: [
|
|
465
|
+
{ title: 'Sentry (errors)', value: 'sentry' },
|
|
466
|
+
{ title: 'Datadog (full)', value: 'datadog' },
|
|
467
|
+
{ title: 'PostHog (product analytics)', value: 'posthog' },
|
|
468
|
+
{ title: 'Grafana + Prometheus', value: 'grafana' },
|
|
469
|
+
{ title: 'Solo logs aplicacion', value: 'logs' },
|
|
470
|
+
],
|
|
471
|
+
},
|
|
472
|
+
{ type: 'select', name: 'alerts', message: 'Alertas:',
|
|
473
|
+
choices: [
|
|
474
|
+
{ title: 'Slack channel', value: 'slack' },
|
|
475
|
+
{ title: 'PagerDuty', value: 'pagerduty' },
|
|
476
|
+
{ title: 'Email', value: 'email' },
|
|
477
|
+
{ title: 'Ninguna por ahora', value: 'none' },
|
|
478
|
+
],
|
|
479
|
+
},
|
|
480
|
+
{ type: 'select', name: 'sla', message: 'SLA target:',
|
|
481
|
+
choices: [
|
|
482
|
+
{ title: 'Best effort (no SLA)', value: 'best_effort' },
|
|
483
|
+
{ title: '99% uptime (8h downtime/mes)', value: '99' },
|
|
484
|
+
{ title: '99.9% (43m downtime/mes)', value: '99.9' },
|
|
485
|
+
{ title: '99.99% (4m downtime/mes)', value: '99.99' },
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
{ type: 'select', name: 'support_hours', message: 'Horario de soporte:',
|
|
489
|
+
choices: [
|
|
490
|
+
{ title: '24/7', value: '24_7' },
|
|
491
|
+
{ title: 'Business hours del usuario', value: 'business' },
|
|
492
|
+
{ title: 'Best effort', value: 'best_effort' },
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
]);
|
|
496
|
+
if (!a.a11y) {
|
|
497
|
+
console.log(c.warn('Cancelado.'));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const decisions = [];
|
|
501
|
+
if (Array.isArray(a.locales) && a.locales.length >= 2) {
|
|
502
|
+
decisions.push('i18n infra completa (catalog + per-locale routing)');
|
|
503
|
+
}
|
|
504
|
+
if (Array.isArray(a.locales) && a.locales.includes('ar')) {
|
|
505
|
+
decisions.push('RTL support (I18nManager.forceRTL + bidi-safe CSS)');
|
|
506
|
+
}
|
|
507
|
+
if (a.budget_monthly < 100 && (a.sla === '99.9' || a.sla === '99.99')) {
|
|
508
|
+
decisions.push('CONFLICTO: SLA estricto incompatible con presupuesto bajo. Sugerencia: edge caching + model routing barato.');
|
|
509
|
+
}
|
|
510
|
+
const bodyMd = [
|
|
511
|
+
'## Locales + accesibilidad',
|
|
512
|
+
'- Locales: ' + (Array.isArray(a.locales) ? a.locales.join(', ') : '(ninguno)'),
|
|
513
|
+
'- A11y target: ' + a.a11y,
|
|
514
|
+
'',
|
|
515
|
+
'## Costos + monitoring',
|
|
516
|
+
'- Presupuesto mensual: USD ' + a.budget_monthly,
|
|
517
|
+
'- Monitoring: ' + (Array.isArray(a.monitoring) ? a.monitoring.join(', ') : '(ninguno)'),
|
|
518
|
+
'- Alertas: ' + a.alerts,
|
|
519
|
+
'',
|
|
520
|
+
'## SLA + soporte',
|
|
521
|
+
'- SLA: ' + a.sla,
|
|
522
|
+
'- Soporte: ' + a.support_hours,
|
|
523
|
+
'',
|
|
524
|
+
'## Decisiones derivadas',
|
|
525
|
+
decisions.length > 0 ? decisions.map((d) => '- ' + d).join('\n') : '_ninguna disparada_',
|
|
526
|
+
].join('\n');
|
|
527
|
+
const docPath = await writeRoundDoc(projectRoot, 'operational', 'NFR Operacionales (step 8)', bodyMd);
|
|
528
|
+
await patchWorkflowGroup(projectRoot, 'clarification', {
|
|
529
|
+
nfr_operational: {
|
|
530
|
+
done_at: new Date().toISOString(),
|
|
531
|
+
summary: `${(Array.isArray(a.locales) ? a.locales.length : 0)} locales / a11y: ${a.a11y} / SLA: ${a.sla} / budget: $${a.budget_monthly}`,
|
|
532
|
+
doc_path: docPath,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
console.log('');
|
|
536
|
+
console.log(c.success('Round operational persistido: ') + c.brand(docPath));
|
|
537
|
+
console.log(c.dim('Siguiente paso: ') + c.code('yf clarify blocks'));
|
|
538
|
+
}
|
|
539
|
+
/* =========================================================================
|
|
540
|
+
* Round 9 -- blocks plan
|
|
541
|
+
* ========================================================================= */
|
|
542
|
+
async function runBlocks(projectRoot) {
|
|
543
|
+
console.log(c.dim('Ronda 9 -- descompose el sistema en bloques + dependencias.'));
|
|
544
|
+
console.log(c.dim('Ingresa los bloques uno por uno. Vacio para terminar.'));
|
|
545
|
+
console.log('');
|
|
546
|
+
const blocks = [];
|
|
547
|
+
while (true) {
|
|
548
|
+
const a = await prompts([
|
|
549
|
+
{ type: 'text', name: 'name', message: `Bloque ${blocks.length + 1} -- nombre (vacio para terminar):` },
|
|
550
|
+
]);
|
|
551
|
+
if (!a.name || a.name.trim() === '')
|
|
552
|
+
break;
|
|
553
|
+
const rest = await prompts([
|
|
554
|
+
{ type: 'select', name: 'effort', message: 'Esfuerzo:',
|
|
555
|
+
choices: [
|
|
556
|
+
{ title: 'S (< 1 dia Sumi)', value: 'S' },
|
|
557
|
+
{ title: 'M (1-2 dias)', value: 'M' },
|
|
558
|
+
{ title: 'L (1 semana)', value: 'L' },
|
|
559
|
+
{ title: 'XL (>= 2 semanas)', value: 'XL' },
|
|
560
|
+
],
|
|
561
|
+
},
|
|
562
|
+
{ type: 'text', name: 'dependencies', message: 'Dependencias (ids de bloques anteriores, separados por coma, vacio = ninguna):' },
|
|
563
|
+
{ type: 'confirm', name: 'mvp', message: 'Este bloque forma parte del MVP?', initial: false },
|
|
564
|
+
]);
|
|
565
|
+
const id = a.name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
|
|
566
|
+
blocks.push({
|
|
567
|
+
id,
|
|
568
|
+
name: a.name,
|
|
569
|
+
effort: rest.effort,
|
|
570
|
+
dependencies: rest.dependencies ? rest.dependencies.split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
571
|
+
mvp: !!rest.mvp,
|
|
572
|
+
});
|
|
573
|
+
console.log(c.success(' +') + c.dim(' ' + id + ' (' + rest.effort + ')'));
|
|
574
|
+
}
|
|
575
|
+
if (blocks.length === 0) {
|
|
576
|
+
console.log(c.warn('Cancelado (cero bloques).'));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
/* MVP path = blocks marked mvp, in dependency order. */
|
|
580
|
+
const mvpBlocks = blocks.filter((b) => b.mvp).map((b) => b.id);
|
|
581
|
+
const bodyMd = [
|
|
582
|
+
'## Bloques identificados (' + blocks.length + ')',
|
|
583
|
+
'',
|
|
584
|
+
'| id | nombre | esfuerzo | deps | mvp |',
|
|
585
|
+
'|----|--------|----------|------|-----|',
|
|
586
|
+
...blocks.map((b) => `| \`${b.id}\` | ${b.name} | ${b.effort} | ${b.dependencies.join(', ') || '-'} | ${b.mvp ? '✔' : ''} |`),
|
|
587
|
+
'',
|
|
588
|
+
'## MVP path',
|
|
589
|
+
mvpBlocks.length > 0 ? mvpBlocks.map((id) => '- ' + id).join('\n') : '_ningun bloque marcado MVP_',
|
|
590
|
+
'',
|
|
591
|
+
'## Orden de construccion sugerido',
|
|
592
|
+
'_Resolve via topological sort once Forge implements the planner_',
|
|
593
|
+
].join('\n');
|
|
594
|
+
const docPath = await writeRoundDoc(projectRoot, 'blocks', 'Bloques prioritarios (step 9)', bodyMd);
|
|
595
|
+
await patchWorkflowGroup(projectRoot, 'clarification', {
|
|
596
|
+
blocks: {
|
|
597
|
+
done_at: new Date().toISOString(),
|
|
598
|
+
blocks,
|
|
599
|
+
mvp_path: mvpBlocks,
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
console.log('');
|
|
603
|
+
console.log(c.success('Round blocks persistido: ') + c.brand(docPath));
|
|
604
|
+
console.log(c.dim(' ' + blocks.length + ' bloques / ' + mvpBlocks.length + ' en MVP.'));
|
|
605
|
+
console.log('');
|
|
606
|
+
console.log(c.dim('Phase II completa. Siguiente paso: ') + c.code('yf spec rfp'));
|
|
607
|
+
}
|
|
608
|
+
/* =========================================================================
|
|
609
|
+
* Dispatcher
|
|
610
|
+
* ========================================================================= */
|
|
611
|
+
export async function runClarify(round, cwd) {
|
|
612
|
+
header('Yujin Forge -- clarify ' + round);
|
|
613
|
+
console.log('');
|
|
614
|
+
const projectRoot = cwd ? path.resolve(cwd) : process.cwd();
|
|
615
|
+
const wf = await readWorkflow(projectRoot);
|
|
616
|
+
if (!wf.tier) {
|
|
617
|
+
console.log(c.error('No hay triage previo. Corre primero ') + c.code('yf triage'));
|
|
618
|
+
process.exitCode = 1;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const pre = PRE_REQ[round];
|
|
622
|
+
if (pre) {
|
|
623
|
+
const preDone = wf.clarification?.[ROUND_STATE_KEY[pre]];
|
|
624
|
+
if (!preDone) {
|
|
625
|
+
console.log(c.error('Pre-requisito faltante: ronda "' + pre + '" no esta completa.'));
|
|
626
|
+
console.log(c.dim('Corre primero: ') + c.code('yf clarify ' + pre));
|
|
627
|
+
process.exitCode = 1;
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
switch (round) {
|
|
632
|
+
case 'functional':
|
|
633
|
+
await runFunctional(projectRoot);
|
|
634
|
+
break;
|
|
635
|
+
case 'quantitative':
|
|
636
|
+
await runQuantitative(projectRoot);
|
|
637
|
+
break;
|
|
638
|
+
case 'structural':
|
|
639
|
+
await runStructural(projectRoot);
|
|
640
|
+
break;
|
|
641
|
+
case 'governance':
|
|
642
|
+
await runGovernance(projectRoot);
|
|
643
|
+
break;
|
|
644
|
+
case 'operational':
|
|
645
|
+
await runOperational(projectRoot);
|
|
646
|
+
break;
|
|
647
|
+
case 'blocks':
|
|
648
|
+
await runBlocks(projectRoot);
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
export function registerClarifyCommand(program) {
|
|
653
|
+
const clarify = program.command('clarify')
|
|
654
|
+
.description('Phase II steps 4-9: 6 structured clarification rounds.');
|
|
655
|
+
const rounds = ['functional', 'quantitative', 'structural', 'governance', 'operational', 'blocks'];
|
|
656
|
+
for (const r of rounds) {
|
|
657
|
+
clarify.command(r)
|
|
658
|
+
.description('Ronda ' + ROUND_NUMBER[r] + ': ' + r)
|
|
659
|
+
.option('--cwd <path>', 'project root (default: current directory)')
|
|
660
|
+
.action(async (opts) => {
|
|
661
|
+
try {
|
|
662
|
+
await runClarify(r, opts.cwd);
|
|
663
|
+
}
|
|
664
|
+
catch (err) {
|
|
665
|
+
console.error(c.error(err instanceof Error ? err.message : String(err)));
|
|
666
|
+
process.exitCode = 1;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
//# sourceMappingURL=clarify.js.map
|