@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,236 +1,236 @@
1
- #!/usr/bin/env node
2
- import path from 'node:path';
3
- import { analyzeCodebase } from '../src/services/codebaseAnalyzer.mjs';
4
- import { loadProjectConfig, getYamlOutputDir } from '../src/configManager.mjs';
5
- import { generateMissionsForRoute } from '../src/services/missionGenerator.mjs';
6
- import { saveGeneratedMissions } from '../src/services/generatedMissionStore.mjs';
7
- import { loadGenerationContext, resolveRouteGuidance } from '../src/services/generationContext.mjs';
8
-
9
- function parseArgs(rawArgs) {
10
- const options = {
11
- count: null,
12
- page: [],
13
- prompt: '',
14
- dryRun: false,
15
- outputDir: null,
16
- debug: false
17
- };
18
-
19
- for (let i = 0; i < rawArgs.length; i++) {
20
- const token = rawArgs[i];
21
- if (token === '--dry-run') {
22
- options.dryRun = true;
23
- continue;
24
- }
25
- if (token === '--debug') {
26
- options.debug = true;
27
- continue;
28
- }
29
- if (token.startsWith('--count=')) {
30
- options.count = Number(token.split('=').slice(1).join('='));
31
- continue;
32
- }
33
- if (token === '--count' && rawArgs[i + 1]) {
34
- options.count = Number(rawArgs[i + 1]);
35
- i += 1;
36
- continue;
37
- }
38
- if (token.startsWith('--page=')) {
39
- options.page = token.split('=').slice(1).join('=').split(',').map(v => v.trim()).filter(Boolean);
40
- continue;
41
- }
42
- if (token === '--page' && rawArgs[i + 1]) {
43
- options.page = rawArgs[i + 1].split(',').map(v => v.trim()).filter(Boolean);
44
- i += 1;
45
- continue;
46
- }
47
- if (token.startsWith('--prompt=')) {
48
- options.prompt = token.split('=').slice(1).join('=').trim();
49
- continue;
50
- }
51
- if (token === '--prompt' && rawArgs[i + 1]) {
52
- options.prompt = rawArgs[i + 1].trim();
53
- i += 1;
54
- continue;
55
- }
56
- if (token.startsWith('--output-dir=')) {
57
- options.outputDir = token.split('=').slice(1).join('=').trim();
58
- continue;
59
- }
60
- if (token === '--output-dir' && rawArgs[i + 1]) {
61
- options.outputDir = rawArgs[i + 1].trim();
62
- i += 1;
63
- continue;
64
- }
65
- }
66
-
67
- return options;
68
- }
69
-
70
- function matchesPageFilters(routeEntry, filters) {
71
- if (!filters || filters.length === 0) return true;
72
- const haystacks = [
73
- routeEntry.pagePath.toLowerCase(),
74
- routeEntry.route.toLowerCase()
75
- ];
76
- return filters.some(filter => {
77
- const normalized = filter.toLowerCase();
78
- return haystacks.some(value => value === normalized || value.includes(normalized));
79
- });
80
- }
81
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
82
-
83
- function isLikelyUuidFilter(filter) {
84
- return UUID_RE.test(String(filter || '').trim());
85
- }
86
-
87
- function formatRouteExamples(routes, maxItems = 3) {
88
- return routes
89
- .slice(0, maxItems)
90
- .map(entry => `${entry.route} -> ${entry.pagePath}`)
91
- .join('\n- ');
92
- }
93
-
94
-
95
- function limitEntries(entries, count) {
96
- if (!Number.isFinite(count) || count <= 0) return entries;
97
- return entries.slice(0, Math.floor(count));
98
- }
99
-
100
- function limitMissions(missions, count) {
101
- if (!Number.isFinite(count) || count <= 0) return missions;
102
- return missions.slice(0, Math.floor(count));
103
- }
104
-
105
- function printSummary(analysis, selectedRoutes, options, generationContext) {
106
- console.log('\nArcality Generate - Discovery Summary');
107
- console.log(`Project: ${analysis.projectName}`);
108
- console.log(`Framework: ${analysis.framework}`);
109
- console.log(`Routes detected: ${analysis.routeCount}`);
110
- console.log(`Routes selected: ${selectedRoutes.length}`);
111
- console.log(`Mission cap: ${Number.isFinite(options.count) && options.count > 0 ? Math.floor(options.count) : 'auto'}`);
112
- console.log(`Output target: ${options.outputTarget}`);
113
- console.log(`Mode: ${options.dryRun ? 'dry-run' : 'write-yaml'}`);
114
- console.log(`QA context: ${generationContext.qaContext.exists ? generationContext.qaContext.status : 'missing'}`);
115
- console.log(`Feasibility guidance: ${generationContext.feasibility.exists ? 'loaded' : 'missing'}`);
116
-
117
- if (options.prompt) {
118
- console.log(`Prompt focus: ${options.prompt}`);
119
- }
120
-
121
- if (analysis.warnings.length > 0) {
122
- console.log('\nWarnings:');
123
- for (const warning of analysis.warnings) {
124
- console.log(`- ${warning}`);
125
- }
126
- }
127
-
128
- if (selectedRoutes.length === 0) {
129
- console.log('\nNo candidate routes matched the provided filters.');
130
- const uuidFilters = (options.page || []).filter(isLikelyUuidFilter);
131
- if (uuidFilters.length > 0) {
132
- console.log('Tip: the current --page filter only matches route fragments or page file paths, not backend page IDs or UUIDs.');
133
- }
134
- if ((options.page || []).length > 0 && analysis.routes.length > 0) {
135
- console.log('Try values such as a route (`/checkout`) or a page path fragment (`app/checkout/page.tsx`).');
136
- console.log(`Examples:\n- ${formatRouteExamples(analysis.routes)}`);
137
- }
138
- return;
139
- }
140
-
141
- console.log('\nCandidate routes:');
142
- selectedRoutes.forEach((entry, index) => {
143
- const guidance = resolveRouteGuidance(entry, generationContext);
144
- console.log(`${index + 1}. ${entry.route} -> ${entry.pagePath} [${entry.routerKind}]`);
145
- if (options.debug) {
146
- console.log(` name: ${entry.name}`);
147
- console.log(` tags: ${entry.tags.join(', ')}`);
148
- console.log(` signals: ${Object.entries(entry.signals || {}).filter(([, value]) => value === true).map(([key]) => key).join(', ') || 'none'}`);
149
- console.log(` route_params: ${(entry.routeParams || []).map(param => `${param.name}:${param.kind}`).join(', ') || 'none'}`);
150
- console.log(` route_example: ${entry.routeExamplePath || entry.route}`);
151
- console.log(` matched_rules: ${guidance.matchedRules.map(rule => rule.text).join(' | ') || 'none'}`);
152
- console.log(` feasibility_hints: ${guidance.feasibilityHints.join(' | ') || 'none'}`);
153
- console.log(` expected_result: ${entry.expectedResult}`);
154
- }
155
- });
156
- }
157
-
158
- function printPhaseNotice(options) {
159
- if (options.dryRun) {
160
- console.log('\nNo files were written because --dry-run is active.');
161
- return;
162
- }
163
- }
164
-
165
- function printWriteSummary(summary, options) {
166
- console.log('\nGeneration result:');
167
- console.log(`- Requested missions: ${summary.total_requested}`);
168
- console.log(`- Created: ${summary.created_count}`);
169
- console.log(`- Skipped: ${summary.skipped_count}`);
170
-
171
- if (summary.created.length > 0) {
172
- console.log('\nCreated files:');
173
- for (const item of summary.created) {
174
- console.log(`- ${item.filePath}`);
175
- }
176
- }
177
-
178
- if (summary.skipped.length > 0 && options.debug) {
179
- console.log('\nSkipped entries:');
180
- for (const item of summary.skipped) {
181
- console.log(`- ${item.name} (${item.page_path}) -> ${item.reason}`);
182
- }
183
- }
184
- }
185
-
186
- function resolveOutputTarget(projectRoot, explicitOutputDir) {
187
- if (explicitOutputDir) {
188
- return path.resolve(projectRoot, explicitOutputDir);
189
- }
190
-
191
- const config = loadProjectConfig(projectRoot);
192
- if (config) {
193
- return path.join(getYamlOutputDir(config, projectRoot), 'generated');
194
- }
195
-
196
- return path.join(projectRoot, '.arcality', 'generated');
197
- }
198
-
199
- async function main() {
200
- const args = process.argv.slice(2);
201
- const options = parseArgs(args);
202
- const projectRoot = process.cwd();
203
- options.outputTarget = resolveOutputTarget(projectRoot, options.outputDir);
204
- const projectConfig = loadProjectConfig(projectRoot);
205
-
206
- const analysis = analyzeCodebase(projectRoot, {
207
- baseUrl: projectConfig?.project?.baseUrl || ''
208
- });
209
- const generationContext = loadGenerationContext(projectRoot);
210
- const filtered = analysis.routes.filter(route => matchesPageFilters(route, options.page));
211
- const selectedRoutes = limitEntries(filtered, options.page.length > 0 ? null : Math.max(filtered.length, 1));
212
-
213
- printSummary(analysis, selectedRoutes, options, generationContext);
214
-
215
- const missions = limitMissions(
216
- selectedRoutes.flatMap(route => generateMissionsForRoute(route, {
217
- ...options,
218
- routeGuidance: resolveRouteGuidance(route, generationContext)
219
- })),
220
- options.count
221
- );
222
- const summary = saveGeneratedMissions({
223
- projectRoot,
224
- targetDir: options.outputTarget,
225
- missions,
226
- dryRun: options.dryRun
227
- });
228
-
229
- printWriteSummary(summary, options);
230
- printPhaseNotice(options);
231
- }
232
-
233
- main().catch(err => {
234
- console.error(`\n[generate] ${err.message}`);
235
- process.exit(1);
236
- });
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { analyzeCodebase } from '../src/services/codebaseAnalyzer.mjs';
4
+ import { loadProjectConfig, getYamlOutputDir } from '../src/configManager.mjs';
5
+ import { generateMissionsForRoute } from '../src/services/missionGenerator.mjs';
6
+ import { saveGeneratedMissions } from '../src/services/generatedMissionStore.mjs';
7
+ import { loadGenerationContext, resolveRouteGuidance } from '../src/services/generationContext.mjs';
8
+
9
+ function parseArgs(rawArgs) {
10
+ const options = {
11
+ count: null,
12
+ page: [],
13
+ prompt: '',
14
+ dryRun: false,
15
+ outputDir: null,
16
+ debug: false
17
+ };
18
+
19
+ for (let i = 0; i < rawArgs.length; i++) {
20
+ const token = rawArgs[i];
21
+ if (token === '--dry-run') {
22
+ options.dryRun = true;
23
+ continue;
24
+ }
25
+ if (token === '--debug') {
26
+ options.debug = true;
27
+ continue;
28
+ }
29
+ if (token.startsWith('--count=')) {
30
+ options.count = Number(token.split('=').slice(1).join('='));
31
+ continue;
32
+ }
33
+ if (token === '--count' && rawArgs[i + 1]) {
34
+ options.count = Number(rawArgs[i + 1]);
35
+ i += 1;
36
+ continue;
37
+ }
38
+ if (token.startsWith('--page=')) {
39
+ options.page = token.split('=').slice(1).join('=').split(',').map(v => v.trim()).filter(Boolean);
40
+ continue;
41
+ }
42
+ if (token === '--page' && rawArgs[i + 1]) {
43
+ options.page = rawArgs[i + 1].split(',').map(v => v.trim()).filter(Boolean);
44
+ i += 1;
45
+ continue;
46
+ }
47
+ if (token.startsWith('--prompt=')) {
48
+ options.prompt = token.split('=').slice(1).join('=').trim();
49
+ continue;
50
+ }
51
+ if (token === '--prompt' && rawArgs[i + 1]) {
52
+ options.prompt = rawArgs[i + 1].trim();
53
+ i += 1;
54
+ continue;
55
+ }
56
+ if (token.startsWith('--output-dir=')) {
57
+ options.outputDir = token.split('=').slice(1).join('=').trim();
58
+ continue;
59
+ }
60
+ if (token === '--output-dir' && rawArgs[i + 1]) {
61
+ options.outputDir = rawArgs[i + 1].trim();
62
+ i += 1;
63
+ continue;
64
+ }
65
+ }
66
+
67
+ return options;
68
+ }
69
+
70
+ function matchesPageFilters(routeEntry, filters) {
71
+ if (!filters || filters.length === 0) return true;
72
+ const haystacks = [
73
+ routeEntry.pagePath.toLowerCase(),
74
+ routeEntry.route.toLowerCase()
75
+ ];
76
+ return filters.some(filter => {
77
+ const normalized = filter.toLowerCase();
78
+ return haystacks.some(value => value === normalized || value.includes(normalized));
79
+ });
80
+ }
81
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
82
+
83
+ function isLikelyUuidFilter(filter) {
84
+ return UUID_RE.test(String(filter || '').trim());
85
+ }
86
+
87
+ function formatRouteExamples(routes, maxItems = 3) {
88
+ return routes
89
+ .slice(0, maxItems)
90
+ .map(entry => `${entry.route} -> ${entry.pagePath}`)
91
+ .join('\n- ');
92
+ }
93
+
94
+
95
+ function limitEntries(entries, count) {
96
+ if (!Number.isFinite(count) || count <= 0) return entries;
97
+ return entries.slice(0, Math.floor(count));
98
+ }
99
+
100
+ function limitMissions(missions, count) {
101
+ if (!Number.isFinite(count) || count <= 0) return missions;
102
+ return missions.slice(0, Math.floor(count));
103
+ }
104
+
105
+ function printSummary(analysis, selectedRoutes, options, generationContext) {
106
+ console.log('\nArcality Generate - Discovery Summary');
107
+ console.log(`Project: ${analysis.projectName}`);
108
+ console.log(`Framework: ${analysis.framework}`);
109
+ console.log(`Routes detected: ${analysis.routeCount}`);
110
+ console.log(`Routes selected: ${selectedRoutes.length}`);
111
+ console.log(`Mission cap: ${Number.isFinite(options.count) && options.count > 0 ? Math.floor(options.count) : 'auto'}`);
112
+ console.log(`Output target: ${options.outputTarget}`);
113
+ console.log(`Mode: ${options.dryRun ? 'dry-run' : 'write-yaml'}`);
114
+ console.log(`QA context: ${generationContext.qaContext.exists ? generationContext.qaContext.status : 'missing'}`);
115
+ console.log(`Feasibility guidance: ${generationContext.feasibility.exists ? 'loaded' : 'missing'}`);
116
+
117
+ if (options.prompt) {
118
+ console.log(`Prompt focus: ${options.prompt}`);
119
+ }
120
+
121
+ if (analysis.warnings.length > 0) {
122
+ console.log('\nWarnings:');
123
+ for (const warning of analysis.warnings) {
124
+ console.log(`- ${warning}`);
125
+ }
126
+ }
127
+
128
+ if (selectedRoutes.length === 0) {
129
+ console.log('\nNo candidate routes matched the provided filters.');
130
+ const uuidFilters = (options.page || []).filter(isLikelyUuidFilter);
131
+ if (uuidFilters.length > 0) {
132
+ console.log('Tip: the current --page filter only matches route fragments or page file paths, not backend page IDs or UUIDs.');
133
+ }
134
+ if ((options.page || []).length > 0 && analysis.routes.length > 0) {
135
+ console.log('Try values such as a route (`/checkout`) or a page path fragment (`app/checkout/page.tsx`).');
136
+ console.log(`Examples:\n- ${formatRouteExamples(analysis.routes)}`);
137
+ }
138
+ return;
139
+ }
140
+
141
+ console.log('\nCandidate routes:');
142
+ selectedRoutes.forEach((entry, index) => {
143
+ const guidance = resolveRouteGuidance(entry, generationContext);
144
+ console.log(`${index + 1}. ${entry.route} -> ${entry.pagePath} [${entry.routerKind}]`);
145
+ if (options.debug) {
146
+ console.log(` name: ${entry.name}`);
147
+ console.log(` tags: ${entry.tags.join(', ')}`);
148
+ console.log(` signals: ${Object.entries(entry.signals || {}).filter(([, value]) => value === true).map(([key]) => key).join(', ') || 'none'}`);
149
+ console.log(` route_params: ${(entry.routeParams || []).map(param => `${param.name}:${param.kind}`).join(', ') || 'none'}`);
150
+ console.log(` route_example: ${entry.routeExamplePath || entry.route}`);
151
+ console.log(` matched_rules: ${guidance.matchedRules.map(rule => rule.text).join(' | ') || 'none'}`);
152
+ console.log(` feasibility_hints: ${guidance.feasibilityHints.join(' | ') || 'none'}`);
153
+ console.log(` expected_result: ${entry.expectedResult}`);
154
+ }
155
+ });
156
+ }
157
+
158
+ function printPhaseNotice(options) {
159
+ if (options.dryRun) {
160
+ console.log('\nNo files were written because --dry-run is active.');
161
+ return;
162
+ }
163
+ }
164
+
165
+ function printWriteSummary(summary, options) {
166
+ console.log('\nGeneration result:');
167
+ console.log(`- Requested missions: ${summary.total_requested}`);
168
+ console.log(`- Created: ${summary.created_count}`);
169
+ console.log(`- Skipped: ${summary.skipped_count}`);
170
+
171
+ if (summary.created.length > 0) {
172
+ console.log('\nCreated files:');
173
+ for (const item of summary.created) {
174
+ console.log(`- ${item.filePath}`);
175
+ }
176
+ }
177
+
178
+ if (summary.skipped.length > 0 && options.debug) {
179
+ console.log('\nSkipped entries:');
180
+ for (const item of summary.skipped) {
181
+ console.log(`- ${item.name} (${item.page_path}) -> ${item.reason}`);
182
+ }
183
+ }
184
+ }
185
+
186
+ function resolveOutputTarget(projectRoot, explicitOutputDir) {
187
+ if (explicitOutputDir) {
188
+ return path.resolve(projectRoot, explicitOutputDir);
189
+ }
190
+
191
+ const config = loadProjectConfig(projectRoot);
192
+ if (config) {
193
+ return path.join(getYamlOutputDir(config, projectRoot), 'generated');
194
+ }
195
+
196
+ return path.join(projectRoot, '.arcality', 'generated');
197
+ }
198
+
199
+ async function main() {
200
+ const args = process.argv.slice(2);
201
+ const options = parseArgs(args);
202
+ const projectRoot = process.cwd();
203
+ options.outputTarget = resolveOutputTarget(projectRoot, options.outputDir);
204
+ const projectConfig = loadProjectConfig(projectRoot);
205
+
206
+ const analysis = analyzeCodebase(projectRoot, {
207
+ baseUrl: projectConfig?.project?.baseUrl || ''
208
+ });
209
+ const generationContext = loadGenerationContext(projectRoot);
210
+ const filtered = analysis.routes.filter(route => matchesPageFilters(route, options.page));
211
+ const selectedRoutes = limitEntries(filtered, options.page.length > 0 ? null : Math.max(filtered.length, 1));
212
+
213
+ printSummary(analysis, selectedRoutes, options, generationContext);
214
+
215
+ const missions = limitMissions(
216
+ selectedRoutes.flatMap(route => generateMissionsForRoute(route, {
217
+ ...options,
218
+ routeGuidance: resolveRouteGuidance(route, generationContext)
219
+ })),
220
+ options.count
221
+ );
222
+ const summary = saveGeneratedMissions({
223
+ projectRoot,
224
+ targetDir: options.outputTarget,
225
+ missions,
226
+ dryRun: options.dryRun
227
+ });
228
+
229
+ printWriteSummary(summary, options);
230
+ printPhaseNotice(options);
231
+ }
232
+
233
+ main().catch(err => {
234
+ console.error(`\n[generate] ${err.message}`);
235
+ process.exit(1);
236
+ });
package/scripts/init.mjs CHANGED
@@ -339,12 +339,12 @@ async function main() {
339
339
  }
