@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.
- package/README.md +75 -6
- package/bin/arcality.mjs +26 -17
- package/package.json +2 -1
- package/playwright.config.js +22 -21
- package/scripts/gen-and-run.mjs +2610 -2591
- package/scripts/generate.mjs +215 -0
- package/scripts/init.mjs +278 -253
- package/scripts/rebrand-report.mjs +19 -18
- package/src/KnowledgeService.ts +29 -29
- package/src/arcalityClient.mjs +31 -18
- package/src/configLoader.mjs +35 -35
- package/src/configManager.mjs +57 -51
- package/src/services/codebaseAnalyzer.mjs +59 -0
- package/src/services/generatedMissionSchema.mjs +76 -0
- package/src/services/generatedMissionStore.mjs +117 -0
- package/src/services/generationContext.mjs +242 -0
- package/src/services/missionGenerator.mjs +328 -0
- package/src/services/routeDiscovery.mjs +747 -0
- package/src/testRunner.ts +2 -1
- package/tests/_helpers/ArcalityReporter.js +15 -0
- package/tests/_helpers/agentic-runner.bundle.spec.js +1053 -151
|
@@ -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 || '
|
|
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
|
package/src/KnowledgeService.ts
CHANGED
|
@@ -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
|
|
package/src/arcalityClient.mjs
CHANGED
|
@@ -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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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) {
|
package/src/configLoader.mjs
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
}
|
package/src/configManager.mjs
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
if (
|
|
16
|
-
return
|
|
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 =
|
|
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 (
|
|
139
|
-
if (config.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (config.auth?.
|
|
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
|
|
154
|
-
|
|
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
|
+
}
|