@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.
Files changed (36) hide show
  1. package/.agents/skills/db-validation-evidence/SKILL.md +43 -0
  2. package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
  3. package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
  4. package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
  5. package/.agents/skills/form-expert/SKILL.md +98 -0
  6. package/.agents/skills/investigation-protocol/SKILL.md +56 -0
  7. package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
  8. package/.agents/skills/modal-master/SKILL.md +46 -0
  9. package/.agents/skills/native-control-expert/SKILL.md +74 -0
  10. package/.agents/skills/qa-context-governance/SKILL.md +23 -0
  11. package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
  12. package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
  13. package/README.md +103 -163
  14. package/bin/arcality.mjs +25 -25
  15. package/package.json +75 -75
  16. package/scripts/edit-config.mjs +843 -0
  17. package/scripts/gen-and-run.mjs +2705 -2609
  18. package/scripts/generate.mjs +236 -236
  19. package/scripts/init.mjs +47 -47
  20. package/src/configManager.mjs +13 -13
  21. package/src/envSetup.ts +229 -205
  22. package/src/services/codebaseAnalyzer.mjs +59 -59
  23. package/src/services/databaseValidationService.mjs +598 -0
  24. package/src/services/executionEvidenceService.mjs +124 -0
  25. package/src/services/generatedMissionSchema.mjs +76 -76
  26. package/src/services/generatedMissionStore.mjs +117 -117
  27. package/src/services/generationContext.mjs +242 -242
  28. package/src/services/mcpStdioClient.mjs +204 -0
  29. package/src/services/missionGenerator.mjs +329 -329
  30. package/src/services/routeDiscovery.mjs +762 -762
  31. package/tests/_helpers/ArcalityReporter.js +1342 -1255
  32. package/tests/_helpers/agentic-runner.bundle.spec.js +1363 -245
  33. package/.agent/skills/form-expert.md +0 -102
  34. package/.agent/skills/investigation-protocol.md +0 -61
  35. package/.agent/skills/modal-master.md +0 -41
  36. package/.agent/skills/native-control-expert.md +0 -82
