@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.
@@ -1,4 +1,5 @@
1
1
  import fs from 'fs';
2
+ import os from 'os';
2
3
  import path from 'path';
3
4
  import { fileURLToPath } from 'url';
4
5
 
@@ -9,11 +10,11 @@ const ARCALITY_ROOT = process.env.ARCALITY_ROOT || path.resolve(__dirname, '..')
9
10
 
10
11
  async function rebrandReport() {
11
12
  // Definir directorios objetivo: El reporte personalizado, el nativo anidado y el reporte interno
12
- const reportDir = process.env.REPORTS_DIR || 'tests-report';
13
+ const reportDir = process.env.REPORTS_DIR || path.join(os.tmpdir(), 'arcality', 'reports');
13
14
  const targetDirs = [
14
15
  reportDir,
15
16
  path.join(reportDir, 'native'),
16
- 'playwright-internal-report'
17
+ path.join(os.tmpdir(), 'arcality', 'playwright-internal-report')
17
18
  ];
18
19
 
19
20
  const logoPngPath = path.join(ARCALITY_ROOT, 'public', 'logo.png');
@@ -52,22 +53,22 @@ function processDirectory(reportDir, logoBase64) {
52
53
  html = html.replace(/at\s+.*?node_modules.*?(\n|\r)/g, '');
53
54
  html = html.replace(/at\s+.*?agentic-runner\.spec\.ts:\d+/g, '');
54
55
 
55
- // 3. Preserve custom Arcality reports before native Playwright styling.
56
- // The custom Arcality report owns its visual system. Keep the
57
- // aggressive Playwright rebrand injection for native reports only.
58
- const isArcalityMissionConsole = html.includes('class="mission-brief"') || html.includes('Arcality QA Engine');
59
- if (isArcalityMissionConsole) {
60
- if (logoBase64) {
61
- html = html.replace(/rel="shortcut icon" href=".*?"/g, `rel="shortcut icon" href="${logoBase64}"`);
62
- html = html.replace(/rel="icon" href=".*?"/g, `rel="icon" href="${logoBase64}"`);
63
- }
64
- fs.writeFileSync(indexPath, html, 'utf8');
65
- processTraceViewer(reportDir, logoBase64);
66
- return;
67
- }
68
-
69
- // 4. Inyección de Estilos y Scripts (MODO AGRESIVO)
70
- const customStyles = `
56
+ // 3. Preserve custom Arcality reports before native Playwright styling.
57
+ // The custom Arcality report owns its visual system. Keep the
58
+ // aggressive Playwright rebrand injection for native reports only.
59
+ const isArcalityMissionConsole = html.includes('class="mission-brief"') || html.includes('Arcality QA Engine');
60
+ if (isArcalityMissionConsole) {
61
+ if (logoBase64) {
62
+ html = html.replace(/rel="shortcut icon" href=".*?"/g, `rel="shortcut icon" href="${logoBase64}"`);
63
+ html = html.replace(/rel="icon" href=".*?"/g, `rel="icon" href="${logoBase64}"`);
64
+ }
65
+ fs.writeFileSync(indexPath, html, 'utf8');
66
+ processTraceViewer(reportDir, logoBase64);
67
+ return;
68
+ }
69
+
70
+ // 4. Inyección de Estilos y Scripts (MODO AGRESIVO)
71
+ const customStyles = `
71
72
  <script id="arcality-branding-script">
72
73
  /* ARCALITY_BRANDING_SCRIPT */
73
74
  // 1. Limpieza de Caché y Service Worker
@@ -30,22 +30,22 @@ export interface PortalContextResponse {
30
30
  fields: Array<{ field_identifier: string; field_type: string; is_required: boolean }>;
31
31
  }
32
32
 
33
- export interface ProjectDto {
34
- id: string;
35
- name: string;
36
- base_url?: string;
37
- }
38
-
39
- function isRealProjectId(projectId?: string | null): boolean {
40
- const id = String(projectId || '').trim();
41
- if (!id) return false;
42
- if (id === 'undefined' || id === 'null') return false;
43
- if (/^(local|mock)_/i.test(id)) return false;
44
- if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(id)) return false;
45
- return true;
46
- }
47
-
48
- export class KnowledgeService {
33
+ export interface ProjectDto {
34
+ id: string;
35
+ name: string;
36
+ base_url?: string;
37
+ }
38
+
39
+ function isRealProjectId(projectId?: string | null): boolean {
40
+ const id = String(projectId || '').trim();
41
+ if (!id) return false;
42
+ if (id === 'undefined' || id === 'null') return false;
43
+ if (/^(local|mock)_/i.test(id)) return false;
44
+ if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(id)) return false;
45
+ return true;
46
+ }
47
+
48
+ export class KnowledgeService {
49
49
  private static instance: KnowledgeService;
50
50
  private apiBase: string;
51
51
  private apiKey: string;
@@ -89,10 +89,10 @@ export class KnowledgeService {
89
89
  }
90
90
 
91
91
  public getProjectId(): string | null {
92
- if (!isRealProjectId(this.projectId)) {
93
- this.projectId = process.env.ARCALITY_PROJECT_ID || null;
94
- }
95
- return isRealProjectId(this.projectId) ? this.projectId : null;
92
+ if (!isRealProjectId(this.projectId)) {
93
+ this.projectId = process.env.ARCALITY_PROJECT_ID || null;
94
+ }
95
+ return isRealProjectId(this.projectId) ? this.projectId : null;
96
96
  }
97
97
 
98
98
  /**
@@ -107,7 +107,7 @@ export class KnowledgeService {
107
107
  const data = await res.json();
108
108
  return data.projects || [];
109
109
  } catch (e: any) {
110
- if (process.env.DEBUG) console.warn(chalk.yellow(`[KnowledgeService] Error al obtener proyectos: ${e.message}`));
110
+ if (process.env.DEBUG) console.warn(chalk.yellow(`[KnowledgeService] Error al obtener proyectos: ${e.message}`));
111
111
  return [];
112
112
  }
113
113
  }
@@ -125,7 +125,7 @@ export class KnowledgeService {
125
125
  if (!res.ok) return null;
126
126
  return await res.json();
127
127
  } catch (e: any) {
128
- if (process.env.DEBUG) console.error(chalk.red(`[KnowledgeService] Falló creación de proyecto: ${e.message}`));
128
+ if (process.env.DEBUG) console.error(chalk.red(`[KnowledgeService] Falló creación de proyecto: ${e.message}`));
129
129
  return null;
130
130
  }
131
131
  }
@@ -134,8 +134,8 @@ export class KnowledgeService {
134
134
  * 2. Ingesta (Post-Percept)
135
135
  */
136
136
  public async ingest(payload: PortalIngestRequest): Promise<void> {
137
- if (!isRealProjectId(payload.project_id)) {
138
- if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Ingesta cancelada: project_id es inválido.`));
137
+ if (!isRealProjectId(payload.project_id)) {
138
+ if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Ingesta cancelada: project_id es inválido.`));
139
139
  return;
140
140
  }
141
141
 
@@ -157,7 +157,7 @@ export class KnowledgeService {
157
157
  }
158
158
  }
