@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.
- package/.agents/skills/db-validation-evidence/SKILL.md +43 -0
- package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
- package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
- package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
- package/.agents/skills/form-expert/SKILL.md +98 -0
- package/.agents/skills/investigation-protocol/SKILL.md +56 -0
- package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
- package/.agents/skills/modal-master/SKILL.md +46 -0
- package/.agents/skills/native-control-expert/SKILL.md +74 -0
- package/.agents/skills/qa-context-governance/SKILL.md +23 -0
- package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
- package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
- package/README.md +103 -163
- package/bin/arcality.mjs +25 -25
- package/package.json +75 -75
- package/scripts/edit-config.mjs +843 -0
- package/scripts/gen-and-run.mjs +260 -170
- package/scripts/generate.mjs +236 -236
- package/scripts/init.mjs +47 -47
- package/src/configManager.mjs +13 -13
- package/src/envSetup.ts +229 -205
- package/src/services/codebaseAnalyzer.mjs +59 -59
- package/src/services/databaseValidationService.mjs +598 -0
- package/src/services/executionEvidenceService.mjs +124 -0
- package/src/services/generatedMissionSchema.mjs +76 -76
- package/src/services/generatedMissionStore.mjs +117 -117
- package/src/services/generationContext.mjs +242 -242
- package/src/services/mcpStdioClient.mjs +204 -0
- package/src/services/missionGenerator.mjs +329 -329
- package/src/services/routeDiscovery.mjs +762 -762
- package/tests/_helpers/ArcalityReporter.js +1342 -1255
- package/tests/_helpers/agentic-runner.bundle.spec.js +1377 -244
- package/.agent/skills/form-expert.md +0 -102
- package/.agent/skills/investigation-protocol.md +0 -61
- package/.agent/skills/modal-master.md +0 -41
- package/.agent/skills/native-control-expert.md +0 -82
package/scripts/generate.mjs
CHANGED
|
@@ -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(
|
package/src/configManager.mjs
CHANGED
|
@@ -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.
|