@arcadialdev/arcality 4.0.2 → 4.1.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/.agents/skills/db-validation-evidence/SKILL.md +43 -0
- package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
- package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
- package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
- package/.agents/skills/form-expert/SKILL.md +98 -0
- package/.agents/skills/investigation-protocol/SKILL.md +56 -0
- package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
- package/.agents/skills/modal-master/SKILL.md +46 -0
- package/.agents/skills/native-control-expert/SKILL.md +74 -0
- package/.agents/skills/qa-context-governance/SKILL.md +23 -0
- package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
- package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
- package/README.md +103 -163
- package/bin/arcality.mjs +25 -25
- package/package.json +75 -75
- package/scripts/edit-config.mjs +843 -0
- package/scripts/gen-and-run.mjs +2705 -2609
- package/scripts/generate.mjs +236 -236
- package/scripts/init.mjs +47 -47
- package/src/configManager.mjs +13 -13
- package/src/envSetup.ts +229 -205
- package/src/services/codebaseAnalyzer.mjs +59 -59
- package/src/services/databaseValidationService.mjs +598 -0
- package/src/services/executionEvidenceService.mjs +124 -0
- package/src/services/generatedMissionSchema.mjs +76 -76
- package/src/services/generatedMissionStore.mjs +117 -117
- package/src/services/generationContext.mjs +242 -242
- package/src/services/mcpStdioClient.mjs +204 -0
- package/src/services/missionGenerator.mjs +329 -329
- package/src/services/routeDiscovery.mjs +762 -762
- package/tests/_helpers/ArcalityReporter.js +1342 -1255
- package/tests/_helpers/agentic-runner.bundle.spec.js +1363 -245
- package/.agent/skills/form-expert.md +0 -102
- package/.agent/skills/investigation-protocol.md +0 -61
- package/.agent/skills/modal-master.md +0 -41
- package/.agent/skills/native-control-expert.md +0 -82
|
@@ -1,329 +1,329 @@
|
|
|
1
|
-
function simpleHash(value) {
|
|
2
|
-
let hash = 0;
|
|
3
|
-
const text = String(value || '');
|
|
4
|
-
for (let i = 0; i < text.length; i++) {
|
|
5
|
-
hash = ((hash << 5) - hash) + text.charCodeAt(i);
|
|
6
|
-
hash |= 0;
|
|
7
|
-
}
|
|
8
|
-
return `h_${Math.abs(hash).toString(36)}`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function toSentenceCase(value) {
|
|
12
|
-
const text = String(value || '').trim();
|
|
13
|
-
if (!text) return text;
|
|
14
|
-
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function inferCollection(route) {
|
|
18
|
-
if (route === '/') return 'generated/root';
|
|
19
|
-
const first = route.replace(/^\/+/, '').split('/')[0] || 'general';
|
|
20
|
-
return `generated/${first}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function normalizeTags(tags = [], promptFocus = '') {
|
|
24
|
-
const result = new Set(tags.filter(Boolean));
|
|
25
|
-
if (promptFocus) {
|
|
26
|
-
const focusTokens = String(promptFocus)
|
|
27
|
-
.toLowerCase()
|
|
28
|
-
.split(/[^a-z0-9]+/i)
|
|
29
|
-
.filter(token => token.length >= 4)
|
|
30
|
-
.slice(0, 3);
|
|
31
|
-
for (const token of focusTokens) result.add(token);
|
|
32
|
-
}
|
|
33
|
-
return Array.from(result);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function buildPromptFocusSuffix(promptFocus) {
|
|
37
|
-
if (!promptFocus) return '';
|
|
38
|
-
return ` Enfoca especialmente la mision en: ${promptFocus}.`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function buildExpectedFocusSuffix(promptFocus) {
|
|
42
|
-
if (!promptFocus) return '';
|
|
43
|
-
return ` Ademas, valida observaciones relacionadas con: ${toSentenceCase(promptFocus)}.`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function buildMissionVariant(routeEntry, variant, options = {}) {
|
|
47
|
-
const promptFocus = String(options.prompt || '').trim();
|
|
48
|
-
const route = routeEntry.route || '/';
|
|
49
|
-
const baseName = routeEntry.name || 'GEN_route';
|
|
50
|
-
const variantCode = variant.code ? `_${variant.code}` : '';
|
|
51
|
-
const guidance = options.routeGuidance || {};
|
|
52
|
-
const guidanceLines = [];
|
|
53
|
-
|
|
54
|
-
if ((guidance.matchedRules || []).length > 0) {
|
|
55
|
-
guidanceLines.push(
|
|
56
|
-
'Aplica estas reglas del contexto del cliente durante la validacion: ' +
|
|
57
|
-
guidance.matchedRules.map(rule => rule.text).join(' | ')
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if ((guidance.feasibilityHints || []).length > 0) {
|
|
62
|
-
guidanceLines.push(
|
|
63
|
-
'Considera estas prioridades de generacion: ' +
|
|
64
|
-
guidance.feasibilityHints.join(' | ')
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const prompt = [
|
|
69
|
-
variant.prompt || routeEntry.prompt,
|
|
70
|
-
guidanceLines.join(' '),
|
|
71
|
-
buildPromptFocusSuffix(promptFocus).trim()
|
|
72
|
-
].filter(Boolean).join(' ').trim();
|
|
73
|
-
|
|
74
|
-
const expectedResult = [
|
|
75
|
-
variant.expectedResult || routeEntry.expectedResult,
|
|
76
|
-
(guidance.matchedRules || []).length > 0 ? 'Ademas, respeta las reglas de negocio configuradas para esta pantalla.' : '',
|
|
77
|
-
buildExpectedFocusSuffix(promptFocus).trim()
|
|
78
|
-
].filter(Boolean).join(' ').trim();
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
name: `${baseName}${variantCode}`,
|
|
82
|
-
prompt,
|
|
83
|
-
page: route,
|
|
84
|
-
page_path: routeEntry.routeExamplePath || route,
|
|
85
|
-
expected_result: expectedResult,
|
|
86
|
-
tags: normalizeTags([...(routeEntry.tags || []), ...(variant.tags || []), ...((guidance.extraTags) || [])], promptFocus),
|
|
87
|
-
priority: guidance.priorityOverride || variant.priority || routeEntry.priority || 'normal',
|
|
88
|
-
collection: inferCollection(route),
|
|
89
|
-
created_at: new Date().toISOString(),
|
|
90
|
-
generation: {
|
|
91
|
-
source: 'arcality generate',
|
|
92
|
-
framework: routeEntry.framework || 'unknown',
|
|
93
|
-
router_kind: routeEntry.routerKind || 'unknown',
|
|
94
|
-
variant: variant.code || 'core',
|
|
95
|
-
context_status: guidance.contextStatus || { qaContext: 'missing', feasibility: 'missing' },
|
|
96
|
-
route_example_path: routeEntry.routeExamplePath || route,
|
|
97
|
-
route_params: routeEntry.routeParams || [],
|
|
98
|
-
source_file_path: routeEntry.pagePath,
|
|
99
|
-
route_hash: simpleHash(`${route}|${routeEntry.pagePath}`),
|
|
100
|
-
prompt_hash: simpleHash(prompt)
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function scoreVariant(routeEntry, variant) {
|
|
106
|
-
const signals = routeEntry.signals || {};
|
|
107
|
-
let score = 0;
|
|
108
|
-
|
|
109
|
-
switch (variant.code) {
|
|
110
|
-
case 'checkout':
|
|
111
|
-
score += 120;
|
|
112
|
-
break;
|
|
113
|
-
case 'auth':
|
|
114
|
-
score += 110;
|
|
115
|
-
break;
|
|
116
|
-
case 'form':
|
|
117
|
-
score += 95;
|
|
118
|
-
break;
|
|
119
|
-
case 'detail':
|
|
120
|
-
score += 90;
|
|
121
|
-
break;
|
|
122
|
-
case 'upload':
|
|
123
|
-
score += 85;
|
|
124
|
-
break;
|
|
125
|
-
case 'settings':
|
|
126
|
-
score += 80;
|
|
127
|
-
break;
|
|
128
|
-
case 'list':
|
|
129
|
-
score += 75;
|
|
130
|
-
break;
|
|
131
|
-
case 'core':
|
|
132
|
-
score += 40;
|
|
133
|
-
break;
|
|
134
|
-
default:
|
|
135
|
-
score += 20;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
score += variant.priority === 'high' ? 10 : 0;
|
|
139
|
-
score += routeEntry.route === '/' ? 6 : 0;
|
|
140
|
-
score += (routeEntry.routeParams || []).length * 4;
|
|
141
|
-
score += signals.hasSearch || signals.hasFilter || signals.hasTable ? 4 : 0;
|
|
142
|
-
score += signals.hasForm ? 4 : 0;
|
|
143
|
-
score += signals.hasUpload ? 4 : 0;
|
|
144
|
-
return score;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function shouldKeepVariant(routeEntry, variant, allVariants) {
|
|
148
|
-
const signals = routeEntry.signals || {};
|
|
149
|
-
const hasSpecialized = allVariants.some(item => item.code !== 'core');
|
|
150
|
-
const isRootLike = routeEntry.route === '/' || signals.isDashboardLike;
|
|
151
|
-
|
|
152
|
-
if (variant.code === 'core') {
|
|
153
|
-
if (!hasSpecialized) return true;
|
|
154
|
-
return isRootLike;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (variant.code === 'list') {
|
|
158
|
-
return Boolean(signals.hasTable || signals.hasSearch || signals.hasFilter || routeEntry.route.includes('list'));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (variant.code === 'form') {
|
|
162
|
-
return Boolean(signals.hasForm || signals.hasCreateAction || signals.hasEditAction || signals.isCrudLike);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (variant.code === 'settings') {
|
|
166
|
-
return Boolean(signals.hasSettings);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (variant.code === 'upload') {
|
|
170
|
-
return Boolean(signals.hasUpload);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (variant.code === 'detail') {
|
|
174
|
-
return Boolean(signals.isDynamicRoute || (routeEntry.routeParams || []).length > 0);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (variant.code === 'auth') {
|
|
178
|
-
return Boolean(signals.hasAuth);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (variant.code === 'checkout') {
|
|
182
|
-
return Boolean(signals.hasCheckout);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function selectUsefulVariants(routeEntry, variants) {
|
|
189
|
-
const filtered = variants.filter(variant => shouldKeepVariant(routeEntry, variant, variants));
|
|
190
|
-
const sorted = filtered.sort((a, b) => scoreVariant(routeEntry, b) - scoreVariant(routeEntry, a));
|
|
191
|
-
const maxVariants = routeEntry.route === '/' ? 5 : 4;
|
|
192
|
-
|
|
193
|
-
const selected = [];
|
|
194
|
-
const seenCodes = new Set();
|
|
195
|
-
for (const variant of sorted) {
|
|
196
|
-
if (seenCodes.has(variant.code)) continue;
|
|
197
|
-
seenCodes.add(variant.code);
|
|
198
|
-
selected.push(variant);
|
|
199
|
-
if (selected.length >= maxVariants) break;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const hasCore = selected.some(variant => variant.code === 'core');
|
|
203
|
-
if (selected.length === 0) {
|
|
204
|
-
const fallback = variants.find(variant => variant.code === 'core') || variants[0];
|
|
205
|
-
return fallback ? [fallback] : [];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (!hasCore && (routeEntry.route === '/' || filtered.length === 1)) {
|
|
209
|
-
const core = variants.find(variant => variant.code === 'core');
|
|
210
|
-
if (core && selected.length < maxVariants) selected.push(core);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return selected.sort((a, b) => scoreVariant(routeEntry, b) - scoreVariant(routeEntry, a));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function buildMissionVariants(routeEntry) {
|
|
217
|
-
const signals = routeEntry.signals || {};
|
|
218
|
-
const route = routeEntry.route || '/';
|
|
219
|
-
const routeLabel = route === '/' ? 'la pagina principal' : `la pagina ${route}`;
|
|
220
|
-
const exampleRoute = routeEntry.routeExamplePath && routeEntry.routeExamplePath !== route ? routeEntry.routeExamplePath : null;
|
|
221
|
-
const variants = [{
|
|
222
|
-
code: 'core',
|
|
223
|
-
prompt: routeEntry.prompt,
|
|
224
|
-
expectedResult: routeEntry.expectedResult,
|
|
225
|
-
tags: ['core-flow'],
|
|
226
|
-
priority: routeEntry.priority || 'normal'
|
|
227
|
-
}];
|
|
228
|
-
|
|
229
|
-
if (signals.hasAuth) {
|
|
230
|
-
variants.push({
|
|
231
|
-
code: 'auth',
|
|
232
|
-
prompt: `Navega a ${route} y valida el flujo de autenticacion visible, incluyendo campos obligatorios, mensajes de error y acceso correcto cuando existan credenciales validas de prueba.`,
|
|
233
|
-
expectedResult: `${routeLabel} permite validar autenticacion y feedback de errores sin fallas visibles.`,
|
|
234
|
-
tags: ['auth', 'login', 'validation'],
|
|
235
|
-
priority: 'high'
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (signals.hasCheckout) {
|
|
240
|
-
variants.push({
|
|
241
|
-
code: 'checkout',
|
|
242
|
-
prompt: `Navega a ${route} y valida el flujo de checkout o pago visible, verificando resumen, captura de datos sensibles, validaciones y confirmacion clara del resultado sin ejecutar cobros reales.`,
|
|
243
|
-
expectedResult: `${routeLabel} permite revisar el flujo de pago con validaciones y estados consistentes sin errores visibles.`,
|
|
244
|
-
tags: ['checkout', 'payment', 'transaction'],
|
|
245
|
-
priority: 'high'
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (signals.hasForm || signals.isCrudLike) {
|
|
250
|
-
variants.push({
|
|
251
|
-
code: 'form',
|
|
252
|
-
prompt: `Navega a ${route} y valida el formulario o flujo CRUD principal, cubriendo captura, validaciones, guardado y mensajes de confirmacion o error.`,
|
|
253
|
-
expectedResult: `${routeLabel} permite capturar o editar informacion con validaciones claras y resultado consistente.`,
|
|
254
|
-
tags: ['form', 'crud', 'save'],
|
|
255
|
-
priority: signals.hasCreateAction || signals.hasEditAction ? 'high' : 'normal'
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (signals.isListLike) {
|
|
260
|
-
variants.push({
|
|
261
|
-
code: 'list',
|
|
262
|
-
prompt: `Navega a ${route} y valida el listado principal, incluyendo carga de registros, filtros, busqueda, orden o paginacion segun aplique.`,
|
|
263
|
-
expectedResult: `${routeLabel} muestra datos consultables y permite usar herramientas de exploracion sin errores visibles.`,
|
|
264
|
-
tags: ['list', 'search', 'filter'],
|
|
265
|
-
priority: 'normal'
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (signals.isDynamicRoute) {
|
|
270
|
-
variants.push({
|
|
271
|
-
code: 'detail',
|
|
272
|
-
prompt: `Navega a ${route}${exampleRoute ? ` usando una ruta ejemplo como ${exampleRoute}` : ''} y valida la vista de detalle, comprobando que la informacion contextual cargue correctamente y que la accion principal asociada al registro sea usable.`,
|
|
273
|
-
expectedResult: `${routeLabel} carga informacion contextual correcta del registro y permite validar al menos una accion clave.`,
|
|
274
|
-
tags: ['detail', 'contextual-data', ...(exampleRoute ? ['example-route'] : [])],
|
|
275
|
-
priority: 'normal'
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (signals.hasSettings) {
|
|
280
|
-
variants.push({
|
|
281
|
-
code: 'settings',
|
|
282
|
-
prompt: `Navega a ${route} y valida el flujo de configuracion o preferencias, verificando persistencia visual, cambios editables y mensajes claros de guardado.`,
|
|
283
|
-
expectedResult: `${routeLabel} permite ajustar configuraciones con persistencia y feedback confiable.`,
|
|
284
|
-
tags: ['settings', 'preferences', 'persistence'],
|
|
285
|
-
priority: 'normal'
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (signals.hasUpload) {
|
|
290
|
-
variants.push({
|
|
291
|
-
code: 'upload',
|
|
292
|
-
prompt: `Navega a ${route} y valida la carga de archivos disponible, comprobando restricciones, mensajes de estado y resultado visible despues del upload.`,
|
|
293
|
-
expectedResult: `${routeLabel} permite cargar archivos con validaciones y confirmacion observable del resultado.`,
|
|
294
|
-
tags: ['upload', 'file'],
|
|
295
|
-
priority: 'normal'
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const uniqueVariants = [];
|
|
300
|
-
const seen = new Set();
|
|
301
|
-
for (const variant of variants) {
|
|
302
|
-
const key = `${variant.code}|${variant.prompt}|${variant.expectedResult}`;
|
|
303
|
-
if (seen.has(key)) continue;
|
|
304
|
-
seen.add(key);
|
|
305
|
-
uniqueVariants.push(variant);
|
|
306
|
-
}
|
|
307
|
-
return selectUsefulVariants(routeEntry, uniqueVariants);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export function generateMissionsForRoute(routeEntry, options = {}) {
|
|
311
|
-
return buildMissionVariants(routeEntry).map(variant => buildMissionVariant(routeEntry, {
|
|
312
|
-
...variant
|
|
313
|
-
}, {
|
|
314
|
-
...options,
|
|
315
|
-
routeGuidance: options.routeGuidance || {}
|
|
316
|
-
}));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export function generateMissionYamlData(routeEntry, options = {}) {
|
|
320
|
-
return generateMissionsForRoute(routeEntry, options)[0];
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export function buildMissionIdentity(missionData) {
|
|
324
|
-
return [
|
|
325
|
-
missionData.page_path || '',
|
|
326
|
-
missionData.prompt || '',
|
|
327
|
-
missionData.expected_result || ''
|
|
328
|
-
].join('||').toLowerCase();
|
|
329
|
-
}
|
|
1
|
+
function simpleHash(value) {
|
|
2
|
+
let hash = 0;
|
|
3
|
+
const text = String(value || '');
|
|
4
|
+
for (let i = 0; i < text.length; i++) {
|
|
5
|
+
hash = ((hash << 5) - hash) + text.charCodeAt(i);
|
|
6
|
+
hash |= 0;
|
|
7
|
+
}
|
|
8
|
+
return `h_${Math.abs(hash).toString(36)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toSentenceCase(value) {
|
|
12
|
+
const text = String(value || '').trim();
|
|
13
|
+
if (!text) return text;
|
|
14
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function inferCollection(route) {
|
|
18
|
+
if (route === '/') return 'generated/root';
|
|
19
|
+
const first = route.replace(/^\/+/, '').split('/')[0] || 'general';
|
|
20
|
+
return `generated/${first}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeTags(tags = [], promptFocus = '') {
|
|
24
|
+
const result = new Set(tags.filter(Boolean));
|
|
25
|
+
if (promptFocus) {
|
|
26
|
+
const focusTokens = String(promptFocus)
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.split(/[^a-z0-9]+/i)
|
|
29
|
+
.filter(token => token.length >= 4)
|
|
30
|
+
.slice(0, 3);
|
|
31
|
+
for (const token of focusTokens) result.add(token);
|
|
32
|
+
}
|
|
33
|
+
return Array.from(result);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildPromptFocusSuffix(promptFocus) {
|
|
37
|
+
if (!promptFocus) return '';
|
|
38
|
+
return ` Enfoca especialmente la mision en: ${promptFocus}.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildExpectedFocusSuffix(promptFocus) {
|
|
42
|
+
if (!promptFocus) return '';
|
|
43
|
+
return ` Ademas, valida observaciones relacionadas con: ${toSentenceCase(promptFocus)}.`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildMissionVariant(routeEntry, variant, options = {}) {
|
|
47
|
+
const promptFocus = String(options.prompt || '').trim();
|
|
48
|
+
const route = routeEntry.route || '/';
|
|
49
|
+
const baseName = routeEntry.name || 'GEN_route';
|
|
50
|
+
const variantCode = variant.code ? `_${variant.code}` : '';
|
|
51
|
+
const guidance = options.routeGuidance || {};
|
|
52
|
+
const guidanceLines = [];
|
|
53
|
+
|
|
54
|
+
if ((guidance.matchedRules || []).length > 0) {
|
|
55
|
+
guidanceLines.push(
|
|
56
|
+
'Aplica estas reglas del contexto del cliente durante la validacion: ' +
|
|
57
|
+
guidance.matchedRules.map(rule => rule.text).join(' | ')
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ((guidance.feasibilityHints || []).length > 0) {
|
|
62
|
+
guidanceLines.push(
|
|
63
|
+
'Considera estas prioridades de generacion: ' +
|
|
64
|
+
guidance.feasibilityHints.join(' | ')
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const prompt = [
|
|
69
|
+
variant.prompt || routeEntry.prompt,
|
|
70
|
+
guidanceLines.join(' '),
|
|
71
|
+
buildPromptFocusSuffix(promptFocus).trim()
|
|
72
|
+
].filter(Boolean).join(' ').trim();
|
|
73
|
+
|
|
74
|
+
const expectedResult = [
|
|
75
|
+
variant.expectedResult || routeEntry.expectedResult,
|
|
76
|
+
(guidance.matchedRules || []).length > 0 ? 'Ademas, respeta las reglas de negocio configuradas para esta pantalla.' : '',
|
|
77
|
+
buildExpectedFocusSuffix(promptFocus).trim()
|
|
78
|
+
].filter(Boolean).join(' ').trim();
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name: `${baseName}${variantCode}`,
|
|
82
|
+
prompt,
|
|
83
|
+
page: route,
|
|
84
|
+
page_path: routeEntry.routeExamplePath || route,
|
|
85
|
+
expected_result: expectedResult,
|
|
86
|
+
tags: normalizeTags([...(routeEntry.tags || []), ...(variant.tags || []), ...((guidance.extraTags) || [])], promptFocus),
|
|
87
|
+
priority: guidance.priorityOverride || variant.priority || routeEntry.priority || 'normal',
|
|
88
|
+
collection: inferCollection(route),
|
|
89
|
+
created_at: new Date().toISOString(),
|
|
90
|
+
generation: {
|
|
91
|
+
source: 'arcality generate',
|
|
92
|
+
framework: routeEntry.framework || 'unknown',
|
|
93
|
+
router_kind: routeEntry.routerKind || 'unknown',
|
|
94
|
+
variant: variant.code || 'core',
|
|
95
|
+
context_status: guidance.contextStatus || { qaContext: 'missing', feasibility: 'missing' },
|
|
96
|
+
route_example_path: routeEntry.routeExamplePath || route,
|
|
97
|
+
route_params: routeEntry.routeParams || [],
|
|
98
|
+
source_file_path: routeEntry.pagePath,
|
|
99
|
+
route_hash: simpleHash(`${route}|${routeEntry.pagePath}`),
|
|
100
|
+
prompt_hash: simpleHash(prompt)
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function scoreVariant(routeEntry, variant) {
|
|
106
|
+
const signals = routeEntry.signals || {};
|
|
107
|
+
let score = 0;
|
|
108
|
+
|
|
109
|
+
switch (variant.code) {
|
|
110
|
+
case 'checkout':
|
|
111
|
+
score += 120;
|
|
112
|
+
break;
|
|
113
|
+
case 'auth':
|
|
114
|
+
score += 110;
|
|
115
|
+
break;
|
|
116
|
+
case 'form':
|
|
117
|
+
score += 95;
|
|
118
|
+
break;
|
|
119
|
+
case 'detail':
|
|
120
|
+
score += 90;
|
|
121
|
+
break;
|
|
122
|
+
case 'upload':
|
|
123
|
+
score += 85;
|
|
124
|
+
break;
|
|
125
|
+
case 'settings':
|
|
126
|
+
score += 80;
|
|
127
|
+
break;
|
|
128
|
+
case 'list':
|
|
129
|
+
score += 75;
|
|
130
|
+
break;
|
|
131
|
+
case 'core':
|
|
132
|
+
score += 40;
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
score += 20;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
score += variant.priority === 'high' ? 10 : 0;
|
|
139
|
+
score += routeEntry.route === '/' ? 6 : 0;
|
|
140
|
+
score += (routeEntry.routeParams || []).length * 4;
|
|
141
|
+
score += signals.hasSearch || signals.hasFilter || signals.hasTable ? 4 : 0;
|
|
142
|
+
score += signals.hasForm ? 4 : 0;
|
|
143
|
+
score += signals.hasUpload ? 4 : 0;
|
|
144
|
+
return score;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function shouldKeepVariant(routeEntry, variant, allVariants) {
|
|
148
|
+
const signals = routeEntry.signals || {};
|
|
149
|
+
const hasSpecialized = allVariants.some(item => item.code !== 'core');
|
|
150
|
+
const isRootLike = routeEntry.route === '/' || signals.isDashboardLike;
|
|
151
|
+
|
|
152
|
+
if (variant.code === 'core') {
|
|
153
|
+
if (!hasSpecialized) return true;
|
|
154
|
+
return isRootLike;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (variant.code === 'list') {
|
|
158
|
+
return Boolean(signals.hasTable || signals.hasSearch || signals.hasFilter || routeEntry.route.includes('list'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (variant.code === 'form') {
|
|
162
|
+
return Boolean(signals.hasForm || signals.hasCreateAction || signals.hasEditAction || signals.isCrudLike);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (variant.code === 'settings') {
|
|
166
|
+
return Boolean(signals.hasSettings);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (variant.code === 'upload') {
|
|
170
|
+
return Boolean(signals.hasUpload);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (variant.code === 'detail') {
|
|
174
|
+
return Boolean(signals.isDynamicRoute || (routeEntry.routeParams || []).length > 0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (variant.code === 'auth') {
|
|
178
|
+
return Boolean(signals.hasAuth);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (variant.code === 'checkout') {
|
|
182
|
+
return Boolean(signals.hasCheckout);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function selectUsefulVariants(routeEntry, variants) {
|
|
189
|
+
const filtered = variants.filter(variant => shouldKeepVariant(routeEntry, variant, variants));
|
|
190
|
+
const sorted = filtered.sort((a, b) => scoreVariant(routeEntry, b) - scoreVariant(routeEntry, a));
|
|
191
|
+
const maxVariants = routeEntry.route === '/' ? 5 : 4;
|
|
192
|
+
|
|
193
|
+
const selected = [];
|
|
194
|
+
const seenCodes = new Set();
|
|
195
|
+
for (const variant of sorted) {
|
|
196
|
+
if (seenCodes.has(variant.code)) continue;
|
|
197
|
+
seenCodes.add(variant.code);
|
|
198
|
+
selected.push(variant);
|
|
199
|
+
if (selected.length >= maxVariants) break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const hasCore = selected.some(variant => variant.code === 'core');
|
|
203
|
+
if (selected.length === 0) {
|
|
204
|
+
const fallback = variants.find(variant => variant.code === 'core') || variants[0];
|
|
205
|
+
return fallback ? [fallback] : [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!hasCore && (routeEntry.route === '/' || filtered.length === 1)) {
|
|
209
|
+
const core = variants.find(variant => variant.code === 'core');
|
|
210
|
+
if (core && selected.length < maxVariants) selected.push(core);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return selected.sort((a, b) => scoreVariant(routeEntry, b) - scoreVariant(routeEntry, a));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildMissionVariants(routeEntry) {
|
|
217
|
+
const signals = routeEntry.signals || {};
|
|
218
|
+
const route = routeEntry.route || '/';
|
|
219
|
+
const routeLabel = route === '/' ? 'la pagina principal' : `la pagina ${route}`;
|
|
220
|
+
const exampleRoute = routeEntry.routeExamplePath && routeEntry.routeExamplePath !== route ? routeEntry.routeExamplePath : null;
|
|
221
|
+
const variants = [{
|
|
222
|
+
code: 'core',
|
|
223
|
+
prompt: routeEntry.prompt,
|
|
224
|
+
expectedResult: routeEntry.expectedResult,
|
|
225
|
+
tags: ['core-flow'],
|
|
226
|
+
priority: routeEntry.priority || 'normal'
|
|
227
|
+
}];
|
|
228
|
+
|
|
229
|
+
if (signals.hasAuth) {
|
|
230
|
+
variants.push({
|
|
231
|
+
code: 'auth',
|
|
232
|
+
prompt: `Navega a ${route} y valida el flujo de autenticacion visible, incluyendo campos obligatorios, mensajes de error y acceso correcto cuando existan credenciales validas de prueba.`,
|
|
233
|
+
expectedResult: `${routeLabel} permite validar autenticacion y feedback de errores sin fallas visibles.`,
|
|
234
|
+
tags: ['auth', 'login', 'validation'],
|
|
235
|
+
priority: 'high'
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (signals.hasCheckout) {
|
|
240
|
+
variants.push({
|
|
241
|
+
code: 'checkout',
|
|
242
|
+
prompt: `Navega a ${route} y valida el flujo de checkout o pago visible, verificando resumen, captura de datos sensibles, validaciones y confirmacion clara del resultado sin ejecutar cobros reales.`,
|
|
243
|
+
expectedResult: `${routeLabel} permite revisar el flujo de pago con validaciones y estados consistentes sin errores visibles.`,
|
|
244
|
+
tags: ['checkout', 'payment', 'transaction'],
|
|
245
|
+
priority: 'high'
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (signals.hasForm || signals.isCrudLike) {
|
|
250
|
+
variants.push({
|
|
251
|
+
code: 'form',
|
|
252
|
+
prompt: `Navega a ${route} y valida el formulario o flujo CRUD principal, cubriendo captura, validaciones, guardado y mensajes de confirmacion o error.`,
|
|
253
|
+
expectedResult: `${routeLabel} permite capturar o editar informacion con validaciones claras y resultado consistente.`,
|
|
254
|
+
tags: ['form', 'crud', 'save'],
|
|
255
|
+
priority: signals.hasCreateAction || signals.hasEditAction ? 'high' : 'normal'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (signals.isListLike) {
|
|
260
|
+
variants.push({
|
|
261
|
+
code: 'list',
|
|
262
|
+
prompt: `Navega a ${route} y valida el listado principal, incluyendo carga de registros, filtros, busqueda, orden o paginacion segun aplique.`,
|
|
263
|
+
expectedResult: `${routeLabel} muestra datos consultables y permite usar herramientas de exploracion sin errores visibles.`,
|
|
264
|
+
tags: ['list', 'search', 'filter'],
|
|
265
|
+
priority: 'normal'
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (signals.isDynamicRoute) {
|
|
270
|
+
variants.push({
|
|
271
|
+
code: 'detail',
|
|
272
|
+
prompt: `Navega a ${route}${exampleRoute ? ` usando una ruta ejemplo como ${exampleRoute}` : ''} y valida la vista de detalle, comprobando que la informacion contextual cargue correctamente y que la accion principal asociada al registro sea usable.`,
|
|
273
|
+
expectedResult: `${routeLabel} carga informacion contextual correcta del registro y permite validar al menos una accion clave.`,
|
|
274
|
+
tags: ['detail', 'contextual-data', ...(exampleRoute ? ['example-route'] : [])],
|
|
275
|
+
priority: 'normal'
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (signals.hasSettings) {
|
|
280
|
+
variants.push({
|
|
281
|
+
code: 'settings',
|
|
282
|
+
prompt: `Navega a ${route} y valida el flujo de configuracion o preferencias, verificando persistencia visual, cambios editables y mensajes claros de guardado.`,
|
|
283
|
+
expectedResult: `${routeLabel} permite ajustar configuraciones con persistencia y feedback confiable.`,
|
|
284
|
+
tags: ['settings', 'preferences', 'persistence'],
|
|
285
|
+
priority: 'normal'
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (signals.hasUpload) {
|
|
290
|
+
variants.push({
|
|
291
|
+
code: 'upload',
|
|
292
|
+
prompt: `Navega a ${route} y valida la carga de archivos disponible, comprobando restricciones, mensajes de estado y resultado visible despues del upload.`,
|
|
293
|
+
expectedResult: `${routeLabel} permite cargar archivos con validaciones y confirmacion observable del resultado.`,
|
|
294
|
+
tags: ['upload', 'file'],
|
|
295
|
+
priority: 'normal'
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const uniqueVariants = [];
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
for (const variant of variants) {
|
|
302
|
+
const key = `${variant.code}|${variant.prompt}|${variant.expectedResult}`;
|
|
303
|
+
if (seen.has(key)) continue;
|
|
304
|
+
seen.add(key);
|
|
305
|
+
uniqueVariants.push(variant);
|
|
306
|
+
}
|
|
307
|
+
return selectUsefulVariants(routeEntry, uniqueVariants);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function generateMissionsForRoute(routeEntry, options = {}) {
|
|
311
|
+
return buildMissionVariants(routeEntry).map(variant => buildMissionVariant(routeEntry, {
|
|
312
|
+
...variant
|
|
313
|
+
}, {
|
|
314
|
+
...options,
|
|
315
|
+
routeGuidance: options.routeGuidance || {}
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function generateMissionYamlData(routeEntry, options = {}) {
|
|
320
|
+
return generateMissionsForRoute(routeEntry, options)[0];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function buildMissionIdentity(missionData) {
|
|
324
|
+
return [
|
|
325
|
+
missionData.page_path || '',
|
|
326
|
+
missionData.prompt || '',
|
|
327
|
+
missionData.expected_result || ''
|
|
328
|
+
].join('||').toLowerCase();
|
|
329
|
+
}
|