159
159
  } catch (e: any) {
160
- if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Error en ingesta: ${e.message}`));
160
+ if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Error en ingesta: ${e.message}`));
161
161
  }
162
162
  }
163
163
 
@@ -179,7 +179,7 @@ export class KnowledgeService {
179
179
  if (!res.ok) return null;
180
180
  return await res.json();
181
181
  } catch (e: any) {
182
- if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Error al obtener contexto: ${e.message}`));
182
+ if (process.env.DEBUG) console.warn(chalk.yellow(`[Knowledge] Error al obtener contexto: ${e.message}`));
183
183
  return null;
184
184
  }
185
185
  }
@@ -207,7 +207,7 @@ export class KnowledgeService {
207
207
  severity
208
208
  })
209
209
  });
210
- if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nueva regla registrada: ${title}`));
210
+ if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nueva regla registrada: ${title}`));
211
211
  } catch (e) { }
212
212
  }
213
213
 
@@ -233,7 +233,7 @@ export class KnowledgeService {
233
233
  source: "agent_auto"
234
234
  })
235
235
  });
236
- if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nuevo campo catalogado: ${identifier}`));
236
+ if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nuevo campo catalogado: ${identifier}`));
237
237
  } catch (e) { }
238
238
  }
239
239
 
@@ -259,7 +259,7 @@ export class KnowledgeService {
259
259
  source: "agent_auto"
260
260
  })
261
261
  });
262
- if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nuevo conocimiento guardado: ${title}`));
262
+ if (process.env.DEBUG) console.log(chalk.green(`[Knowledge] Nuevo conocimiento guardado: ${title}`));
263
263
  } catch (e) { }
