qa-flowkit 0.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.qa-ai/adapters/aider/.aider/README.md +25 -0
- package/.qa-ai/adapters/aider/.aider.conf.yml +6 -0
- package/.qa-ai/adapters/claude/agents/qa-workflow-orchestrator.md +18 -0
- package/.qa-ai/adapters/claude/commands/qa-add-tests.md +42 -0
- package/.qa-ai/adapters/claude/commands/qa-automation-plan.md +43 -0
- package/.qa-ai/adapters/claude/commands/qa-clean.md +42 -0
- package/.qa-ai/adapters/claude/commands/qa-config.md +51 -0
- package/.qa-ai/adapters/claude/commands/qa-coverage.md +46 -0
- package/.qa-ai/adapters/claude/commands/qa-doctor.md +11 -0
- package/.qa-ai/adapters/claude/commands/qa-full-flow.md +59 -0
- package/.qa-ai/adapters/claude/commands/qa-gate.md +36 -0
- package/.qa-ai/adapters/claude/commands/qa-help.md +30 -0
- package/.qa-ai/adapters/claude/commands/qa-init.md +70 -0
- package/.qa-ai/adapters/claude/commands/qa-status.md +56 -0
- package/.qa-ai/adapters/claude/commands/qa-update-tests.md +47 -0
- package/.qa-ai/adapters/claude/commands/qa-validate-features.md +36 -0
- package/.qa-ai/adapters/cline/.cline/README.md +25 -0
- package/.qa-ai/adapters/cline/.clinerules +9 -0
- package/.qa-ai/adapters/codex/README.md +44 -0
- package/.qa-ai/adapters/codex/prompts/implement-project.md +15 -0
- package/.qa-ai/adapters/continue/README.md +26 -0
- package/.qa-ai/adapters/continue/checks/qa-feature-conventions.md +15 -0
- package/.qa-ai/adapters/gemini/GEMINI.md +40 -0
- package/.qa-ai/adapters/generic/AGENTS.md +100 -0
- package/.qa-ai/adapters/goose/recipes/qa-flowkit.yaml +20 -0
- package/.qa-ai/adapters/opencode/README.md +57 -0
- package/.qa-ai/adapters/opencode/agents/qa-workflow.md +18 -0
- package/.qa-ai/adapters/opencode/commands/qa-add-tests.md +42 -0
- package/.qa-ai/adapters/opencode/commands/qa-automation-plan.md +43 -0
- package/.qa-ai/adapters/opencode/commands/qa-clean.md +42 -0
- package/.qa-ai/adapters/opencode/commands/qa-config.md +51 -0
- package/.qa-ai/adapters/opencode/commands/qa-coverage.md +46 -0
- package/.qa-ai/adapters/opencode/commands/qa-doctor.md +13 -0
- package/.qa-ai/adapters/opencode/commands/qa-full-flow.md +59 -0
- package/.qa-ai/adapters/opencode/commands/qa-gate.md +36 -0
- package/.qa-ai/adapters/opencode/commands/qa-help.md +30 -0
- package/.qa-ai/adapters/opencode/commands/qa-init.md +70 -0
- package/.qa-ai/adapters/opencode/commands/qa-status.md +56 -0
- package/.qa-ai/adapters/opencode/commands/qa-update-tests.md +47 -0
- package/.qa-ai/adapters/opencode/commands/qa-validate-features.md +36 -0
- package/.qa-ai/agents/README.md +39 -0
- package/.qa-ai/agents/api-testing-agent.md +73 -0
- package/.qa-ai/agents/automation-feasibility-agent.md +128 -0
- package/.qa-ai/agents/gherkin-test-design-agent.md +110 -0
- package/.qa-ai/agents/jira-task-agent.md +92 -0
- package/.qa-ai/agents/pr-agent.md +101 -0
- package/.qa-ai/agents/qa-context-intake-agent.md +75 -0
- package/.qa-ai/agents/qa-workflow-orchestrator.md +113 -0
- package/.qa-ai/agents/release-gate-agent.md +50 -0
- package/.qa-ai/agents/requirements-intake-agent.md +79 -0
- package/.qa-ai/agents/requirements-normalization-agent.md +80 -0
- package/.qa-ai/agents/specialists/available/appium.md +59 -0
- package/.qa-ai/agents/specialists/available/cypress.md +68 -0
- package/.qa-ai/agents/specialists/available/generic-test-design.md +117 -0
- package/.qa-ai/agents/specialists/available/jira.md +108 -0
- package/.qa-ai/agents/specialists/available/karate.md +97 -0
- package/.qa-ai/agents/specialists/available/playwright-api.md +87 -0
- package/.qa-ai/agents/specialists/available/playwright-ui.md +87 -0
- package/.qa-ai/agents/specialists/available/postman.md +108 -0
- package/.qa-ai/agents/specialists/available/rest-assured.md +103 -0
- package/.qa-ai/agents/specialists/available/selenium.md +91 -0
- package/.qa-ai/agents/specialists/available/testrail.md +85 -0
- package/.qa-ai/agents/specialists/available/webdriverio.md +81 -0
- package/.qa-ai/agents/test-design-system-agent.md +33 -0
- package/.qa-ai/agents/testrail-coverage-agent.md +84 -0
- package/.qa-ai/agents/testrail-sync-agent.md +96 -0
- package/.qa-ai/agents/webdriverio-implementation-agent.md +84 -0
- package/.qa-ai/presets/manual-only.yaml +65 -0
- package/.qa-ai/presets/selenium-jest-browserstack.yaml +72 -0
- package/.qa-ai/presets/webdriverio-playwright-api.yaml +85 -0
- package/.qa-ai/rules/api-testing.rules.md +7 -0
- package/.qa-ai/rules/approval.rules.md +8 -0
- package/.qa-ai/rules/automation.rules.md +7 -0
- package/.qa-ai/rules/gherkin.rules.md +12 -0
- package/.qa-ai/rules/testrail.rules.md +10 -0
- package/.qa-ai/rules/webdriverio.rules.md +9 -0
- package/.qa-ai/scripts/bootstrap-agent-adapters.mjs +127 -0
- package/.qa-ai/scripts/clean.mjs +243 -0
- package/.qa-ai/scripts/config.mjs +202 -0
- package/.qa-ai/scripts/doctor.mjs +383 -0
- package/.qa-ai/scripts/init.mjs +447 -0
- package/.qa-ai/scripts/lib/markdown-table.mjs +76 -0
- package/.qa-ai/scripts/lib/project-config.mjs +184 -0
- package/.qa-ai/scripts/lib/qa-next-steps.mjs +578 -0
- package/.qa-ai/scripts/lib/release-gate.mjs +66 -0
- package/.qa-ai/scripts/lib/test-design.mjs +92 -0
- package/.qa-ai/scripts/lib/test-management-mapping.mjs +73 -0
- package/.qa-ai/scripts/lib/utils.mjs +331 -0
- package/.qa-ai/scripts/qa-help.mjs +44 -0
- package/.qa-ai/scripts/smoke-npm-pack.mjs +187 -0
- package/.qa-ai/scripts/smoke-test.mjs +465 -0
- package/.qa-ai/scripts/sync-agent-adapters.mjs +121 -0
- package/.qa-ai/scripts/test-validators.mjs +334 -0
- package/.qa-ai/scripts/validate-active-specialists.mjs +106 -0
- package/.qa-ai/scripts/validate-features.mjs +277 -0
- package/.qa-ai/scripts/validate-release-gate.mjs +105 -0
- package/.qa-ai/scripts/validate-sync-plan.mjs +186 -0
- package/.qa-ai/scripts/validate-target.mjs +104 -0
- package/.qa-ai/scripts/validate-test-design.mjs +117 -0
- package/.qa-ai/scripts/validate-traceability.mjs +183 -0
- package/.qa-ai/templates/automation-feasibility-report.template.md +21 -0
- package/.qa-ai/templates/automation-implementation-plan.template.md +23 -0
- package/.qa-ai/templates/feature.template +13 -0
- package/.qa-ai/templates/jira-automation-task.template.md +25 -0
- package/.qa-ai/templates/pr-template.md +60 -0
- package/.qa-ai/templates/release-gate.template.yaml +16 -0
- package/.qa-ai/templates/requirement-analysis.template.md +17 -0
- package/.qa-ai/templates/test-design-proposal.template.md +26 -0
- package/.qa-ai/templates/test-design-system.template.md +15 -0
- package/.qa-ai/templates/test-management-mapping.template.json +18 -0
- package/.qa-ai/templates/testrail-coverage-analysis.template.md +17 -0
- package/.qa-ai/templates/testrail-sync-plan.template.md +22 -0
- package/.qa-ai/templates/traceability-matrix.template.md +4 -0
- package/.qa-ai/workflows/automation-analysis.md +23 -0
- package/.qa-ai/workflows/cleanup.md +52 -0
- package/.qa-ai/workflows/context-intake.md +66 -0
- package/.qa-ai/workflows/full-flow.md +55 -0
- package/.qa-ai/workflows/implementation.md +24 -0
- package/.qa-ai/workflows/intake.md +3 -0
- package/.qa-ai/workflows/pr.md +3 -0
- package/.qa-ai/workflows/release-gate.md +22 -0
- package/.qa-ai/workflows/test-design-system.md +33 -0
- package/.qa-ai/workflows/test-design.md +23 -0
- package/.qa-ai/workflows/testrail-sync.md +23 -0
- package/CHANGELOG.md +108 -0
- package/CODE_OF_CONDUCT.md +11 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.es.md +602 -0
- package/README.md +633 -0
- package/ROADMAP.md +107 -0
- package/SECURITY.md +18 -0
- package/bin/qa-flowkit.mjs +214 -0
- package/docs/qa-ai/agent-compatibility.md +100 -0
- package/docs/qa-ai/architecture.md +130 -0
- package/docs/qa-ai/backlog.md +393 -0
- package/docs/qa-ai/cleanup.md +104 -0
- package/docs/qa-ai/customizing-agents.md +148 -0
- package/docs/qa-ai/getting-started.md +385 -0
- package/docs/qa-ai/implementation-guide-for-codex.md +210 -0
- package/docs/qa-ai/npm-migration-plan.md +50 -0
- package/docs/qa-ai/open-source-release-checklist.md +17 -0
- package/docs/qa-ai/qa-help.md +76 -0
- package/docs/qa-ai/release-gate.md +60 -0
- package/docs/qa-ai/terminal-transcripts.md +316 -0
- package/docs/qa-ai/test-design-dual-mode.md +75 -0
- package/docs/qa-ai/troubleshooting.md +740 -0
- package/docs/qa-ai/workflow.md +147 -0
- package/package.json +72 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import {
|
|
5
|
+
activeSpecialistsContent,
|
|
6
|
+
configuredDirs,
|
|
7
|
+
isConfiguredFramework,
|
|
8
|
+
slug
|
|
9
|
+
} from './lib/project-config.mjs';
|
|
10
|
+
import {
|
|
11
|
+
commaList,
|
|
12
|
+
ensureDir,
|
|
13
|
+
getConfigValue,
|
|
14
|
+
loadQaAiConfig,
|
|
15
|
+
manifestEntry,
|
|
16
|
+
manifestPath,
|
|
17
|
+
parseArgs,
|
|
18
|
+
parseSimpleYaml,
|
|
19
|
+
pathExists,
|
|
20
|
+
recordManifestEntries,
|
|
21
|
+
readText,
|
|
22
|
+
relativeTo,
|
|
23
|
+
resolveRepoPath,
|
|
24
|
+
writeFileSafe,
|
|
25
|
+
yamlScalar,
|
|
26
|
+
logHeader
|
|
27
|
+
} from './lib/utils.mjs';
|
|
28
|
+
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const args = parseArgs(process.argv);
|
|
31
|
+
const force = Boolean(args.force);
|
|
32
|
+
const withDocTemplates = Boolean(args['with-doc-templates'] || args.withDocTemplates);
|
|
33
|
+
const withTestManagementMapping = Boolean(args['with-test-management-mapping'] || args.withTestManagementMapping);
|
|
34
|
+
const presetName = args.preset || 'webdriverio-playwright-api';
|
|
35
|
+
const interfaceLanguage = normalizeLanguage(args['interface-language'] || args.interfaceLanguage || 'en', 'interface language');
|
|
36
|
+
const gherkinLanguage = normalizeLanguage(args['gherkin-language'] || args.gherkinLanguage || args.gherkin || 'en', 'Gherkin language');
|
|
37
|
+
let validatedQaContextPath = null;
|
|
38
|
+
const qaAiDir = path.join(cwd, '.qa-ai');
|
|
39
|
+
const presetsDir = path.join(qaAiDir, 'presets');
|
|
40
|
+
const presetPath = path.join(qaAiDir, 'presets', `${presetName}.yaml`);
|
|
41
|
+
|
|
42
|
+
function printHelp() {
|
|
43
|
+
console.log(`Usage: node .qa-ai/scripts/init.mjs [options]
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
--preset <name> Base template from .qa-ai/presets (default: webdriverio-playwright-api)
|
|
47
|
+
--interface-language <en|es> User-facing workflow language (default: en)
|
|
48
|
+
--gherkin-language <en|es> Gherkin feature language (default: en)
|
|
49
|
+
--requirements-source <name> Primary requirement source, for example markdown, jira, confluence
|
|
50
|
+
--test-management-tool <name> Test management tool, for example none, testrail, zephyr, xray
|
|
51
|
+
--issue-tracker <name> Issue tracker, for example none, jira, github
|
|
52
|
+
--qa-track <name> QA workflow depth: quick, standard, enterprise (default from preset)
|
|
53
|
+
--qa-context <path> Repo-local folder with QA working-practice docs for agent-assisted init
|
|
54
|
+
--ui-framework <name> UI/E2E framework, or none/undecided
|
|
55
|
+
--api-framework <name> API/integration framework, or none/undecided
|
|
56
|
+
--ui-specs-path <path> UI/E2E specs directory
|
|
57
|
+
--ui-page-objects-path <path> UI page objects directory
|
|
58
|
+
--api-specs-path <path> API/integration specs directory
|
|
59
|
+
--specialist-mode <auto|off|required> Specialist agent activation mode (default from base template)
|
|
60
|
+
--set <key=value> Repeatable scalar config override, for example automation.ui.framework=cypress
|
|
61
|
+
--adapters <list> Comma-separated adapters to generate, or "all" (default: opencode)
|
|
62
|
+
--adapter <name> Repeatable single adapter name
|
|
63
|
+
--no-adapters Skip adapter generation
|
|
64
|
+
--with-doc-templates Generate starter QA docs under qa-ai-output/
|
|
65
|
+
--with-test-management-mapping Generate the configured test management mapping file
|
|
66
|
+
--force Overwrite generated files when they already exist
|
|
67
|
+
--help Show this help
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function availablePresets() {
|
|
72
|
+
if (!await pathExists(presetsDir)) return [];
|
|
73
|
+
const entries = await fs.readdir(presetsDir, { withFileTypes: true });
|
|
74
|
+
return entries
|
|
75
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.yaml'))
|
|
76
|
+
.map((entry) => path.basename(entry.name, '.yaml'))
|
|
77
|
+
.sort();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeLanguage(value, label = 'language') {
|
|
81
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
82
|
+
if (['es', 'esp', 'spa', 'spanish', 'espanol', 'español'].includes(normalized)) return 'es';
|
|
83
|
+
if (['en', 'eng', 'english', 'ingles', 'inglés'].includes(normalized)) return 'en';
|
|
84
|
+
console.error(`Unsupported ${label}: ${value}. Use "en" or "es".`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function scalarOverrideValue(value) {
|
|
89
|
+
if (value === undefined || value === null || value === false) return null;
|
|
90
|
+
return yamlScalar(String(value));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function escapeRegExp(value) {
|
|
94
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function selectedQaContextPath() {
|
|
98
|
+
const values = commaList(args['qa-context'] || args.qaContext || args['qa-context-path'] || args.qaContextPath);
|
|
99
|
+
if (values.length === 0) return null;
|
|
100
|
+
if (values.length > 1) {
|
|
101
|
+
console.error('Only one --qa-context folder is supported in the MVP.');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
return values[0];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setSimpleYamlScalar(content, keyPath, value) {
|
|
108
|
+
const parts = String(keyPath || '').split('.').map((part) => part.trim()).filter(Boolean);
|
|
109
|
+
if (parts.length === 0) return content;
|
|
110
|
+
|
|
111
|
+
const lines = content.replace(/\r/g, '').split('\n');
|
|
112
|
+
let searchStart = 0;
|
|
113
|
+
let parentIndent = -1;
|
|
114
|
+
|
|
115
|
+
for (let depth = 0; depth < parts.length - 1; depth += 1) {
|
|
116
|
+
const key = parts[depth];
|
|
117
|
+
const pattern = new RegExp(`^(\\s*)${escapeRegExp(key)}:\\s*$`);
|
|
118
|
+
let foundIndex = -1;
|
|
119
|
+
let foundIndent = -1;
|
|
120
|
+
for (let i = searchStart; i < lines.length; i += 1) {
|
|
121
|
+
const line = lines[i];
|
|
122
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
123
|
+
const indent = line.match(/^ */)?.[0].length ?? 0;
|
|
124
|
+
if (parentIndent >= 0 && indent <= parentIndent) break;
|
|
125
|
+
const match = line.match(pattern);
|
|
126
|
+
if (match) {
|
|
127
|
+
foundIndex = i;
|
|
128
|
+
foundIndent = match[1].length;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (foundIndex === -1) return content;
|
|
133
|
+
searchStart = foundIndex + 1;
|
|
134
|
+
parentIndent = foundIndent;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const target = parts.at(-1);
|
|
138
|
+
const targetPattern = new RegExp(`^(\\s*)${escapeRegExp(target)}:\\s*.*$`);
|
|
139
|
+
for (let i = searchStart; i < lines.length; i += 1) {
|
|
140
|
+
const line = lines[i];
|
|
141
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
142
|
+
const indent = line.match(/^ */)?.[0].length ?? 0;
|
|
143
|
+
if (parentIndent >= 0 && indent <= parentIndent) break;
|
|
144
|
+
const match = line.match(targetPattern);
|
|
145
|
+
if (match) {
|
|
146
|
+
lines[i] = `${match[1]}${target}: ${value}`;
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const indent = parentIndent >= 0 ? ' '.repeat(parentIndent + 2) : '';
|
|
152
|
+
lines.splice(searchStart, 0, `${indent}${target}: ${value}`);
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function configOverrides() {
|
|
157
|
+
const uiFramework = args['ui-framework'] || args.uiFramework;
|
|
158
|
+
const apiFramework = args['api-framework'] || args.apiFramework;
|
|
159
|
+
const testManagementTool = args['test-management-tool'] || args.testManagementTool;
|
|
160
|
+
const uiSpecsPath = args['ui-specs-path'] || args.uiSpecsPath;
|
|
161
|
+
const uiPageObjectsPath = args['ui-page-objects-path'] || args.uiPageObjectsPath;
|
|
162
|
+
const apiSpecsPath = args['api-specs-path'] || args.apiSpecsPath;
|
|
163
|
+
const normalizedUiFramework = slug(uiFramework);
|
|
164
|
+
const normalizedApiFramework = slug(apiFramework);
|
|
165
|
+
const overrides = [
|
|
166
|
+
['project.defaultLanguage', interfaceLanguage],
|
|
167
|
+
['project.interfaceLanguage', interfaceLanguage],
|
|
168
|
+
['project.qaTrack', args['qa-track'] || args.qaTrack],
|
|
169
|
+
['gherkin.language', gherkinLanguage],
|
|
170
|
+
['knowledge.enabled', validatedQaContextPath ? 'true' : undefined],
|
|
171
|
+
['knowledge.sourcePath', validatedQaContextPath],
|
|
172
|
+
['sources.main', args['requirements-source'] || args.requirementsSource],
|
|
173
|
+
['tools.testManagement', testManagementTool],
|
|
174
|
+
['tools.issueTracker', args['issue-tracker'] || args.issueTracker],
|
|
175
|
+
['agents.specialistMode', args['specialist-mode'] || args.specialistMode],
|
|
176
|
+
['automation.ui.framework', uiFramework],
|
|
177
|
+
['automation.api.framework', apiFramework],
|
|
178
|
+
['automation.ui.specsPath', uiSpecsPath],
|
|
179
|
+
['automation.ui.pageObjectsPath', uiPageObjectsPath],
|
|
180
|
+
['automation.api.specsPath', apiSpecsPath]
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
if (uiFramework && !uiSpecsPath && normalizedUiFramework !== 'webdriverio') {
|
|
184
|
+
overrides.push(['automation.ui.specsPath', isConfiguredFramework(uiFramework) ? ['tests', slug(uiFramework), 'specs'].join('/') : '']);
|
|
185
|
+
}
|
|
186
|
+
if (uiFramework && !uiPageObjectsPath && normalizedUiFramework !== 'webdriverio') {
|
|
187
|
+
overrides.push(['automation.ui.pageObjectsPath', isConfiguredFramework(uiFramework) ? ['tests', slug(uiFramework), 'pageobjects'].join('/') : '']);
|
|
188
|
+
}
|
|
189
|
+
if (apiFramework && !apiSpecsPath && normalizedApiFramework !== 'playwright-api') {
|
|
190
|
+
overrides.push(['automation.api.specsPath', isConfiguredFramework(apiFramework) ? ['tests', slug(apiFramework), 'specs'].join('/') : '']);
|
|
191
|
+
}
|
|
192
|
+
if (testManagementTool) {
|
|
193
|
+
const isTestrail = String(testManagementTool).trim().toLowerCase() === 'testrail';
|
|
194
|
+
overrides.push(['testrail.enabled', isTestrail ? 'true' : 'false']);
|
|
195
|
+
if (!isTestrail) overrides.push(['testrail.mappingFile', '']);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const item of commaList(args.set)) {
|
|
199
|
+
const equalsIndex = item.indexOf('=');
|
|
200
|
+
if (equalsIndex <= 0) {
|
|
201
|
+
console.error(`Invalid --set value: ${item}. Use key.path=value.`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
overrides.push([item.slice(0, equalsIndex).trim(), item.slice(equalsIndex + 1).trim()]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return overrides
|
|
208
|
+
.map(([key, value]) => [key, scalarOverrideValue(value)])
|
|
209
|
+
.filter(([, value]) => value !== null);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function personalizeConfig(content) {
|
|
213
|
+
let updated = content.replace(/^(\s*name:\s*)CHANGE_ME\s*$/m, `$1${yamlScalar(path.basename(cwd))}`);
|
|
214
|
+
for (const [key, value] of configOverrides()) {
|
|
215
|
+
updated = setSimpleYamlScalar(updated, key, value);
|
|
216
|
+
}
|
|
217
|
+
return updated;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function selectedAdapters() {
|
|
221
|
+
if (args['no-adapters']) return [];
|
|
222
|
+
const requested = [...commaList(args.adapters), ...commaList(args.adapter)].map((name) => name.toLowerCase());
|
|
223
|
+
if (requested.length === 0) return ['opencode'];
|
|
224
|
+
if (requested.includes('all')) return ['all'];
|
|
225
|
+
return requested.includes('generic') ? requested : ['generic', ...requested];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function generatedDocs() {
|
|
229
|
+
return [
|
|
230
|
+
['templates/requirement-analysis.template.md', 'qa-ai-output/requirement-analysis.md'],
|
|
231
|
+
['templates/testrail-coverage-analysis.template.md', 'qa-ai-output/testrail-coverage-analysis.md'],
|
|
232
|
+
['templates/test-design-system.template.md', 'qa-ai-output/test-design-system.md'],
|
|
233
|
+
['templates/test-design-proposal.template.md', 'qa-ai-output/test-design-proposal.md'],
|
|
234
|
+
['templates/automation-feasibility-report.template.md', 'qa-ai-output/automation-feasibility-report.md'],
|
|
235
|
+
['templates/automation-implementation-plan.template.md', 'qa-ai-output/automation-implementation-plan.md'],
|
|
236
|
+
['templates/traceability-matrix.template.md', 'qa-ai-output/traceability-matrix.md'],
|
|
237
|
+
['templates/testrail-sync-plan.template.md', 'qa-ai-output/testrail-sync-plan.md'],
|
|
238
|
+
['templates/jira-automation-task.template.md', 'qa-ai-output/jira-automation-task.md'],
|
|
239
|
+
['templates/pr-template.md', 'qa-ai-output/pr-summary.md'],
|
|
240
|
+
['templates/release-gate.template.yaml', 'qa-ai-output/release-gate.yaml']
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const spanishTemplateHeadings = new Map([
|
|
245
|
+
['# Requirement Analysis', '# Analisis de requisitos'],
|
|
246
|
+
['## Main source', '## Fuente principal'],
|
|
247
|
+
['## Complementary sources', '## Fuentes complementarias'],
|
|
248
|
+
['## Functional scope', '## Alcance funcional'],
|
|
249
|
+
['## Acceptance Criteria', '## Criterios de aceptacion'],
|
|
250
|
+
['## Inferred Acceptance Criteria', '## Criterios de aceptacion inferidos'],
|
|
251
|
+
['## Ambiguities requiring user decision', '## Ambiguedades que requieren decision del usuario'],
|
|
252
|
+
['## Ambiguities', '## Ambiguedades'],
|
|
253
|
+
['## Out of scope', '## Fuera de alcance'],
|
|
254
|
+
['## QA impact', '## Impacto en QA'],
|
|
255
|
+
['# TestRail Coverage Analysis', '# Analisis de cobertura de gestion de pruebas'],
|
|
256
|
+
['# System Test Design', '# Diseno de pruebas de sistema'],
|
|
257
|
+
['## Architecture alignment', '## Alineacion con arquitectura'],
|
|
258
|
+
['## Testability risks', '## Riesgos de testabilidad'],
|
|
259
|
+
['## Cross-RF coverage strategy', '## Estrategia de cobertura entre RFs'],
|
|
260
|
+
['## Shared fixtures and data', '## Fixtures y datos compartidos'],
|
|
261
|
+
['## Non-functional focus', '## Enfoque no funcional'],
|
|
262
|
+
['## Open questions', '## Preguntas abiertas'],
|
|
263
|
+
['# Test Design Proposal (per RF / epic)', '# Propuesta de diseno de pruebas (por RF / epic)'],
|
|
264
|
+
['## Official RF ID', '## RF oficial'],
|
|
265
|
+
['## Scope', '## Alcance'],
|
|
266
|
+
['## Proposed tests', '## Pruebas propuestas'],
|
|
267
|
+
['## Existing tests to reuse', '## Pruebas existentes para reutilizar'],
|
|
268
|
+
['## Existing tests requiring modification', '## Pruebas existentes que requieren modificacion'],
|
|
269
|
+
['## New tests to create', '## Nuevas pruebas a crear'],
|
|
270
|
+
['## Approval request', '## Solicitud de aprobacion'],
|
|
271
|
+
['# Automation Feasibility Report', '# Informe de viabilidad de automatizacion'],
|
|
272
|
+
['# Automation Implementation Plan', '# Plan de implementacion de automatizacion'],
|
|
273
|
+
['# Traceability Matrix', '# Matriz de trazabilidad'],
|
|
274
|
+
['# TestRail Sync Plan', '# Plan de sincronizacion de gestion de pruebas'],
|
|
275
|
+
['# Jira Automation Task Draft', '# Borrador de tarea de automatizacion'],
|
|
276
|
+
['# PR Summary', '# Resumen de PR'],
|
|
277
|
+
['## Summary', '## Resumen'],
|
|
278
|
+
['## Validation', '## Validacion'],
|
|
279
|
+
['## Risks', '## Riesgos'],
|
|
280
|
+
['## Residual risk', '## Riesgo residual']
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
function localizeTemplate(content, language) {
|
|
284
|
+
if (String(language || '').toLowerCase() !== 'es') return content;
|
|
285
|
+
let updated = content;
|
|
286
|
+
for (const [english, spanish] of spanishTemplateHeadings) {
|
|
287
|
+
updated = updated.replaceAll(english, spanish);
|
|
288
|
+
}
|
|
289
|
+
return updated.replaceAll('Do you approve generating the proposed `.feature` files?', 'Apruebas generar los archivos `.feature` propuestos?');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function main() {
|
|
293
|
+
if (args.help) {
|
|
294
|
+
printHelp();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
logHeader('QA FlowKit init');
|
|
299
|
+
|
|
300
|
+
if (!await pathExists(qaAiDir)) {
|
|
301
|
+
console.error('Missing .qa-ai folder. Copy it into the repository root first.');
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!await pathExists(presetPath)) {
|
|
306
|
+
const names = await availablePresets();
|
|
307
|
+
console.error(`Base template not found: ${presetName}`);
|
|
308
|
+
console.error(`Available base templates: ${names.length > 0 ? names.join(', ') : '(none found)'}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const qaContextPath = selectedQaContextPath();
|
|
313
|
+
if (qaContextPath) {
|
|
314
|
+
const resolvedContext = resolveRepoPath(cwd, qaContextPath, { label: 'QA context folder' });
|
|
315
|
+
if (!await pathExists(resolvedContext)) {
|
|
316
|
+
console.error(`QA context folder not found: ${relativeTo(cwd, resolvedContext)}`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
const contextStats = await fs.stat(resolvedContext);
|
|
320
|
+
if (!contextStats.isDirectory()) {
|
|
321
|
+
console.error(`QA context path must be a folder: ${relativeTo(cwd, resolvedContext)}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
validatedQaContextPath = qaContextPath;
|
|
325
|
+
console.log(`Using QA context folder: ${qaContextPath}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
console.log(`Using base template: ${presetName}`);
|
|
329
|
+
console.log(`Using interface language: ${interfaceLanguage}`);
|
|
330
|
+
console.log(`Using Gherkin language: ${gherkinLanguage}`);
|
|
331
|
+
|
|
332
|
+
const configPath = path.join(cwd, 'qa-ai.config.yaml');
|
|
333
|
+
const configContent = personalizeConfig(await readText(presetPath));
|
|
334
|
+
const writes = [];
|
|
335
|
+
const manifestEntries = [];
|
|
336
|
+
const configWrite = await writeFileSafe(configPath, configContent, { force });
|
|
337
|
+
writes.push(configWrite);
|
|
338
|
+
if (configWrite.written) {
|
|
339
|
+
manifestEntries.push(await manifestEntry(cwd, configWrite.path, {
|
|
340
|
+
type: 'file',
|
|
341
|
+
category: 'generated',
|
|
342
|
+
source: 'init'
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const effectiveConfigContent = configWrite.written ? configContent : (await loadQaAiConfig(cwd)).content;
|
|
347
|
+
const config = parseSimpleYaml(effectiveConfigContent);
|
|
348
|
+
|
|
349
|
+
const dirs = configuredDirs(config);
|
|
350
|
+
|
|
351
|
+
const dirResults = [];
|
|
352
|
+
for (const dir of [...dirs].filter(Boolean).sort()) {
|
|
353
|
+
const result = await ensureDir(resolveRepoPath(cwd, dir, { label: `configured directory "${dir}"` }));
|
|
354
|
+
dirResults.push(result);
|
|
355
|
+
if (result.created) {
|
|
356
|
+
manifestEntries.push(await manifestEntry(cwd, result.path, {
|
|
357
|
+
type: 'dir',
|
|
358
|
+
category: 'generated',
|
|
359
|
+
source: 'init'
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (withDocTemplates) {
|
|
365
|
+
for (const [src, dest] of generatedDocs()) {
|
|
366
|
+
const source = path.join(qaAiDir, src);
|
|
367
|
+
if (await pathExists(source)) {
|
|
368
|
+
const outputLanguage = getConfigValue(config, 'project.interfaceLanguage', interfaceLanguage);
|
|
369
|
+
const content = localizeTemplate(await readText(source), outputLanguage);
|
|
370
|
+
const result = await writeFileSafe(resolveRepoPath(cwd, dest, { label: `generated artifact "${dest}"` }), content, { force });
|
|
371
|
+
writes.push(result);
|
|
372
|
+
if (result.written) {
|
|
373
|
+
manifestEntries.push(await manifestEntry(cwd, result.path, {
|
|
374
|
+
type: 'file',
|
|
375
|
+
category: 'generated',
|
|
376
|
+
source: 'init'
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
console.log('\nSkipping starter QA docs. Use --with-doc-templates to generate qa-ai-output/*.md templates.');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const specialistsResult = await writeFileSafe(
|
|
386
|
+
resolveRepoPath(cwd, '.qa-ai/agents/specialists/active.md', { label: 'active specialists index' }),
|
|
387
|
+
activeSpecialistsContent(config),
|
|
388
|
+
{ force: true }
|
|
389
|
+
);
|
|
390
|
+
writes.push(specialistsResult);
|
|
391
|
+
if (specialistsResult.written) {
|
|
392
|
+
manifestEntries.push(await manifestEntry(cwd, specialistsResult.path, {
|
|
393
|
+
type: 'file',
|
|
394
|
+
category: 'generated',
|
|
395
|
+
source: 'init'
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const mappingFile = getConfigValue(config, 'testrail.mappingFile', 'qa-ai-output/test-management-mapping.json');
|
|
400
|
+
if (mappingFile && withTestManagementMapping) {
|
|
401
|
+
const result = await writeFileSafe(resolveRepoPath(cwd, mappingFile, { label: 'test management mapping file' }), '{}\n', { force });
|
|
402
|
+
writes.push(result);
|
|
403
|
+
if (result.written) {
|
|
404
|
+
manifestEntries.push(await manifestEntry(cwd, result.path, {
|
|
405
|
+
type: 'file',
|
|
406
|
+
category: 'generated',
|
|
407
|
+
source: 'init'
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
} else if (mappingFile) {
|
|
411
|
+
console.log('Skipping test management mapping file. Use --with-test-management-mapping to create it.');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const adapters = selectedAdapters();
|
|
415
|
+
if (adapters.length > 0) {
|
|
416
|
+
console.log('\nSyncing agent adapters...');
|
|
417
|
+
const { spawn } = await import('node:child_process');
|
|
418
|
+
const syncArgs = [path.join(qaAiDir, 'scripts', 'sync-agent-adapters.mjs')];
|
|
419
|
+
if (force) syncArgs.push('--force');
|
|
420
|
+
if (!(adapters.length === 1 && adapters[0] === 'all')) syncArgs.push('--adapters', adapters.join(','));
|
|
421
|
+
const exitCode = await new Promise((resolve) => {
|
|
422
|
+
const child = spawn(process.execPath, syncArgs, { stdio: 'inherit', shell: false });
|
|
423
|
+
child.on('close', resolve);
|
|
424
|
+
});
|
|
425
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
426
|
+
} else {
|
|
427
|
+
console.log('\nSkipping agent adapter sync.');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const manifest = await recordManifestEntries(cwd, manifestEntries);
|
|
431
|
+
|
|
432
|
+
console.log('\nInit completed. Summary:');
|
|
433
|
+
for (const result of dirResults) {
|
|
434
|
+
console.log(`${result.created ? 'created' : 'exists '} ${relativeTo(cwd, result.path)}`);
|
|
435
|
+
}
|
|
436
|
+
for (const result of writes) {
|
|
437
|
+
if (!result) continue;
|
|
438
|
+
console.log(`${result.written ? 'created' : 'skipped'} ${relativeTo(cwd, result.path)}`);
|
|
439
|
+
}
|
|
440
|
+
if (manifest) console.log(`updated ${relativeTo(cwd, manifestPath(cwd))}`);
|
|
441
|
+
console.log('\nNext: node .qa-ai/scripts/doctor.mjs');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
main().catch((error) => {
|
|
445
|
+
console.error(error);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function normalizeColumn(value) {
|
|
2
|
+
return String(value || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function splitMarkdownRow(line) {
|
|
6
|
+
const trimmed = String(line || '').trim();
|
|
7
|
+
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) return null;
|
|
8
|
+
return trimmed.slice(1, -1).split('|').map((cell) => cell.trim());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isSeparatorRow(cells) {
|
|
12
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function rowValues(header, cells) {
|
|
16
|
+
return Object.fromEntries(header.map((column, index) => [normalizeColumn(column), cells[index]]));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseMarkdownTable(content, {
|
|
20
|
+
label = 'Markdown table',
|
|
21
|
+
requiredColumns = []
|
|
22
|
+
} = {}) {
|
|
23
|
+
const lines = String(content || '').replace(/\r/g, '').split('\n');
|
|
24
|
+
const errors = [];
|
|
25
|
+
const tableLines = [];
|
|
26
|
+
|
|
27
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
28
|
+
const cells = splitMarkdownRow(lines[index]);
|
|
29
|
+
if (!cells) continue;
|
|
30
|
+
tableLines.push({ line: index + 1, cells });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (tableLines.length === 0) {
|
|
34
|
+
return { errors: [`${label} must contain a Markdown table.`], rows: [], header: [] };
|
|
35
|
+
}
|
|
36
|
+
if (tableLines.length === 1) {
|
|
37
|
+
return { errors: [`${label} is missing a separator row and data rows.`], rows: [], header: tableLines[0].cells };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const header = tableLines[0].cells;
|
|
41
|
+
const separatorLine = tableLines[1];
|
|
42
|
+
const normalizedHeader = header.map(normalizeColumn);
|
|
43
|
+
|
|
44
|
+
if (!isSeparatorRow(separatorLine.cells)) {
|
|
45
|
+
errors.push(`Line ${separatorLine.line}: ${label} must have a Markdown separator row after the header.`);
|
|
46
|
+
}
|
|
47
|
+
if (separatorLine.cells.length !== header.length) {
|
|
48
|
+
errors.push(`Line ${separatorLine.line}: separator has ${separatorLine.cells.length} cell(s), expected ${header.length}.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const column of requiredColumns) {
|
|
52
|
+
if (!normalizedHeader.includes(normalizeColumn(column))) {
|
|
53
|
+
errors.push(`${label} is missing required column "${column}".`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (const row of tableLines.slice(2)) {
|
|
59
|
+
if (row.cells.length !== header.length) {
|
|
60
|
+
errors.push(`Line ${row.line}: row has ${row.cells.length} cell(s), expected ${header.length}.`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (row.cells.every((cell) => cell.trim() === '')) {
|
|
64
|
+
errors.push(`Line ${row.line}: row is empty.`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
rows.push({
|
|
69
|
+
line: row.line,
|
|
70
|
+
cells: row.cells,
|
|
71
|
+
values: rowValues(header, row.cells)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { errors, rows, header };
|
|
76
|
+
}
|