@arcadialdev/arcality 4.1.0 → 4.1.2

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 +260 -170
  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 +1377 -244
  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
@@ -1,242 +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
- }
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
+ }