264
264
  }
265
265
 
@@ -306,10 +306,16 @@ function simpleHash(str) {
306
306
  hash = ((hash << 5) - hash) + char;
307
307
  hash |= 0;
308
308
  }
309
- return 'ph_' + Math.abs(hash).toString(36);
310
- }
311
-
312
- /**
309
+ return 'ph_' + Math.abs(hash).toString(36);
310
+ }
311
+
312
+ function normalizeBugClassification(value) {
313
+ const text = String(value || '').trim();
314
+ const allowed = ['[Backend Bug]', '[Frontend Bug]', '[UI Regression]', '[Flaky/Stale Selector]'];
315
+ return allowed.find(category => text.includes(category)) || '';
316
+ }
317
+
318
+ /**
313
319
  * Crea una Task en Azure DevOps cuando se detecta un bug en el portal.
314
320
  * Si la organización no tiene la integración configurada (HTTP 400) se registra
315
321
  * sólo en modo DEBUG y NO interrumpe el flujo del test runner.
@@ -318,7 +324,7 @@ function simpleHash(str) {
318
324
  * @param {string} description - Descripción detallada del bug detectado
319
325
  * @returns {Promise<{success: boolean, taskUrl?: string, error?: string}>}
320
326
  */
321
- export async function createAdoTask(title, description) {
327
+ export async function createAdoTask(title, description, metadata = {}) {
322
328
  const apiBase = getEffectiveApiBase();
323
329
  if (!apiBase) {
324
330
  if (process.env.DEBUG) console.log(`[ADO] No hay ARCALITY_API_URL configurado. Omitiendo creación de Task en Azure DevOps.`);
@@ -337,19 +343,26 @@ export async function createAdoTask(title, description) {
337
343
  return { success: false, error: 'no_api_key' };
338
344
  }
339
345
 
340
- try {
341
- const controller = new AbortController();
342
- const timeout = setTimeout(() => controller.abort(), 15000);
343
-
344
- const res = await fetch(`${apiBase}/api/v1/integrations/azuredevops/task`, {
345
- method: 'POST',
346
- headers: {
347
- 'Content-Type': 'application/json',
348
- 'x-api-key': key
349
- },
350
- body: JSON.stringify({ project_id: projectId, title, description }),
351
- signal: controller.signal
352
- });
346
+ try {
347
+ const controller = new AbortController();
348
+ const timeout = setTimeout(() => controller.abort(), 15000);
349
+ const bugClassification = normalizeBugClassification(metadata?.classification);
350
+ const taskTitle = bugClassification && !String(title).includes(bugClassification)
351
+ ? `${bugClassification} ${title}`
352
+ : title;
353
+ const taskDescription = bugClassification && !String(description).includes('Clasificacion')
354
+ ? `**Clasificacion:** ${bugClassification}\n\n${description}`
355
+ : description;
356
+
357
+ const res = await fetch(`${apiBase}/api/v1/integrations/azuredevops/task`, {
358
+ method: 'POST',
359
+ headers: {
360
+ 'Content-Type': 'application/json',
361
+ 'x-api-key': key
362
+ },
363
+ body: JSON.stringify({ project_id: projectId, title: taskTitle, description: taskDescription }),
364
+ signal: controller.signal
365
+ });
353
366
  clearTimeout(timeout);
354
367
 
355
368
  if (res.status === 400) {
@@ -1,10 +1,10 @@
1
1
  // src/configLoader.mjs
2
2
  // Configuration loader — supports arcality.config (primary) and .env / global config (fallback)
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import os from 'node:os';
6
- import dotenv from 'dotenv';
7
- import { isRealProjectId } from './configManager.mjs';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import dotenv from 'dotenv';
7
+ import { isRealProjectId } from './configManager.mjs';
8
8
 
9
9
  dotenv.config();
10
10
 
@@ -59,22 +59,22 @@ export function loadConfig() {
59
59
  ARCALITY_PROJECT_ID: localConfig?.projectId || process.env.ARCALITY_PROJECT_ID,
60
60
  };
61
61
 
62
- if (!config.ARCALITY_API_KEY) {
63
- throw new Error('ARCALITY_API_KEY is not defined. Run `arcality init` or set it in your .env file.');
64
- }
65
-
66
- if (!isRealProjectId(config.ARCALITY_PROJECT_ID)) {
67
- throw new Error('ARCALITY_PROJECT_ID is not connected to a real Arcality project. Run `arcality init`.');
68
- }
69
-
70
- return config;
71
- }
62
+ if (!config.ARCALITY_API_KEY) {
63
+ throw new Error('ARCALITY_API_KEY is not defined. Run `arcality init` or set it in your .env file.');
64
+ }
65
+
66
+ if (!isRealProjectId(config.ARCALITY_PROJECT_ID)) {
67
+ throw new Error('ARCALITY_PROJECT_ID is not connected to a real Arcality project. Run `arcality init`.');
68
+ }
72
69
 
73
- export function loadProjectId() {
74
- const localConfig = loadLocalArcalityConfig();
75
- const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID;
76
- return isRealProjectId(projectId) ? projectId : null;
77
- }
70
+ return config;
71
+ }
72
+
73
+ export function loadProjectId() {
74
+ const localConfig = loadLocalArcalityConfig();
75
+ const projectId = localConfig?.projectId || process.env.ARCALITY_PROJECT_ID;
76
+ return isRealProjectId(projectId) ? projectId : null;
77
+ }
78
78
 
79
79
 
80
80
  /**
@@ -133,25 +133,25 @@ export function getApiKey() {
133
133
  return { key: null, source: 'none' };
134
134
  }
135
135
 
136
- /**
137
- * Loads supported Arcality runtime values into process.env.
138
- */
139
- export function setupProcessEnv() {
140
- const config = loadGlobalConfig();
141
- if (!config) return;
142
-
143
- // Load Arcality API Key
144
- getApiKey();
145
-
146
- // Also inject arcality.config values into process.env for backward compat
147
- const localConfig = loadLocalArcalityConfig();
136
+ /**
137
+ * Loads supported Arcality runtime values into process.env.
138
+ */
139
+ export function setupProcessEnv() {
140
+ const config = loadGlobalConfig();
141
+ if (!config) return;
142
+
143
+ // Load Arcality API Key
144
+ getApiKey();
145
+
146
+ // Also inject arcality.config values into process.env for backward compat
147
+ const localConfig = loadLocalArcalityConfig();
148
148
  if (localConfig) {
149
149
  if (localConfig.apiKey && !process.env.ARCALITY_API_KEY) {
150
150
  process.env.ARCALITY_API_KEY = localConfig.apiKey;
151
151
  }
152
- if (isRealProjectId(localConfig.projectId) && !process.env.ARCALITY_PROJECT_ID) {
153
- process.env.ARCALITY_PROJECT_ID = localConfig.projectId;
154
- }
152
+ if (isRealProjectId(localConfig.projectId) && !process.env.ARCALITY_PROJECT_ID) {
153
+ process.env.ARCALITY_PROJECT_ID = localConfig.projectId;
154
+ }
155
155
  if (localConfig.project?.baseUrl && !process.env.BASE_URL) {
156
156
  process.env.BASE_URL = localConfig.project.baseUrl;
157
157
  }
@@ -6,15 +6,17 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
 
8
8
  const CONFIG_FILENAME = 'arcality.config';
9
-
10
- export function isRealProjectId(projectId) {
11
- const id = String(projectId || '').trim();
12
- if (!id) return false;
13
- if (id === 'undefined' || id === 'null') return false;
14
- if (/^(local|mock)_/i.test(id)) return false;
15
- if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(id)) return false;
16
- return true;
17
- }
9
+ const LEGACY_OUTPUT_DIRS = new Set(['arcality', './arcality', '.\\arcality']);
10
+ const DEFAULT_YAML_OUTPUT_DIR = './.arcality';
11
+
12
+ export function isRealProjectId(projectId) {
13
+ const id = String(projectId || '').trim();
14
+ if (!id) return false;
15
+ if (id === 'undefined' || id === 'null') return false;
16
+ if (/^(local|mock)_/i.test(id)) return false;
17
+ if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(id)) return false;
18
+ return true;
19
+ }
18
20
 
19
21
  /**
20
22
  * Resolves the path to arcality.config in the project root.
@@ -68,55 +70,55 @@ export function saveProjectConfig(config, projectRoot) {
68
70
  * @param {ArcalityConfig} config
69
71
  * @returns {{ valid: boolean, missing: string[] }}
70
72
  */
71
- export function validateConfig(config) {
72
- const missing = [];
73
+ export function validateConfig(config) {
74
+ const missing = [];
73
75
 
74
76
  if (!config) {
75
77
  return { valid: false, missing: ['arcality.config file'] };
76
78
  }
77
79
 
78
- if (!config.apiKey) missing.push('apiKey');
79
- if (!isRealProjectId(config.projectId)) missing.push('projectId');
80
- if (!config.project?.baseUrl) missing.push('project.baseUrl');
81
- if (!config.auth?.username) missing.push('auth.username');
82
- if (!config.auth?.password) missing.push('auth.password');
83
-
84
- return { valid: missing.length === 0, missing };
85
- }
80
+ if (!config.apiKey) missing.push('apiKey');
81
+ if (!isRealProjectId(config.projectId)) missing.push('projectId');
82
+ if (!config.project?.baseUrl) missing.push('project.baseUrl');
83
+ if (!config.auth?.username) missing.push('auth.username');
84
+ if (!config.auth?.password) missing.push('auth.password');
85
+
86
+ return { valid: missing.length === 0, missing };
87
+ }
86
88
 
87
89
  /**
88
90
  * Creates a fresh arcality.config structure.
89
91
  * @param {object} params
90
92
  * @returns {ArcalityConfig}
91
93
  */
92
- export function createConfig({
94
+ export function createConfig({
93
95
  apiKey,
94
96
  organizationId,
95
97
  projectId,
96
- projectName,
97
- baseUrl,
98
- frameworkDetected,
99
- username,
100
- password,
101
- arcalityVersion,
102
- yamlOutputDir = './arcality',
98
+ projectName,
99
+ baseUrl,
100
+ frameworkDetected,
101
+ username,
102
+ password,
103
+ arcalityVersion,
104
+ yamlOutputDir = DEFAULT_YAML_OUTPUT_DIR,
103
105
  }) {
104
- return {
106
+ return {
105
107
  version: '1',
106
108
  apiKey,
107
109
  organizationId: organizationId || null,
108
110
  projectId,
109
111
  project: {
110
112
  name: projectName,
111
- baseUrl,
112
- frameworkDetected: frameworkDetected || null,
113
- },
114
- auth: {
115
- username,
116
- password,
117
- },
118
- runtime: {
119
- yamlOutputDir,
113
+ baseUrl,
114
+ frameworkDetected: frameworkDetected || null,
115
+ },
116
+ auth: {
117
+ username,
118
+ password,
119
+ },
120
+ runtime: {
121
+ yamlOutputDir,
120
122
  reuseSuccessfulYamls: true,
121
123
  singleConfigurationMode: true,
122
124
  },
@@ -131,17 +133,18 @@ export function createConfig({
131
133
  * with existing modules that read from process.env.
132
134
  * @param {ArcalityConfig} config
133
135
  */
134
- export function injectConfigToEnv(config) {
135
- if (!config) return;
136
+ export function injectConfigToEnv(config) {
137
+ if (!config) return;
136
138
 
137
139
  if (config.apiKey) process.env.ARCALITY_API_KEY = config.apiKey;
138
- if (isRealProjectId(config.projectId)) process.env.ARCALITY_PROJECT_ID = config.projectId;
139
- if (config.project?.baseUrl && !process.env.BASE_URL) {
140
- process.env.BASE_URL = config.project.baseUrl;
141
- }
142
- if (config.auth?.username) process.env.LOGIN_USER = config.auth.username;
143
- if (config.auth?.password) process.env.LOGIN_PASSWORD = config.auth.password;
144
- }
140
+ if (config.organizationId) process.env.ARCALITY_ORG_ID = config.organizationId;
141
+ if (isRealProjectId(config.projectId)) process.env.ARCALITY_PROJECT_ID = config.projectId;
142
+ if (config.project?.baseUrl && !process.env.BASE_URL) {
143
+ process.env.BASE_URL = config.project.baseUrl;
144
+ }
145
+ if (config.auth?.username) process.env.LOGIN_USER = config.auth.username;
146
+ if (config.auth?.password) process.env.LOGIN_PASSWORD = config.auth.password;
147
+ }
145
148
 
146
149
  /**
147
150
  * Gets the YAML output directory from config, falling back to default.
@@ -149,10 +152,13 @@ export function injectConfigToEnv(config) {
149
152
  * @param {string} [projectRoot]
150
153
  * @returns {string} absolute path
151
154
  */
152
- export function getYamlOutputDir(config, projectRoot) {
153
- const dir = config?.runtime?.yamlOutputDir || './arcality';
154
- return path.resolve(projectRoot || process.cwd(), dir);
155
- }
155
+ export function getYamlOutputDir(config, projectRoot) {
156
+ const configuredDir = String(config?.runtime?.yamlOutputDir || '').trim();
157
+ const dir = LEGACY_OUTPUT_DIRS.has(configuredDir) || !configuredDir
158
+ ? DEFAULT_YAML_OUTPUT_DIR
159
+ : configuredDir;
160
+ return path.resolve(projectRoot || process.cwd(), dir);
161
+ }
156
162
 
157
163
  /**
158
164
  * Ensures the YAML output directory exists.
@@ -175,7 +181,7 @@ export function ensureYamlOutputDir(config, projectRoot) {
175
181
  * @property {string|null} organizationId
176
182
  * @property {string} projectId
177
183
  * @property {{ name: string, baseUrl: string, frameworkDetected: string|null }} project
178
- * @property {{ username: string, password: string }} auth
184
+ * @property {{ username: string, password: string }} auth
179
185
  * @property {{ yamlOutputDir: string, reuseSuccessfulYamls: boolean, singleConfigurationMode: boolean }} runtime
180
186
  * @property {{ arcalityVersion: string }} meta
181
187
  */
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { discoverProjectRoutes } from './routeDiscovery.mjs';
4
+
5
+ function readJson(filePath) {
6
+ const raw = fs.readFileSync(filePath, 'utf8');
7
+ return JSON.parse(raw);
8
+ }
9
+
10
+ function detectFramework(pkg) {
11
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
12
+ if (deps.next) return 'next';
13
+ if (deps.vite) return 'vite';
14
+ if (deps['react-scripts']) return 'cra';
15
+ return 'unknown';
16
+ }
17
+
18
+ export function analyzeCodebase(projectRoot = process.cwd(), options = {}) {
19
+ const pkgPath = path.join(projectRoot, 'package.json');
20
+ const result = {
21
+ projectRoot,
22
+ packageJsonPath: pkgPath,
23
+ projectName: path.basename(projectRoot),
24
+ framework: 'unknown',
25
+ scripts: [],
26
+ dependencies: [],
27
+ routeCount: 0,
28
+ routes: [],
29
+ warnings: []
30
+ };
31
+
32
+ if (!fs.existsSync(pkgPath)) {
33
+ result.warnings.push('No se encontro package.json en el proyecto destino.');
34
+ return result;
35
+ }
36
+
37
+ try {
38
+ const pkg = readJson(pkgPath);
39
+ result.projectName = pkg.name || result.projectName;
40
+ result.framework = detectFramework(pkg);
41
+ result.scripts = Object.keys(pkg.scripts || {});
42
+ result.dependencies = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) });
43
+ result.routes = discoverProjectRoutes(projectRoot, result.framework, {
44
+ baseUrl: options.baseUrl || ''
45
+ });
46
+ result.routeCount = result.routes.length;
47
+
48
+ if (result.routeCount === 0) {
49
+ result.warnings.push(`No se detectaron rutas automaticamente para el framework "${result.framework}".`);
50
+ }
51
+ if (result.framework === 'unknown') {
52
+ result.warnings.push('Framework no reconocido automaticamente. Se usaron heuristicas de fallback.');
53
+ }
54
+ } catch (error) {
55
+ result.warnings.push(`No se pudo analizar el proyecto: ${error.message}`);
56
+ }
57
+
58
+ return result;
59
+ }
@@ -0,0 +1,76 @@
1
+ function isNonEmptyString(value) {
2
+ return typeof value === 'string' && value.trim().length > 0;
3
+ }
4
+
5
+ function isPriority(value) {
6
+ return value === 'high' || value === 'normal' || value === 'low';
7
+ }
8
+
9
+ export function validateGeneratedMissionSchema(mission) {
10
+ const issues = [];
11
+
12
+ if (!mission || typeof mission !== 'object' || Array.isArray(mission)) {
13
+ return {
14
+ valid: false,
15
+ issues: ['Mission must be a plain object.']
16
+ };
17
+ }
18
+
19
+ if (!isNonEmptyString(mission.name)) issues.push('name is required.');
20
+ if (!isNonEmptyString(mission.prompt)) issues.push('prompt is required.');
21
+ if (!isNonEmptyString(mission.page)) issues.push('page is required.');
22
+ if (!isNonEmptyString(mission.page_path)) issues.push('page_path is required.');
23
+ if (!isNonEmptyString(mission.expected_result)) issues.push('expected_result is required.');
24
+ if (!Array.isArray(mission.tags) || mission.tags.length === 0) issues.push('tags must be a non-empty array.');
25
+ if (!isPriority(mission.priority)) issues.push('priority must be one of: high, normal, low.');
26
+ if (!isNonEmptyString(mission.collection)) issues.push('collection is required.');
27
+ if (!isNonEmptyString(mission.created_at) || Number.isNaN(Date.parse(mission.created_at))) {
28
+ issues.push('created_at must be a valid ISO date string.');
29
+ }
30
+
31
+ if (!mission.generation || typeof mission.generation !== 'object' || Array.isArray(mission.generation)) {
32
+ issues.push('generation metadata is required.');
33
+ } else {
34
+ if (!isNonEmptyString(mission.generation.source)) issues.push('generation.source is required.');
35
+ if (!isNonEmptyString(mission.generation.framework)) issues.push('generation.framework is required.');
36
+ if (!isNonEmptyString(mission.generation.router_kind)) issues.push('generation.router_kind is required.');
37
+ if (!isNonEmptyString(mission.generation.variant)) issues.push('generation.variant is required.');
38
+ if (!isNonEmptyString(mission.generation.route_hash)) issues.push('generation.route_hash is required.');
39
+ if (!isNonEmptyString(mission.generation.prompt_hash)) issues.push('generation.prompt_hash is required.');
40
+ if (!isNonEmptyString(mission.generation.route_example_path)) {
41
+ issues.push('generation.route_example_path is required.');
42
+ }
43
+ if (!Array.isArray(mission.generation.route_params)) {
44
+ issues.push('generation.route_params must be an array.');
45
+ }
46
+
47
+ const status = mission.generation.context_status;
48
+ if (!status || typeof status !== 'object' || Array.isArray(status)) {
49
+ issues.push('generation.context_status is required.');
50
+ } else {
51
+ if (!isNonEmptyString(status.qaContext)) issues.push('generation.context_status.qaContext is required.');
52
+ if (!isNonEmptyString(status.feasibility)) issues.push('generation.context_status.feasibility is required.');
53
+ }
54
+ }
55
+
56
+ if (Array.isArray(mission.tags)) {
57
+ for (const tag of mission.tags) {
58
+ if (!isNonEmptyString(tag)) {
59
+ issues.push('tags must contain only non-empty strings.');
60
+ break;
61
+ }
62
+ }
63
+ }
64
+
65
+ return {
66
+ valid: issues.length === 0,
67
+ issues
68
+ };
69
+ }
70
+
71
+ export function assertGeneratedMissionSchema(mission) {
72
+ const result = validateGeneratedMissionSchema(mission);
73
+ if (!result.valid) {
74
+ throw new Error(`Generated mission schema validation failed: ${result.issues.join(' ')}`);
75
+ }
76
+ }