@@ -0,0 +1,124 @@
1
+ function normalizeStatus(value) {
2
+ const normalized = String(value || '').trim().toLowerCase();
3
+ if (normalized === 'passed' || normalized === 'pass' || normalized === 'success') return 'passed';
4
+ if (normalized === 'failed' || normalized === 'fail' || normalized === 'error') return 'failed';
5
+ return 'unknown';
6
+ }
7
+
8
+ function classifyLogs(logs = []) {
9
+ const entries = Array.isArray(logs) ? logs.map(entry => String(entry || '').trim()).filter(Boolean) : [];
10
+ const errors = entries.filter(entry => entry.includes('[ERROR]') || entry.includes('[NETWORK_ERROR]'));
11
+ const warnings = entries.filter(entry => entry.includes('[WARNING]'));
12
+ return {
13
+ total: entries.length,
14
+ error_count: errors.length,
15
+ warning_count: warnings.length,
16
+ recent: entries.slice(-10)
17
+ };
18
+ }
19
+
20
+ function summarizeAttachments(attachments = []) {
21
+ const normalized = Array.isArray(attachments)
22
+ ? attachments.map(att => ({
23
+ name: String(att?.name || '').trim(),
24
+ contentType: String(att?.contentType || '').trim(),
25
+ path: att?.path ? String(att.path) : undefined
26
+ })).filter(att => att.name || att.contentType || att.path)
27
+ : [];
28
+
29
+ return {
30
+ total: normalized.length,
31
+ images: normalized.filter(att => att.contentType.startsWith('image/')).length,
32
+ videos: normalized.filter(att => att.contentType.startsWith('video/')).length,
33
+ json: normalized.filter(att => att.contentType === 'application/json').length,
34
+ names: normalized.map(att => att.name || att.path || 'attachment').slice(0, 12)
35
+ };
36
+ }
37
+
38
+ function deriveDatabaseStatus(input) {
39
+ const explicit = normalizeStatus(input?.database_status);
40
+ if (explicit !== 'unknown') return explicit;
41
+ if (typeof input?.database_report?.passed === 'boolean') {
42
+ return input.database_report.passed ? 'passed' : 'failed';
43
+ }
44
+ return 'unknown';
45
+ }
46
+
47
+ function buildContradictions(uiStatus, dbStatus) {
48
+ const contradictions = [];
49
+ if (uiStatus === 'passed' && dbStatus === 'failed') contradictions.push('ui_passed_but_db_failed');
50
+ if (uiStatus === 'failed' && dbStatus === 'passed') contradictions.push('ui_failed_but_db_passed');
51
+ return contradictions;
52
+ }
53
+
54
+ function buildRemediationHints(finalStatus, contradictions, consoleSummary) {
55
+ const hints = [];
56
+ if (contradictions.includes('ui_passed_but_db_failed')) {
57
+ hints.push('Revisar la persistencia backend o la confirmacion visual de guardado.');
58
+ }
59
+ if (contradictions.includes('ui_failed_but_db_passed')) {
60
+ hints.push('Revisar si la UI muestra un error falso o si el flujo se completo parcialmente.');
61
+ }
62
+ if (consoleSummary.error_count > 0) {
63
+ hints.push('Inspeccionar errores de consola o red asociados al momento de la falla.');
64
+ }
65
+ if (finalStatus === 'unknown') {
66
+ hints.push('Completar evidencia faltante antes de cerrar la conclusion.');
67
+ }
68
+ return hints;
69
+ }
70
+
71
+ export function summarizeExecutionEvidence(input = {}, context = {}) {
72
+ const uiStatus = normalizeStatus(input.ui_status);
73
+ const dbStatus = deriveDatabaseStatus(input);
74
+ const consoleSummary = classifyLogs(context.logs || input.console_logs);
75
+ const attachmentSummary = summarizeAttachments(context.attachments || input.attachments);
76
+ const contradictions = buildContradictions(uiStatus, dbStatus);
77
+
78
+ let finalStatus = 'unknown';
79
+ if (contradictions.length > 0) {
80
+ finalStatus = 'contradiction';
81
+ } else if (uiStatus === 'failed' || dbStatus === 'failed') {
82
+ finalStatus = 'failed';
83
+ } else if (uiStatus === 'passed' || dbStatus === 'passed') {
84
+ finalStatus = 'passed';
85
+ }
86
+
87
+ const remediationHints = buildRemediationHints(finalStatus, contradictions, consoleSummary);
88
+ const missionName = String(input.mission_name || input.name || 'execution_evidence').trim();
89
+ const expectedOutcome = String(input.expected_business_outcome || '').trim();
90
+ const uiSummary = String(input.ui_summary || '').trim();
91
+ const validationName = String(input.database_validation_name || input.database_report?.name || '').trim();
92
+ const queryId = String(input.query_id || input.database_report?.query_id || '').trim();
93
+
94
+ const summary = finalStatus === 'contradiction'
95
+ ? 'La evidencia es contradictoria entre UI y base de datos.'
96
+ : finalStatus === 'failed'
97
+ ? 'La ejecucion contiene evidencia suficiente para marcar falla.'
98
+ : finalStatus === 'passed'
99
+ ? 'La evidencia disponible respalda un resultado exitoso.'
100
+ : 'La ejecucion requiere mas evidencia para una conclusion firme.';
101
+
102
+ return {
103
+ type: 'execution_evidence_summary',
104
+ mission_name: missionName,
105
+ final_status: finalStatus,
106
+ summary,
107
+ expected_business_outcome: expectedOutcome || undefined,
108
+ current_url: context.current_url || input.current_url || undefined,
109
+ ui: {
110
+ status: uiStatus,
111
+ summary: uiSummary || undefined
112
+ },
113
+ database: {
114
+ status: dbStatus,
115
+ validation_name: validationName || undefined,
116
+ query_id: queryId || undefined
117
+ },
118
+ console: consoleSummary,
119
+ attachments: attachmentSummary,
120
+ contradictions,
121
+ remediation_hints: remediationHints,
122
+ generated_at: new Date().toISOString()
123
+ };
124
+ }
@@ -1,76 +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
- }
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
+ }
@@ -1,117 +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
- }
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
+ }