@arcadialdev/arcality 3.0.3 → 4.0.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.
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { dump, load as loadYaml } from 'js-yaml';
4
+ import { buildMissionIdentity } from './missionGenerator.mjs';
5
+ import { assertGeneratedMissionSchema } from './generatedMissionSchema.mjs';
6
+
7
+ const YAML_RE = /\.(yaml|yml)$/i;
8
+
9
+ function toPosix(value) {
10
+ return String(value || '').split(path.sep).join('/');
11
+ }
12
+
13
+ function walk(dir, out = []) {
14
+ if (!fs.existsSync(dir)) return out;
15
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ const full = path.join(dir, entry.name);
18
+ if (entry.isDirectory()) {
19
+ if (entry.name === '_templates') continue;
20
+ walk(full, out);
21
+ } else {
22
+ out.push(full);
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function sanitizeName(value) {
29
+ return String(value || 'generated_mission')
30
+ .trim()
31
+ .replace(/[^\w.-]+/g, '_')
32
+ .replace(/_+/g, '_')
33
+ .replace(/^_+|_+$/g, '') || 'generated_mission';
34
+ }
35
+
36
+ function ensureDir(dir) {
37
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
38
+ }
39
+
40
+ function loadExistingMissionIdentities(targetDir) {
41
+ const identities = new Set();
42
+ if (!fs.existsSync(targetDir)) return identities;
43
+
44
+ const files = walk(targetDir).filter(file => YAML_RE.test(file)).filter(file => !/[\\/]_collection\.(yaml|yml)$/i.test(file));
45
+ for (const file of files) {
46
+ try {
47
+ const parsed = loadYaml(fs.readFileSync(file, 'utf8'));
48
+ if (!parsed || typeof parsed !== 'object') continue;
49
+ const identity = buildMissionIdentity(parsed);
50
+ if (identity) identities.add(identity);
51
+ } catch {
52
+ // Silencioso: si un YAML existente no se puede leer, no bloqueamos la generación.
53
+ }
54
+ }
55
+
56
+ return identities;
57
+ }
58
+
59
+ function writeCache(projectRoot, payload) {
60
+ const cacheDir = path.join(projectRoot, '.arcality', 'cache');
61
+ ensureDir(cacheDir);
62
+ fs.writeFileSync(path.join(cacheDir, 'generate-state.json'), JSON.stringify(payload, null, 2), 'utf8');
63
+ }
64
+
65
+ export function saveGeneratedMissions({ projectRoot, targetDir, missions, dryRun = false }) {
66
+ const existingIdentities = loadExistingMissionIdentities(targetDir);
67
+ const created = [];
68
+ const skipped = [];
69
+
70
+ if (!dryRun) ensureDir(targetDir);
71
+
72
+ for (const mission of missions) {
73
+ assertGeneratedMissionSchema(mission);
74
+ const identity = buildMissionIdentity(mission);
75
+ if (!identity || existingIdentities.has(identity)) {
76
+ skipped.push({
77
+ name: mission.name,
78
+ page_path: mission.page_path,
79
+ reason: 'duplicate'
80
+ });
81
+ continue;
82
+ }
83
+
84
+ const collectionRel = String(mission.collection || 'generated/general').replace(/^\/+/, '');
85
+ const targetCollectionDir = path.join(targetDir, collectionRel);
86
+ const fileName = `${sanitizeName(mission.name)}.yaml`;
87
+ const filePath = path.join(targetCollectionDir, fileName);
88
+
89
+ if (!dryRun) {
90
+ ensureDir(targetCollectionDir);
91
+ fs.writeFileSync(filePath, dump(mission, { lineWidth: 120, noRefs: true }), 'utf8');
92
+ }
93
+
94
+ existingIdentities.add(identity);
95
+ created.push({
96
+ name: mission.name,
97
+ page_path: mission.page_path,
98
+ filePath: toPosix(path.relative(projectRoot, filePath))
99
+ });
100
+ }
101
+
102
+ const summary = {
103
+ generated_at: new Date().toISOString(),
104
+ target_dir: toPosix(path.relative(projectRoot, targetDir) || '.'),
105
+ total_requested: missions.length,
106
+ created_count: created.length,
107
+ skipped_count: skipped.length,
108
+ created,
109
+ skipped
110
+ };
111
+
112
+ if (!dryRun) {
113
+ writeCache(projectRoot, summary);
114
+ }
115
+
116
+ return summary;
117
+ }
@@ -0,0 +1,242 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ function asTrimmedString(value) {
5
+ if (typeof value !== 'string') return null;
6
+ const trimmed = value.trim();
7
+ return trimmed || null;
8
+ }
9
+
10
+ function asStringList(value) {
11
+ if (!Array.isArray(value)) return [];
12
+ return value.map(asTrimmedString).filter(Boolean);
13
+ }
14
+
15
+ function normalizeRuleText(value) {
16
+ return String(value || '')
17
+ .replace(/^[-*+]\s+/, '')
18
+ .replace(/^\d+\.\s+/, '')
19
+ .trim();
20
+ }
21
+
22
+ function tokenize(value) {
23
+ return String(value || '')
24
+ .toLowerCase()
25
+ .split(/[^a-z0-9áéíóúñ]+/i)
26
+ .map(token => token.trim())
27
+ .filter(token => token.length >= 3);
28
+ }
29
+
30
+ function stripComments(content) {
31
+ return String(content || '').replace(/<!--[\s\S]*?-->/g, '');
32
+ }
33
+
34
+ function parseQaContext(projectRoot) {
35
+ const filePath = path.join(projectRoot, '.arcality', 'qa-context.md');
36
+ const metaPath = path.join(projectRoot, '.arcality', 'qa-context.meta.json');
37
+ const result = {
38
+ exists: false,
39
+ filePath,
40
+ status: 'missing',
41
+ warnings: [],
42
+ rules: [],
43
+ tags: [],
44
+ governance: {
45
+ exists: false,
46
+ version: null,
47
+ updatedBy: null,
48
+ ownerTeam: null,
49
+ changeSummary: null
50
+ }
51
+ };
52
+
53
+ if (fs.existsSync(metaPath)) {
54
+ try {
55
+ const parsed = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
56
+ result.governance = {
57
+ exists: true,
58
+ version: asTrimmedString(parsed?.version),
59
+ updatedBy: asTrimmedString(parsed?.updated_by ?? parsed?.updatedBy),
60
+ ownerTeam: asTrimmedString(parsed?.owner_team ?? parsed?.ownerTeam),
61
+ changeSummary: asTrimmedString(parsed?.change_summary ?? parsed?.changeSummary)
62
+ };
63
+ result.tags = asStringList(parsed?.tags);
64
+ } catch {
65
+ result.warnings.push('qa-context.meta.json no se pudo parsear para generation guidance.');
66
+ }
67
+ }
68
+
69
+ if (!fs.existsSync(filePath)) {
70
+ return result;
71
+ }
72
+
73
+ result.exists = true;
74
+ const raw = stripComments(fs.readFileSync(filePath, 'utf8'));
75
+ const lines = raw.split(/\r?\n/);
76
+ let currentSection = 'General';
77
+ const seen = new Set();
78
+
79
+ for (const rawLine of lines) {
80
+ const line = rawLine.trim();
81
+ if (!line) continue;
82
+ if (/^##\s+/.test(line)) {
83
+ currentSection = line.replace(/^##\s+/, '').trim() || 'General';
84
+ continue;
85
+ }
86
+ if (!/^[-*+]\s+/.test(line) && !/^\d+\.\s+/.test(line)) continue;
87
+ const rule = normalizeRuleText(line);
88
+ if (!rule) continue;
89
+ const dedupeKey = rule.toLowerCase();
90
+ if (seen.has(dedupeKey)) continue;
91
+ seen.add(dedupeKey);
92
+ result.rules.push({
93
+ section: currentSection,
94
+ text: rule,
95
+ tokens: tokenize(rule)
96
+ });
97
+ }
98
+
99
+ result.status = result.rules.length > 0 ? 'ready' : 'empty';
100
+ if (result.exists && !result.governance.exists) {
101
+ result.warnings.push('QA context existe pero no tiene metadata de gobernanza.');
102
+ }
103
+ if (result.exists && result.rules.length === 0) {
104
+ result.warnings.push('QA context sin reglas accionables.');
105
+ }
106
+
107
+ return result;
108
+ }
109
+
110
+ function parseFeasibilityDoc(projectRoot) {
111
+ const filePath = path.join(projectRoot, 'BUGSTER_FEATURE_FEASIBILITY.md');
112
+ const result = {
113
+ exists: false,
114
+ filePath,
115
+ warnings: [],
116
+ globalHints: [],
117
+ routeHints: {
118
+ dynamic: [],
119
+ checkout: [],
120
+ form: [],
121
+ auth: [],
122
+ list: []
123
+ }
124
+ };
125
+
126
+ if (!fs.existsSync(filePath)) {
127
+ return result;
128
+ }
129
+
130
+ result.exists = true;
131
+ const raw = fs.readFileSync(filePath, 'utf8');
132
+ const lower = raw.toLowerCase();
133
+
134
+ if (lower.includes('pocos escenarios observables y accionables')) {
135
+ result.globalHints.push('Prioriza escenarios observables y accionables sobre cobertura generica.');
136
+ }
137
+ if (lower.includes('rutas dinamicas necesitan ejemplos de parametros')) {
138
+ result.routeHints.dynamic.push('Si la ruta es dinamica, valida contexto del registro y considera necesidad de parametros de ejemplo.');
139
+ }
140
+ if (lower.includes('focus on validation and payment edge cases') || lower.includes('payment edge cases')) {
141
+ result.routeHints.checkout.push('Enfatiza validaciones y edge cases de pago sin ejecutar cobros reales.');
142
+ }
143
+ if (lower.includes('validaciones visibles') || lower.includes('formularios')) {
144
+ result.routeHints.form.push('Prioriza validaciones visibles, mensajes de error y reglas de captura observables.');
145
+ }
146
+ if (lower.includes('autenticacion') || lower.includes('auth')) {
147
+ result.routeHints.auth.push('Valida autenticacion con feedback observable y estados de acceso consistentes.');
148
+ }
149
+ if (lower.includes('filtros') || lower.includes('busqueda') || lower.includes('listado')) {
150
+ result.routeHints.list.push('Valida exploracion de datos mediante listados, filtros y busqueda cuando aplique.');
151
+ }
152
+
153
+ if (result.globalHints.length === 0) {
154
+ result.globalHints.push('Prioriza escenarios observables y accionables sobre cobertura generica.');
155
+ }
156
+
157
+ return result;
158
+ }
159
+
160
+ function buildRouteKeywords(routeEntry) {
161
+ return Array.from(new Set([
162
+ ...tokenize(routeEntry.route),
163
+ ...tokenize(routeEntry.pagePath),
164
+ ...(routeEntry.tags || []).flatMap(tokenize),
165
+ ...((routeEntry.signals?.keywords) || []).flatMap(tokenize)
166
+ ]));
167
+ }
168
+
169
+ function collectThemeKeywords(routeEntry) {
170
+ const signals = routeEntry.signals || {};
171
+ const routeKeywords = buildRouteKeywords(routeEntry);
172
+ const themes = new Set(routeKeywords);
173
+
174
+ if (signals.hasAuth) ['login', 'auth', 'password', 'session', 'acceso'].forEach(value => themes.add(value));
175
+ if (signals.hasCheckout) ['checkout', 'payment', 'billing', 'pago', 'card'].forEach(value => themes.add(value));
176
+ if (signals.hasForm || signals.isCrudLike) ['form', 'formulario', 'validacion', 'guardar', 'campo'].forEach(value => themes.add(value));
177
+ if (signals.isListLike) ['list', 'listado', 'tabla', 'busqueda', 'filtro'].forEach(value => themes.add(value));
178
+ if (signals.hasSettings) ['settings', 'configuracion', 'preferencias'].forEach(value => themes.add(value));
179
+ if (signals.hasUpload) ['upload', 'archivo', 'imagen'].forEach(value => themes.add(value));
180
+ if (signals.isDynamicRoute) ['detalle', 'detail', 'registro', 'contexto'].forEach(value => themes.add(value));
181
+
182
+ return Array.from(themes);
183
+ }
184
+
185
+ function scoreRule(rule, routeKeywords) {
186
+ const tokens = rule.tokens || [];
187
+ let score = 0;
188
+ for (const token of tokens) {
189
+ if (routeKeywords.includes(token)) score += 2;
190
+ }
191
+
192
+ const lower = rule.text.toLowerCase();
193
+ if (/(nunca|siempre|obligatorio|critic|must|required|bloque)/i.test(lower)) score += 1;
194
+ return score;
195
+ }
196
+
197
+ function inferContextPriority(matchedRules = []) {
198
+ for (const rule of matchedRules) {
199
+ if (/(critic|obligatorio|nunca|must|required|seguridad|pago|payment|auth)/i.test(rule.text)) {
200
+ return 'high';
201
+ }
202
+ }
203
+ return null;
204
+ }
205
+
206
+ export function loadGenerationContext(projectRoot = process.cwd()) {
207
+ return {
208
+ qaContext: parseQaContext(projectRoot),
209
+ feasibility: parseFeasibilityDoc(projectRoot)
210
+ };
211
+ }
212
+
213
+ export function resolveRouteGuidance(routeEntry, generationContext = {}) {
214
+ const qaContext = generationContext.qaContext || { exists: false, rules: [], tags: [], warnings: [] };
215
+ const feasibility = generationContext.feasibility || { exists: false, globalHints: [], routeHints: {} };
216
+ const routeKeywords = collectThemeKeywords(routeEntry);
217
+
218
+ const matchedRules = (qaContext.rules || [])
219
+ .map(rule => ({ ...rule, score: scoreRule(rule, routeKeywords) }))
220
+ .filter(rule => rule.score > 0)
221
+ .sort((a, b) => b.score - a.score)
222
+ .slice(0, 3);
223
+
224
+ const feasibilityHints = [...(feasibility.globalHints || [])];
225
+ const signals = routeEntry.signals || {};
226
+ if (signals.isDynamicRoute) feasibilityHints.push(...(feasibility.routeHints?.dynamic || []));
227
+ if (signals.hasCheckout) feasibilityHints.push(...(feasibility.routeHints?.checkout || []));
228
+ if (signals.hasForm || signals.isCrudLike) feasibilityHints.push(...(feasibility.routeHints?.form || []));
229
+ if (signals.hasAuth) feasibilityHints.push(...(feasibility.routeHints?.auth || []));
230
+ if (signals.isListLike) feasibilityHints.push(...(feasibility.routeHints?.list || []));
231
+
232
+ return {
233
+ matchedRules,
234
+ feasibilityHints: Array.from(new Set(feasibilityHints)).slice(0, 3),
235
+ priorityOverride: inferContextPriority(matchedRules),
236
+ extraTags: Array.from(new Set(qaContext.tags || [])).slice(0, 4),
237
+ contextStatus: {
238
+ qaContext: qaContext.exists ? qaContext.status : 'missing',
239
+ feasibility: feasibility.exists ? 'loaded' : 'missing'
240
+ }
241
+ };
242
+ }
@@ -0,0 +1,328 @@
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.pagePath,
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
+ route_hash: simpleHash(`${route}|${routeEntry.pagePath}`),
99
+ prompt_hash: simpleHash(prompt)
100
+ }
101
+ };
102
+ }
103
+
104
+ function scoreVariant(routeEntry, variant) {
105
+ const signals = routeEntry.signals || {};
106
+ let score = 0;
107
+
108
+ switch (variant.code) {
109
+ case 'checkout':
110
+ score += 120;
111
+ break;
112
+ case 'auth':
113
+ score += 110;
114
+ break;
115
+ case 'form':
116
+ score += 95;
117
+ break;
118
+ case 'detail':
119
+ score += 90;
120
+ break;
121
+ case 'upload':
122
+ score += 85;
123
+ break;
124
+ case 'settings':
125
+ score += 80;
126
+ break;
127
+ case 'list':
128
+ score += 75;
129
+ break;
130
+ case 'core':
131
+ score += 40;
132
+ break;
133
+ default:
134
+ score += 20;
135
+ }
136
+
137
+ score += variant.priority === 'high' ? 10 : 0;
138
+ score += routeEntry.route === '/' ? 6 : 0;
139
+ score += (routeEntry.routeParams || []).length * 4;
140
+ score += signals.hasSearch || signals.hasFilter || signals.hasTable ? 4 : 0;
141
+ score += signals.hasForm ? 4 : 0;
142
+ score += signals.hasUpload ? 4 : 0;
143
+ return score;
144
+ }
145
+
146
+ function shouldKeepVariant(routeEntry, variant, allVariants) {
147
+ const signals = routeEntry.signals || {};
148
+ const hasSpecialized = allVariants.some(item => item.code !== 'core');
149
+ const isRootLike = routeEntry.route === '/' || signals.isDashboardLike;
150
+
151
+ if (variant.code === 'core') {
152
+ if (!hasSpecialized) return true;
153
+ return isRootLike;
154
+ }
155
+
156
+ if (variant.code === 'list') {
157
+ return Boolean(signals.hasTable || signals.hasSearch || signals.hasFilter || routeEntry.route.includes('list'));
158
+ }
159
+
160
+ if (variant.code === 'form') {
161
+ return Boolean(signals.hasForm || signals.hasCreateAction || signals.hasEditAction || signals.isCrudLike);
162
+ }
163
+
164
+ if (variant.code === 'settings') {
165
+ return Boolean(signals.hasSettings);
166
+ }
167
+
168
+ if (variant.code === 'upload') {
169
+ return Boolean(signals.hasUpload);
170
+ }
171
+
172
+ if (variant.code === 'detail') {
173
+ return Boolean(signals.isDynamicRoute || (routeEntry.routeParams || []).length > 0);
174
+ }
175
+
176
+ if (variant.code === 'auth') {
177
+ return Boolean(signals.hasAuth);
178
+ }
179
+
180
+ if (variant.code === 'checkout') {
181
+ return Boolean(signals.hasCheckout);
182
+ }
183
+
184
+ return true;
185
+ }
186
+
187
+ function selectUsefulVariants(routeEntry, variants) {
188
+ const filtered = variants.filter(variant => shouldKeepVariant(routeEntry, variant, variants));
189
+ const sorted = filtered.sort((a, b) => scoreVariant(routeEntry, b) - scoreVariant(routeEntry, a));
190
+ const maxVariants = routeEntry.route === '/' ? 5 : 4;
191
+
192
+ const selected = [];
193
+ const seenCodes = new Set();
194
+ for (const variant of sorted) {
195
+ if (seenCodes.has(variant.code)) continue;
196
+ seenCodes.add(variant.code);
197
+ selected.push(variant);
198
+ if (selected.length >= maxVariants) break;
199
+ }
200
+
201
+ const hasCore = selected.some(variant => variant.code === 'core');
202
+ if (selected.length === 0) {
203
+ const fallback = variants.find(variant => variant.code === 'core') || variants[0];
204
+ return fallback ? [fallback] : [];
205
+ }
206
+
207
+ if (!hasCore && (routeEntry.route === '/' || filtered.length === 1)) {
208
+ const core = variants.find(variant => variant.code === 'core');
209
+ if (core && selected.length < maxVariants) selected.push(core);
210
+ }
211
+
212
+ return selected.sort((a, b) => scoreVariant(routeEntry, b) - scoreVariant(routeEntry, a));
213
+ }
214
+
215
+ function buildMissionVariants(routeEntry) {
216
+ const signals = routeEntry.signals || {};
217
+ const route = routeEntry.route || '/';
218
+ const routeLabel = route === '/' ? 'la pagina principal' : `la pagina ${route}`;
219
+ const exampleRoute = routeEntry.routeExamplePath && routeEntry.routeExamplePath !== route ? routeEntry.routeExamplePath : null;
220
+ const variants = [{
221
+ code: 'core',
222
+ prompt: routeEntry.prompt,
223
+ expectedResult: routeEntry.expectedResult,
224
+ tags: ['core-flow'],
225
+ priority: routeEntry.priority || 'normal'
226
+ }];
227
+
228
+ if (signals.hasAuth) {
229
+ variants.push({
230
+ code: 'auth',
231
+ 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.`,
232
+ expectedResult: `${routeLabel} permite validar autenticacion y feedback de errores sin fallas visibles.`,
233
+ tags: ['auth', 'login', 'validation'],
234
+ priority: 'high'
235
+ });
236
+ }
237
+
238
+ if (signals.hasCheckout) {
239
+ variants.push({
240
+ code: 'checkout',
241
+ 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.`,
242
+ expectedResult: `${routeLabel} permite revisar el flujo de pago con validaciones y estados consistentes sin errores visibles.`,
243
+ tags: ['checkout', 'payment', 'transaction'],
244
+ priority: 'high'
245
+ });
246
+ }
247
+
248
+ if (signals.hasForm || signals.isCrudLike) {
249
+ variants.push({
250
+ code: 'form',
251
+ prompt: `Navega a ${route} y valida el formulario o flujo CRUD principal, cubriendo captura, validaciones, guardado y mensajes de confirmacion o error.`,
252
+ expectedResult: `${routeLabel} permite capturar o editar informacion con validaciones claras y resultado consistente.`,
253
+ tags: ['form', 'crud', 'save'],
254
+ priority: signals.hasCreateAction || signals.hasEditAction ? 'high' : 'normal'
255
+ });
256
+ }
257
+
258
+ if (signals.isListLike) {
259
+ variants.push({
260
+ code: 'list',
261
+ prompt: `Navega a ${route} y valida el listado principal, incluyendo carga de registros, filtros, busqueda, orden o paginacion segun aplique.`,
262
+ expectedResult: `${routeLabel} muestra datos consultables y permite usar herramientas de exploracion sin errores visibles.`,
263
+ tags: ['list', 'search', 'filter'],
264
+ priority: 'normal'
265
+ });
266
+ }
267
+
268
+ if (signals.isDynamicRoute) {
269
+ variants.push({
270
+ code: 'detail',
271
+ 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.`,
272
+ expectedResult: `${routeLabel} carga informacion contextual correcta del registro y permite validar al menos una accion clave.`,
273
+ tags: ['detail', 'contextual-data', ...(exampleRoute ? ['example-route'] : [])],
274
+ priority: 'normal'
275
+ });
276
+ }
277
+
278
+ if (signals.hasSettings) {
279
+ variants.push({
280
+ code: 'settings',
281
+ prompt: `Navega a ${route} y valida el flujo de configuracion o preferencias, verificando persistencia visual, cambios editables y mensajes claros de guardado.`,
282
+ expectedResult: `${routeLabel} permite ajustar configuraciones con persistencia y feedback confiable.`,
283
+ tags: ['settings', 'preferences', 'persistence'],
284
+ priority: 'normal'
285
+ });
286
+ }
287
+
288
+ if (signals.hasUpload) {
289
+ variants.push({
290
+ code: 'upload',
291
+ prompt: `Navega a ${route} y valida la carga de archivos disponible, comprobando restricciones, mensajes de estado y resultado visible despues del upload.`,
292
+ expectedResult: `${routeLabel} permite cargar archivos con validaciones y confirmacion observable del resultado.`,
293
+ tags: ['upload', 'file'],
294
+ priority: 'normal'
295
+ });
296
+ }
297
+
298
+ const uniqueVariants = [];
299
+ const seen = new Set();
300
+ for (const variant of variants) {
301
+ const key = `${variant.code}|${variant.prompt}|${variant.expectedResult}`;
302
+ if (seen.has(key)) continue;
303
+ seen.add(key);
304
+ uniqueVariants.push(variant);
305
+ }
306
+ return selectUsefulVariants(routeEntry, uniqueVariants);
307
+ }
308
+
309
+ export function generateMissionsForRoute(routeEntry, options = {}) {
310
+ return buildMissionVariants(routeEntry).map(variant => buildMissionVariant(routeEntry, {
311
+ ...variant
312
+ }, {
313
+ ...options,
314
+ routeGuidance: options.routeGuidance || {}
315
+ }));
316
+ }
317
+
318
+ export function generateMissionYamlData(routeEntry, options = {}) {
319
+ return generateMissionsForRoute(routeEntry, options)[0];
320
+ }
321
+
322
+ export function buildMissionIdentity(missionData) {
323
+ return [
324
+ missionData.page_path || '',
325
+ missionData.prompt || '',
326
+ missionData.expected_result || ''
327
+ ].join('||').toLowerCase();
328
+ }