340
340
 
341
341
  // ── Step 7: Custom QA Context ──
342
- note(
343
- chalk.white('El archivo ') + chalk.cyan('.arcality/qa-context.md') + chalk.white(' se inyecta al prompt del agente antes de cada misión.') + '\n' +
344
- chalk.white('Úsalo para reglas del negocio, navegación especial y anti-patrones que Arcality deba respetar siempre.') + '\n' +
345
- chalk.white('Complementa el archivo con ') + chalk.cyan('.arcality/qa-context.meta.json') + chalk.white(' para versionar responsables y cambios.'),
346
- 'QA Context Local'
347
- );
342
+ note(
343
+ chalk.white('El archivo ') + chalk.cyan('.arcality/qa-context.md') + chalk.white(' se inyecta al prompt del agente antes de cada misión.') + '\n' +
344
+ chalk.white('Úsalo para reglas del negocio, navegación especial y anti-patrones que Arcality deba respetar siempre.') + '\n' +
345
+ chalk.white('Complementa el archivo con ') + chalk.cyan('.arcality/qa-context.meta.json') + chalk.white(' para versionar responsables y cambios.'),
346
+ 'QA Context Local'
347
+ );
348
348
 
349
349
  const wantsContext = await confirm({
350
350
  message: chalk.cyan('📄 ¿Deseas generar un archivo local QA Context (.arcality/qa-context.md) para enseñarle al agente reglas del negocio?'),
@@ -356,12 +356,12 @@ async function main() {
356
356
  fs.mkdirSync(arcalityDir, { recursive: true });
357
357
  }
358
358
 
359
- const qaContextPath = path.join(arcalityDir, 'qa-context.md');
360
- const qaContextMetaPath = path.join(arcalityDir, 'qa-context.meta.json');
361
- const qaTemplate = `# Arcality QA Context
362
- <!--
363
- Este archivo se inyecta antes de cada mision.
364
- Escribe reglas cortas, especificas y accionables.
359
+ const qaContextPath = path.join(arcalityDir, 'qa-context.md');
360
+ const qaContextMetaPath = path.join(arcalityDir, 'qa-context.meta.json');
361
+ const qaTemplate = `# Arcality QA Context
362
+ <!--
363
+ Este archivo se inyecta antes de cada mision.
364
+ Escribe reglas cortas, especificas y accionables.
365
365
  Usa un bullet por regla real.
366
366
  Los bullets que empiecen con "Ejemplo:" se ignoran hasta que los reemplaces.
367
367
  -->
@@ -386,41 +386,41 @@ Los bullets que empiecen con "Ejemplo:" se ignoran hasta que los reemplaces.
386
386
  - Ejemplo: Usa el usuario QA_TIMESHEET_01 para pruebas de captura diaria.
387
387
  - Ejemplo: No reutilices folios ya aprobados.
388
388
 
389
- ## Resultado Esperado y Validaciones Clave
390
- <!-- Como saber que la mision realmente termino bien -->
391
- - Ejemplo: La prueba termina cuando el registro aparece en la tabla "Timesheet - Hoy" de /welcome.
392
- - Ejemplo: Si aparece la leyenda "Expirado" en el proyecto, la prueba sigue siendo valida.
393
- `;
394
- const qaMetaTemplate = {
395
- version: '1.0.0',
396
- updated_by: username.trim() || 'qa.owner@empresa.com',
397
- approved_by: '',
398
- owner_team: 'QA',
399
- effective_from: new Date().toISOString().slice(0, 10),
400
- change_summary: 'Creacion inicial del QA Context para este proyecto.',
401
- tags: ['business-rules', 'ui-navigation']
402
- };
403
- if (!fs.existsSync(qaContextPath)) {
404
- fs.writeFileSync(qaContextPath, qaTemplate);
405
- note(chalk.green(`✅ QA Context creado en: ${qaContextPath}`), 'Contexto Guardado');
406
- } else {
407
- note(chalk.cyan(`ℹ️ QA Context ya existente: ${qaContextPath}`), 'Contexto Detectado');
408
- }
409
- if (!fs.existsSync(qaContextMetaPath)) {
410
- fs.writeFileSync(qaContextMetaPath, JSON.stringify(qaMetaTemplate, null, 2), 'utf8');
411
- note(chalk.green(`✅ QA Context metadata creada en: ${qaContextMetaPath}`), 'Metadata Guardada');
412
- } else {
413
- note(chalk.cyan(`ℹ️ QA Context metadata ya existente: ${qaContextMetaPath}`), 'Metadata Detectada');
414
- }
415
-
416
- note(
417
- chalk.white('Si ejecutas Arcality en CI, la gobernanza del contexto se aplica en modo ') +
418
- chalk.cyan('strict') + chalk.white(' por defecto.') + '\n' +
419
- chalk.white('Puedes controlarlo con la variable ') + chalk.cyan('ARCALITY_QA_CONTEXT_GOVERNANCE') +
420
- chalk.white(' usando ') + chalk.cyan('off') + chalk.white(', ') + chalk.cyan('warn') + chalk.white(' o ') + chalk.cyan('strict') + chalk.white('.'),
421
- 'Gobernanza del Contexto'
422
- );
423
- }
389
+ ## Resultado Esperado y Validaciones Clave
390
+ <!-- Como saber que la mision realmente termino bien -->
391
+ - Ejemplo: La prueba termina cuando el registro aparece en la tabla "Timesheet - Hoy" de /welcome.
392
+ - Ejemplo: Si aparece la leyenda "Expirado" en el proyecto, la prueba sigue siendo valida.
393
+ `;
394
+ const qaMetaTemplate = {
395
+ version: '1.0.0',
396
+ updated_by: username.trim() || 'qa.owner@empresa.com',
397
+ approved_by: '',
398
+ owner_team: 'QA',
399
+ effective_from: new Date().toISOString().slice(0, 10),
400
+ change_summary: 'Creacion inicial del QA Context para este proyecto.',
401
+ tags: ['business-rules', 'ui-navigation']
402
+ };
403
+ if (!fs.existsSync(qaContextPath)) {
404
+ fs.writeFileSync(qaContextPath, qaTemplate);
405
+ note(chalk.green(`✅ QA Context creado en: ${qaContextPath}`), 'Contexto Guardado');
406
+ } else {
407
+ note(chalk.cyan(`ℹ️ QA Context ya existente: ${qaContextPath}`), 'Contexto Detectado');
408
+ }
409
+ if (!fs.existsSync(qaContextMetaPath)) {
410
+ fs.writeFileSync(qaContextMetaPath, JSON.stringify(qaMetaTemplate, null, 2), 'utf8');
411
+ note(chalk.green(`✅ QA Context metadata creada en: ${qaContextMetaPath}`), 'Metadata Guardada');
412
+ } else {
413
+ note(chalk.cyan(`ℹ️ QA Context metadata ya existente: ${qaContextMetaPath}`), 'Metadata Detectada');
414
+ }
415
+
416
+ note(
417
+ chalk.white('Si ejecutas Arcality en CI, la gobernanza del contexto se aplica en modo ') +
418
+ chalk.cyan('strict') + chalk.white(' por defecto.') + '\n' +
419
+ chalk.white('Puedes controlarlo con la variable ') + chalk.cyan('ARCALITY_QA_CONTEXT_GOVERNANCE') +
420
+ chalk.white(' usando ') + chalk.cyan('off') + chalk.white(', ') + chalk.cyan('warn') + chalk.white(' o ') + chalk.cyan('strict') + chalk.white('.'),
421
+ 'Gobernanza del Contexto'
422
+ );
423
+ }
424
424
 
425
425
  // ── Summary ──
426
426
  note(
@@ -5,9 +5,9 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
 
8
- const CONFIG_FILENAME = 'arcality.config';
9
- const LEGACY_OUTPUT_DIRS = new Set(['arcality', './arcality', '.\\arcality']);
10
- const DEFAULT_YAML_OUTPUT_DIR = './.arcality';
8
+ const CONFIG_FILENAME = 'arcality.config';
9
+ const LEGACY_OUTPUT_DIRS = new Set(['arcality', './arcality', '.\\arcality']);
10
+ const DEFAULT_YAML_OUTPUT_DIR = './.arcality';
11
11
 
12
12
  export function isRealProjectId(projectId) {
13
13
  const id = String(projectId || '').trim();
@@ -91,7 +91,7 @@ export function validateConfig(config) {
91
91
  * @param {object} params
92
92
  * @returns {ArcalityConfig}
93
93
  */
94
- export function createConfig({
94
+ export function createConfig({
95
95
  apiKey,
96
96
  organizationId,
97
97
  projectId,
@@ -101,8 +101,8 @@ export function createConfig({
101
101
  username,
102
102
  password,
103
103
  arcalityVersion,
104
- yamlOutputDir = DEFAULT_YAML_OUTPUT_DIR,
105
- }) {
104
+ yamlOutputDir = DEFAULT_YAML_OUTPUT_DIR,
105
+ }) {
106
106
  return {
107
107
  version: '1',
108
108
  apiKey,
@@ -152,13 +152,13 @@ export function injectConfigToEnv(config) {
152
152
  * @param {string} [projectRoot]
153
153
  * @returns {string} absolute path
154
154
  */
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
- }
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
+ }
162
162
 
163
163
  /**
164
164
  * Ensures the YAML output directory exists.