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,578 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
getConfigValue,
|
|
4
|
+
listFilesRecursive,
|
|
5
|
+
loadQaAiConfig,
|
|
6
|
+
parseSimpleYaml,
|
|
7
|
+
pathExists,
|
|
8
|
+
readText
|
|
9
|
+
} from './utils.mjs';
|
|
10
|
+
import { normalizeGateDecision } from './release-gate.mjs';
|
|
11
|
+
import { isConfiguredFramework } from './project-config.mjs';
|
|
12
|
+
|
|
13
|
+
export const QA_TRACK_IDS = ['quick', 'standard', 'enterprise'];
|
|
14
|
+
|
|
15
|
+
export const QA_TRACKS = {
|
|
16
|
+
quick: {
|
|
17
|
+
label: 'Quick',
|
|
18
|
+
description:
|
|
19
|
+
'Requirements to Gherkin, traceability and PR summary without test-management sync or automation implementation.'
|
|
20
|
+
},
|
|
21
|
+
standard: {
|
|
22
|
+
label: 'Standard',
|
|
23
|
+
description:
|
|
24
|
+
'Full QA workflow: test-management planning, feasibility, automation phases when configured, and PR summary.'
|
|
25
|
+
},
|
|
26
|
+
enterprise: {
|
|
27
|
+
label: 'Enterprise',
|
|
28
|
+
description:
|
|
29
|
+
'Standard workflow plus strict target validation and release-readiness checks for compliance-oriented teams.'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const TRACK_PHASE_ORDER = {
|
|
34
|
+
quick: ['context', 'intake', 'normalize', 'gherkin', 'traceability', 'pr'],
|
|
35
|
+
standard: [
|
|
36
|
+
'context',
|
|
37
|
+
'intake',
|
|
38
|
+
'normalize',
|
|
39
|
+
'test-design-system',
|
|
40
|
+
'test-design-rf',
|
|
41
|
+
'gherkin',
|
|
42
|
+
'tm-coverage',
|
|
43
|
+
'tm-sync',
|
|
44
|
+
'traceability',
|
|
45
|
+
'feasibility',
|
|
46
|
+
'ui-impl',
|
|
47
|
+
'api-impl',
|
|
48
|
+
'jira',
|
|
49
|
+
'pr'
|
|
50
|
+
],
|
|
51
|
+
enterprise: [
|
|
52
|
+
'context',
|
|
53
|
+
'intake',
|
|
54
|
+
'normalize',
|
|
55
|
+
'test-design-system',
|
|
56
|
+
'test-design-rf',
|
|
57
|
+
'gherkin',
|
|
58
|
+
'tm-coverage',
|
|
59
|
+
'tm-sync',
|
|
60
|
+
'traceability',
|
|
61
|
+
'feasibility',
|
|
62
|
+
'ui-impl',
|
|
63
|
+
'api-impl',
|
|
64
|
+
'jira',
|
|
65
|
+
'pr',
|
|
66
|
+
'release-gate'
|
|
67
|
+
]
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const PHASE_DEFINITIONS = {
|
|
71
|
+
context: {
|
|
72
|
+
name: 'QA context intake',
|
|
73
|
+
agent: 'qa-context-intake-agent.md',
|
|
74
|
+
slashCommand: '/qa-init',
|
|
75
|
+
workflow: '.qa-ai/workflows/context-intake.md'
|
|
76
|
+
},
|
|
77
|
+
intake: {
|
|
78
|
+
name: 'Requirements intake',
|
|
79
|
+
agent: 'requirements-intake-agent.md',
|
|
80
|
+
slashCommand: '/qa-full-flow',
|
|
81
|
+
workflow: '.qa-ai/workflows/intake.md',
|
|
82
|
+
artifacts: ['qa-ai-output/requirement-analysis.md']
|
|
83
|
+
},
|
|
84
|
+
normalize: {
|
|
85
|
+
name: 'Requirements normalization',
|
|
86
|
+
agent: 'requirements-normalization-agent.md',
|
|
87
|
+
slashCommand: '/qa-full-flow',
|
|
88
|
+
artifacts: ['qa-ai-output/normalized-requirements.md']
|
|
89
|
+
},
|
|
90
|
+
'test-design-system': {
|
|
91
|
+
name: 'System test design',
|
|
92
|
+
agent: 'test-design-system-agent.md',
|
|
93
|
+
slashCommand: '/qa-full-flow',
|
|
94
|
+
workflow: '.qa-ai/workflows/test-design-system.md',
|
|
95
|
+
artifacts: ['qa-ai-output/test-design-system.md'],
|
|
96
|
+
validateScript: 'node .qa-ai/scripts/validate-test-design.mjs --allow-missing'
|
|
97
|
+
},
|
|
98
|
+
'test-design-rf': {
|
|
99
|
+
name: 'Per-RF test design',
|
|
100
|
+
agent: 'gherkin-test-design-agent.md',
|
|
101
|
+
slashCommand: '/qa-full-flow',
|
|
102
|
+
workflow: '.qa-ai/workflows/test-design.md',
|
|
103
|
+
artifacts: ['qa-ai-output/test-design-proposal.md'],
|
|
104
|
+
validateScript: 'node .qa-ai/scripts/validate-test-design.mjs --allow-missing'
|
|
105
|
+
},
|
|
106
|
+
gherkin: {
|
|
107
|
+
name: 'Gherkin feature generation',
|
|
108
|
+
agent: 'gherkin-test-design-agent.md',
|
|
109
|
+
slashCommand: '/qa-full-flow',
|
|
110
|
+
workflow: '.qa-ai/workflows/test-design.md',
|
|
111
|
+
featureFiles: true
|
|
112
|
+
},
|
|
113
|
+
'tm-coverage': {
|
|
114
|
+
name: 'Test management coverage',
|
|
115
|
+
agent: 'testrail-coverage-agent.md',
|
|
116
|
+
slashCommand: '/qa-coverage',
|
|
117
|
+
artifacts: ['qa-ai-output/testrail-coverage-analysis.md']
|
|
118
|
+
},
|
|
119
|
+
'tm-sync': {
|
|
120
|
+
name: 'Test management sync plan',
|
|
121
|
+
agent: 'testrail-sync-agent.md',
|
|
122
|
+
slashCommand: '/qa-full-flow',
|
|
123
|
+
workflow: '.qa-ai/workflows/testrail-sync.md',
|
|
124
|
+
artifacts: ['qa-ai-output/testrail-sync-plan.md']
|
|
125
|
+
},
|
|
126
|
+
traceability: {
|
|
127
|
+
name: 'Traceability matrix',
|
|
128
|
+
agent: 'gherkin-test-design-agent.md',
|
|
129
|
+
slashCommand: '/qa-full-flow',
|
|
130
|
+
configArtifact: 'traceability.matrixPath',
|
|
131
|
+
validateScript: 'node .qa-ai/scripts/validate-traceability.mjs'
|
|
132
|
+
},
|
|
133
|
+
feasibility: {
|
|
134
|
+
name: 'Automation feasibility',
|
|
135
|
+
agent: 'automation-feasibility-agent.md',
|
|
136
|
+
slashCommand: '/qa-automation-plan',
|
|
137
|
+
workflow: '.qa-ai/workflows/automation-analysis.md',
|
|
138
|
+
artifacts: ['qa-ai-output/automation-feasibility-report.md']
|
|
139
|
+
},
|
|
140
|
+
'ui-impl': {
|
|
141
|
+
name: 'UI/E2E automation implementation',
|
|
142
|
+
agent: 'webdriverio-implementation-agent.md',
|
|
143
|
+
slashCommand: '/qa-full-flow',
|
|
144
|
+
workflow: '.qa-ai/workflows/implementation.md',
|
|
145
|
+
artifacts: ['qa-ai-output/automation-implementation-plan.md']
|
|
146
|
+
},
|
|
147
|
+
'api-impl': {
|
|
148
|
+
name: 'API automation implementation',
|
|
149
|
+
agent: 'api-testing-agent.md',
|
|
150
|
+
slashCommand: '/qa-full-flow',
|
|
151
|
+
workflow: '.qa-ai/workflows/implementation.md',
|
|
152
|
+
artifacts: ['qa-ai-output/automation-implementation-plan.md']
|
|
153
|
+
},
|
|
154
|
+
jira: {
|
|
155
|
+
name: 'Issue tracker task drafts',
|
|
156
|
+
agent: 'jira-task-agent.md',
|
|
157
|
+
slashCommand: '/qa-full-flow',
|
|
158
|
+
artifacts: ['qa-ai-output/jira-automation-task.md']
|
|
159
|
+
},
|
|
160
|
+
pr: {
|
|
161
|
+
name: 'PR summary',
|
|
162
|
+
agent: 'pr-agent.md',
|
|
163
|
+
slashCommand: '/qa-full-flow',
|
|
164
|
+
workflow: '.qa-ai/workflows/pr.md',
|
|
165
|
+
artifacts: ['qa-ai-output/pr-summary.md']
|
|
166
|
+
},
|
|
167
|
+
'release-gate': {
|
|
168
|
+
name: 'Release quality gate',
|
|
169
|
+
agent: 'release-gate-agent.md',
|
|
170
|
+
slashCommand: '/qa-gate',
|
|
171
|
+
workflow: '.qa-ai/workflows/release-gate.md',
|
|
172
|
+
releaseGate: true,
|
|
173
|
+
validateScript: 'node .qa-ai/scripts/validate-release-gate.mjs'
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export function normalizeQaTrack(value) {
|
|
178
|
+
const normalized = String(value || 'standard').trim().toLowerCase();
|
|
179
|
+
if (['quick', 'fast', 'minimal', 'light'].includes(normalized)) return 'quick';
|
|
180
|
+
if (['enterprise', 'compliance', 'regulated'].includes(normalized)) return 'enterprise';
|
|
181
|
+
if (['standard', 'method', 'full', 'default'].includes(normalized)) return 'standard';
|
|
182
|
+
return QA_TRACK_IDS.includes(normalized) ? normalized : 'standard';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isEnabled(value) {
|
|
186
|
+
return value === true || String(value || '').trim().toLowerCase() === 'true';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isConfiguredTool(value) {
|
|
190
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
191
|
+
return Boolean(normalized) && !['none', 'undecided', 'n/a', 'na'].includes(normalized);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveConfigPath(config, keyOrPath) {
|
|
195
|
+
if (!keyOrPath) return '';
|
|
196
|
+
const text = String(keyOrPath).trim();
|
|
197
|
+
if (text.includes('/') || text.endsWith('.md') || text.endsWith('.json') || text.endsWith('.feature')) {
|
|
198
|
+
return text;
|
|
199
|
+
}
|
|
200
|
+
if (text.includes('.')) {
|
|
201
|
+
return String(getConfigValue(config, text, '') || '').trim();
|
|
202
|
+
}
|
|
203
|
+
return text;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function artifactExists(cwd, config, relPath) {
|
|
207
|
+
const target = resolveConfigPath(config, relPath);
|
|
208
|
+
if (!target) return false;
|
|
209
|
+
return pathExists(path.join(cwd, target));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function hasFeatureFiles(cwd, config) {
|
|
213
|
+
const featureRoot = resolveConfigPath(config, 'gherkin.featurePath') || 'features';
|
|
214
|
+
const files = await listFilesRecursive(path.join(cwd, featureRoot), (file) => file.endsWith('.feature'));
|
|
215
|
+
return files.length > 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildContext(config) {
|
|
219
|
+
const knowledgeEnabled = isEnabled(getConfigValue(config, 'knowledge.enabled', false));
|
|
220
|
+
const testManagement = getConfigValue(config, 'tools.testManagement', '');
|
|
221
|
+
const issueTracker = getConfigValue(config, 'tools.issueTracker', '');
|
|
222
|
+
const uiFramework = String(getConfigValue(config, 'automation.ui.framework', 'none')).toLowerCase();
|
|
223
|
+
const apiFramework = String(getConfigValue(config, 'automation.api.framework', 'none')).toLowerCase();
|
|
224
|
+
const track = normalizeQaTrack(getConfigValue(config, 'project.qaTrack', 'standard'));
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
track,
|
|
228
|
+
knowledgeEnabled,
|
|
229
|
+
testManagementConfigured: isConfiguredTool(testManagement),
|
|
230
|
+
issueTrackerConfigured: isConfiguredTool(issueTracker),
|
|
231
|
+
uiAutomationConfigured: isConfiguredFramework(uiFramework),
|
|
232
|
+
apiAutomationConfigured: isConfiguredFramework(apiFramework)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function skipReason(phaseId, ctx) {
|
|
237
|
+
if (phaseId === 'context' && !ctx.knowledgeEnabled) {
|
|
238
|
+
return 'knowledge.enabled is false';
|
|
239
|
+
}
|
|
240
|
+
if (['tm-coverage', 'tm-sync'].includes(phaseId)) {
|
|
241
|
+
if (ctx.track === 'quick') return `not included in ${ctx.track} track`;
|
|
242
|
+
if (!ctx.testManagementConfigured) return 'tools.testManagement is none or missing';
|
|
243
|
+
}
|
|
244
|
+
if (['test-design-system', 'test-design-rf'].includes(phaseId) && ctx.track === 'quick') {
|
|
245
|
+
return `not included in ${ctx.track} track (use gherkin phase for proposal + features)`;
|
|
246
|
+
}
|
|
247
|
+
if (phaseId === 'feasibility' && ctx.track === 'quick') {
|
|
248
|
+
return `not included in ${ctx.track} track`;
|
|
249
|
+
}
|
|
250
|
+
if (phaseId === 'ui-impl') {
|
|
251
|
+
if (ctx.track === 'quick') return `not included in ${ctx.track} track`;
|
|
252
|
+
if (!ctx.uiAutomationConfigured) return 'automation.ui.framework is none or undecided';
|
|
253
|
+
}
|
|
254
|
+
if (phaseId === 'api-impl') {
|
|
255
|
+
if (ctx.track === 'quick') return `not included in ${ctx.track} track`;
|
|
256
|
+
if (!ctx.apiAutomationConfigured) return 'automation.api.framework is none or undecided';
|
|
257
|
+
}
|
|
258
|
+
if (phaseId === 'jira') {
|
|
259
|
+
if (ctx.track === 'quick') return `not included in ${ctx.track} track`;
|
|
260
|
+
if (!ctx.issueTrackerConfigured) return 'tools.issueTracker is none or missing';
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function isPhaseComplete(cwd, config, phaseId, def) {
|
|
266
|
+
if (def.releaseGate) {
|
|
267
|
+
const gatePath = resolveConfigPath(
|
|
268
|
+
config,
|
|
269
|
+
getConfigValue(config, 'release.gatePath', 'qa-ai-output/release-gate.yaml')
|
|
270
|
+
);
|
|
271
|
+
const absolute = path.join(cwd, gatePath);
|
|
272
|
+
if (!await pathExists(absolute)) return false;
|
|
273
|
+
try {
|
|
274
|
+
const data = parseSimpleYaml(await readText(absolute));
|
|
275
|
+
const decision = normalizeGateDecision(data?.decision);
|
|
276
|
+
return Boolean(decision) && decision !== 'PENDING';
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (phaseId === 'context') {
|
|
282
|
+
const summary = resolveConfigPath(config, 'knowledge.summaryPath') || 'qa-ai-output/qa-knowledge-summary.md';
|
|
283
|
+
return artifactExists(cwd, config, summary);
|
|
284
|
+
}
|
|
285
|
+
if (def.featureFiles) {
|
|
286
|
+
return hasFeatureFiles(cwd, config);
|
|
287
|
+
}
|
|
288
|
+
if (def.configArtifact) {
|
|
289
|
+
return artifactExists(cwd, config, def.configArtifact);
|
|
290
|
+
}
|
|
291
|
+
if (def.artifacts) {
|
|
292
|
+
const checks = await Promise.all(def.artifacts.map((item) => artifactExists(cwd, config, item)));
|
|
293
|
+
return checks.every(Boolean);
|
|
294
|
+
}
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function phaseCommand(def) {
|
|
299
|
+
const parts = [];
|
|
300
|
+
if (def.slashCommand) parts.push(def.slashCommand);
|
|
301
|
+
if (def.agent) parts.push(`load .qa-ai/agents/${def.agent}`);
|
|
302
|
+
if (def.validateScript) parts.push(def.validateScript);
|
|
303
|
+
return parts.join(' · ');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function inspectQaWorkflow(cwd) {
|
|
307
|
+
const frameworkPath = path.join(cwd, '.qa-ai');
|
|
308
|
+
const hasFramework = await pathExists(frameworkPath);
|
|
309
|
+
const configInfo = await loadQaAiConfig(cwd);
|
|
310
|
+
|
|
311
|
+
if (!hasFramework) {
|
|
312
|
+
return {
|
|
313
|
+
initialized: false,
|
|
314
|
+
configExists: false,
|
|
315
|
+
track: 'standard',
|
|
316
|
+
context: null,
|
|
317
|
+
phases: [],
|
|
318
|
+
recommendations: [
|
|
319
|
+
{
|
|
320
|
+
priority: 'required',
|
|
321
|
+
title: 'Install the portable framework',
|
|
322
|
+
command: 'cp -R /path/to/QA_FlowKit/.qa-ai .qa-ai',
|
|
323
|
+
detail: 'Copy the .qa-ai folder into the target repository root.'
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
priority: 'required',
|
|
327
|
+
title: 'Initialize the target repository',
|
|
328
|
+
command: 'node .qa-ai/scripts/init.mjs',
|
|
329
|
+
detail: 'Generates qa-ai.config.yaml, folders and default adapters.'
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
priority: 'recommended',
|
|
333
|
+
title: 'Bootstrap agent-first commands',
|
|
334
|
+
command: 'node .qa-ai/scripts/bootstrap-agent-adapters.mjs --agents claude,opencode',
|
|
335
|
+
detail: 'Optional: enables /qa-init in Claude Code and OpenCode.'
|
|
336
|
+
}
|
|
337
|
+
],
|
|
338
|
+
completedPhaseIds: [],
|
|
339
|
+
pendingPhaseIds: [],
|
|
340
|
+
skippedPhaseIds: []
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!configInfo.exists) {
|
|
345
|
+
return {
|
|
346
|
+
initialized: true,
|
|
347
|
+
configExists: false,
|
|
348
|
+
track: 'standard',
|
|
349
|
+
context: null,
|
|
350
|
+
phases: [],
|
|
351
|
+
recommendations: [
|
|
352
|
+
{
|
|
353
|
+
priority: 'required',
|
|
354
|
+
title: 'Generate project configuration',
|
|
355
|
+
command: 'node .qa-ai/scripts/init.mjs',
|
|
356
|
+
detail: 'Creates qa-ai.config.yaml and the configured folder layout.'
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
priority: 'recommended',
|
|
360
|
+
title: 'Verify setup',
|
|
361
|
+
command: 'node .qa-ai/scripts/doctor.mjs',
|
|
362
|
+
detail: 'Validates framework assets and configured paths.'
|
|
363
|
+
}
|
|
364
|
+
],
|
|
365
|
+
completedPhaseIds: [],
|
|
366
|
+
pendingPhaseIds: [],
|
|
367
|
+
skippedPhaseIds: []
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const ctx = buildContext(configInfo.data);
|
|
372
|
+
const order = TRACK_PHASE_ORDER[ctx.track] || TRACK_PHASE_ORDER.standard;
|
|
373
|
+
const phases = [];
|
|
374
|
+
const completedPhaseIds = [];
|
|
375
|
+
const pendingPhaseIds = [];
|
|
376
|
+
const skippedPhaseIds = [];
|
|
377
|
+
|
|
378
|
+
for (const phaseId of order) {
|
|
379
|
+
const def = PHASE_DEFINITIONS[phaseId];
|
|
380
|
+
const skip = skipReason(phaseId, ctx);
|
|
381
|
+
if (skip) {
|
|
382
|
+
skippedPhaseIds.push(phaseId);
|
|
383
|
+
phases.push({
|
|
384
|
+
id: phaseId,
|
|
385
|
+
name: def.name,
|
|
386
|
+
status: 'skipped',
|
|
387
|
+
skipReason: skip,
|
|
388
|
+
command: phaseCommand(def)
|
|
389
|
+
});
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const complete = await isPhaseComplete(cwd, configInfo.data, phaseId, def);
|
|
394
|
+
if (complete) {
|
|
395
|
+
completedPhaseIds.push(phaseId);
|
|
396
|
+
phases.push({
|
|
397
|
+
id: phaseId,
|
|
398
|
+
name: def.name,
|
|
399
|
+
status: 'complete',
|
|
400
|
+
command: phaseCommand(def)
|
|
401
|
+
});
|
|
402
|
+
} else {
|
|
403
|
+
pendingPhaseIds.push(phaseId);
|
|
404
|
+
phases.push({
|
|
405
|
+
id: phaseId,
|
|
406
|
+
name: def.name,
|
|
407
|
+
status: 'pending',
|
|
408
|
+
command: phaseCommand(def),
|
|
409
|
+
agent: def.agent,
|
|
410
|
+
workflow: def.workflow || null,
|
|
411
|
+
validateScript: def.validateScript || null
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const recommendations = buildRecommendations({
|
|
417
|
+
ctx,
|
|
418
|
+
pendingPhaseIds,
|
|
419
|
+
completedPhaseIds,
|
|
420
|
+
config: configInfo.data
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
initialized: true,
|
|
425
|
+
configExists: true,
|
|
426
|
+
track: ctx.track,
|
|
427
|
+
trackInfo: QA_TRACKS[ctx.track],
|
|
428
|
+
context: ctx,
|
|
429
|
+
phases,
|
|
430
|
+
completedPhaseIds,
|
|
431
|
+
pendingPhaseIds,
|
|
432
|
+
skippedPhaseIds,
|
|
433
|
+
recommendations
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildRecommendations({ ctx, pendingPhaseIds, completedPhaseIds, config }) {
|
|
438
|
+
const items = [];
|
|
439
|
+
const nextId = pendingPhaseIds[0];
|
|
440
|
+
|
|
441
|
+
if (nextId) {
|
|
442
|
+
const def = PHASE_DEFINITIONS[nextId];
|
|
443
|
+
items.push({
|
|
444
|
+
priority: 'required',
|
|
445
|
+
title: `Next phase: ${def.name}`,
|
|
446
|
+
command: def.slashCommand || '/qa-full-flow',
|
|
447
|
+
detail: `Load ${def.agent}${def.workflow ? ` and ${def.workflow}` : ''}.`
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
if (nextId === 'gherkin') {
|
|
451
|
+
items.push({
|
|
452
|
+
priority: 'recommended',
|
|
453
|
+
title: 'Validate features after generation',
|
|
454
|
+
command: 'node .qa-ai/scripts/validate-features.mjs',
|
|
455
|
+
detail: 'Checks Gherkin structure, tags, RF IDs and acceptance criteria.'
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (nextId === 'traceability') {
|
|
459
|
+
items.push({
|
|
460
|
+
priority: 'recommended',
|
|
461
|
+
title: 'Validate traceability coverage',
|
|
462
|
+
command: 'node .qa-ai/scripts/validate-traceability.mjs',
|
|
463
|
+
detail: 'Ensures feature identifiers appear in the traceability matrix.'
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
} else if (completedPhaseIds.length > 0) {
|
|
467
|
+
if (ctx.track === 'enterprise' && !completedPhaseIds.includes('release-gate')) {
|
|
468
|
+
items.push({
|
|
469
|
+
priority: 'required',
|
|
470
|
+
title: 'Record release quality gate',
|
|
471
|
+
command: '/qa-gate',
|
|
472
|
+
detail: 'Produce qa-ai-output/release-gate.yaml with PASS, CONCERNS, FAIL or WAIVED.'
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
items.push({
|
|
476
|
+
priority: 'required',
|
|
477
|
+
title: 'QA track workflow complete',
|
|
478
|
+
command: '/qa-status',
|
|
479
|
+
detail: 'Summarize artifacts, run validators and confirm release readiness.'
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (ctx.track === 'enterprise' || ctx.track === 'standard') {
|
|
485
|
+
if (pendingPhaseIds.length === 0 || ['traceability', 'pr', 'jira', 'api-impl', 'ui-impl'].includes(nextId)) {
|
|
486
|
+
items.push({
|
|
487
|
+
priority: ctx.track === 'enterprise' ? 'required' : 'recommended',
|
|
488
|
+
title: 'Run aggregated target validation',
|
|
489
|
+
command: 'node .qa-ai/scripts/validate-target.mjs',
|
|
490
|
+
detail:
|
|
491
|
+
ctx.track === 'enterprise'
|
|
492
|
+
? 'Runs strict doctor, validators and release gate for CI-style hardening.'
|
|
493
|
+
: 'Runs doctor --strict and validators in sequence for initialized repositories.'
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (ctx.track === 'enterprise' && nextId === 'release-gate') {
|
|
497
|
+
items.push({
|
|
498
|
+
priority: 'recommended',
|
|
499
|
+
title: 'Validate release gate file',
|
|
500
|
+
command: 'node .qa-ai/scripts/validate-release-gate.mjs',
|
|
501
|
+
detail: 'Checks decision, risks, evidence paths and approver rules.'
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (pendingPhaseIds.length === 0) {
|
|
507
|
+
items.push({
|
|
508
|
+
priority: 'optional',
|
|
509
|
+
title: 'Inspect repository status',
|
|
510
|
+
command: '/qa-status',
|
|
511
|
+
detail: 'Human-readable summary of config, artifacts and gaps.'
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const featurePath = resolveConfigPath(config, 'gherkin.featurePath') || 'features';
|
|
516
|
+
if (!pendingPhaseIds.includes('gherkin') && completedPhaseIds.includes('gherkin')) {
|
|
517
|
+
items.push({
|
|
518
|
+
priority: 'optional',
|
|
519
|
+
title: 'Add or update tests for a scope',
|
|
520
|
+
command: '/qa-add-tests',
|
|
521
|
+
detail: `Extend coverage under ${featurePath} for a new RF or change request.`
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return items;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function formatHelpReport(report, { query = '' } = {}) {
|
|
529
|
+
const lines = [];
|
|
530
|
+
lines.push('QA FlowKit — guided next steps');
|
|
531
|
+
if (query) lines.push(`Query: ${query}`);
|
|
532
|
+
lines.push('');
|
|
533
|
+
|
|
534
|
+
if (!report.initialized || !report.configExists) {
|
|
535
|
+
for (const item of report.recommendations) {
|
|
536
|
+
lines.push(`[${item.priority.toUpperCase()}] ${item.title}`);
|
|
537
|
+
lines.push(` ${item.command}`);
|
|
538
|
+
if (item.detail) lines.push(` ${item.detail}`);
|
|
539
|
+
lines.push('');
|
|
540
|
+
}
|
|
541
|
+
return lines.join('\n').trimEnd();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
lines.push(`Track: ${report.track} (${report.trackInfo.label})`);
|
|
545
|
+
lines.push(report.trackInfo.description);
|
|
546
|
+
lines.push('');
|
|
547
|
+
lines.push(`Completed phases (${report.completedPhaseIds.length}): ${
|
|
548
|
+
report.completedPhaseIds.length ? report.completedPhaseIds.join(', ') : 'none'
|
|
549
|
+
}`);
|
|
550
|
+
lines.push(`Skipped phases (${report.skippedPhaseIds.length}): ${
|
|
551
|
+
report.skippedPhaseIds.length ? report.skippedPhaseIds.join(', ') : 'none'
|
|
552
|
+
}`);
|
|
553
|
+
lines.push(`Pending phases (${report.pendingPhaseIds.length}): ${
|
|
554
|
+
report.pendingPhaseIds.length ? report.pendingPhaseIds.join(', ') : 'none'
|
|
555
|
+
}`);
|
|
556
|
+
lines.push('');
|
|
557
|
+
|
|
558
|
+
if (report.pendingPhaseIds.length > 0 || report.completedPhaseIds.length > 0) {
|
|
559
|
+
lines.push('Phase status:');
|
|
560
|
+
for (const phase of report.phases) {
|
|
561
|
+
const marker = phase.status === 'complete' ? '[done]' : phase.status === 'skipped' ? '[skip]' : '[ ]';
|
|
562
|
+
const suffix = phase.skipReason ? ` — ${phase.skipReason}` : '';
|
|
563
|
+
lines.push(` ${marker} ${phase.name}${suffix}`);
|
|
564
|
+
}
|
|
565
|
+
lines.push('');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
lines.push('Recommendations:');
|
|
569
|
+
for (const item of report.recommendations) {
|
|
570
|
+
lines.push(`[${item.priority.toUpperCase()}] ${item.title}`);
|
|
571
|
+
lines.push(` ${item.command}`);
|
|
572
|
+
if (item.detail) lines.push(` ${item.detail}`);
|
|
573
|
+
lines.push('');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
lines.push('Tip: run `/qa-help` in your agent or `node .qa-ai/scripts/qa-help.mjs` after each workflow step.');
|
|
577
|
+
return lines.join('\n').trimEnd();
|
|
578
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const GATE_DECISIONS = ['PASS', 'CONCERNS', 'FAIL', 'WAIVED', 'PENDING'];
|
|
2
|
+
|
|
3
|
+
export function normalizeGateDecision(value) {
|
|
4
|
+
return String(value || '')
|
|
5
|
+
.trim()
|
|
6
|
+
.toUpperCase()
|
|
7
|
+
.replace(/\s+/g, '_');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizeRiskList(value) {
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'string' && value.trim()) {
|
|
15
|
+
return value
|
|
16
|
+
.split(/\r?\n/)
|
|
17
|
+
.map((line) => line.replace(/^-\s*/, '').trim())
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateReleaseGateData(data, { source = 'release gate', allowPending = false } = {}) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
const decision = normalizeGateDecision(data?.decision);
|
|
26
|
+
|
|
27
|
+
if (!GATE_DECISIONS.includes(decision)) {
|
|
28
|
+
errors.push(`${source}: decision must be one of ${GATE_DECISIONS.join(', ')}.`);
|
|
29
|
+
return { decision, errors };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (decision === 'PENDING' && !allowPending) {
|
|
33
|
+
errors.push(`${source}: decision is PENDING; set PASS, CONCERNS, FAIL or WAIVED after review.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const approver = String(data?.approver || '').trim();
|
|
37
|
+
const waivedReason = String(data?.waived_reason || data?.waivedReason || '').trim();
|
|
38
|
+
const risks = normalizeRiskList(data?.open_risks ?? data?.openRisks);
|
|
39
|
+
const evidence = normalizeRiskList(data?.evidence_paths ?? data?.evidencePaths);
|
|
40
|
+
const coverage = String(data?.coverage_summary ?? data?.coverageSummary ?? '').trim();
|
|
41
|
+
|
|
42
|
+
if (!coverage) {
|
|
43
|
+
errors.push(`${source}: coverage_summary is required.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (evidence.length === 0) {
|
|
47
|
+
errors.push(`${source}: evidence_paths must list at least one repository-relative path.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (['CONCERNS', 'FAIL'].includes(decision) && risks.length === 0) {
|
|
51
|
+
errors.push(`${source}: open_risks must document at least one item when decision is ${decision}.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (decision === 'WAIVED') {
|
|
55
|
+
if (!approver) errors.push(`${source}: approver is required when decision is WAIVED.`);
|
|
56
|
+
if (!waivedReason) errors.push(`${source}: waived_reason is required when decision is WAIVED.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (decision === 'PASS' && risks.length === 1 && /^none documented$/i.test(risks[0])) {
|
|
60
|
+
// valid explicit none
|
|
61
|
+
} else if (decision === 'PASS' && risks.some((risk) => /^(fail|blocked|critical)/i.test(risk))) {
|
|
62
|
+
errors.push(`${source}: PASS cannot be used with blocking open_risks text.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { decision, errors, risks, evidence, approver, waivedReason, coverage };
|
|
66
|
+
}
|