@keber/qa-framework 1.11.3 → 1.13.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/CHANGELOG.md +340 -0
- package/README.md +42 -2
- package/docs/architecture.md +30 -26
- package/docs/decisions/lane-heartbeat-caller.md +86 -0
- package/docs/final-report.md +23 -23
- package/docs/folder-structure-guide.md +100 -80
- package/docs/installation.md +42 -7
- package/docs/skills-architecture.md +23 -23
- package/docs/spec-driven-philosophy.md +9 -9
- package/docs/usage-with-agent.md +88 -9
- package/docs/visuals/diag-01-ejecutivo-1.png +0 -0
- package/docs/visuals/diag-02-tactico-1.png +0 -0
- package/docs/visuals/diag-03-tecnico-1.png +0 -0
- package/docs/visuals/info-01-ejecutivo-1.png +0 -0
- package/docs/visuals/info-02-tactico-1.png +0 -0
- package/docs/visuals/info-03-tecnico-1.png +0 -0
- package/docs/visuals/pres-01-ejecutivo-1.png +0 -0
- package/docs/visuals/pres-02-tactico-1.png +0 -0
- package/docs/visuals/pres-03-tecnico-1.png +0 -0
- package/docs/visuals/qa-01-problema-1.png +0 -0
- package/docs/visuals/qa-02-que-es-1.png +0 -0
- package/docs/visuals/qa-03-pipeline-1.png +0 -0
- package/docs/visuals/qa-04-6-archivos-1.png +0 -0
- package/docs/visuals/qa-05-humano-1.png +0 -0
- package/docs/visuals/qa-06-agente-1.png +0 -0
- package/docs/visuals/qa-07-skills-arch-1.png +0 -0
- package/docs/visuals/qa-08-traceability-1.png +0 -0
- package/docs/visuals/qa-en-01-problem-1.png +0 -0
- package/docs/visuals/qa-en-02-what-is-1.png +0 -0
- package/docs/visuals/qa-en-03-pipeline-1.png +0 -0
- package/docs/visuals/qa-en-04-6files-1.png +0 -0
- package/docs/visuals/qa-en-05-human-1.png +0 -0
- package/docs/visuals/qa-en-06-agent-1.png +0 -0
- package/docs/visuals/qa-en-07-skills-arch-1.png +0 -0
- package/docs/visuals/qa-en-08-traceability-1.png +0 -0
- package/examples/module-example/suppliers/00-inventory.md +1 -1
- package/examples/module-example/suppliers/suppliers-create.spec.ts +8 -8
- package/integrations/ado-powershell/README.md +1 -1
- package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
- package/integrations/playwright/README.md +4 -4
- package/package.json +7 -3
- package/qa-framework.config.json +25 -3
- package/scripts/check-forbidden-chars.js +187 -0
- package/scripts/check-spanish-accents.js +159 -0
- package/scripts/cli.js +1 -1
- package/scripts/generate.js +2 -2
- package/scripts/init.js +101 -12
- package/scripts/lib/claude-agents.js +347 -0
- package/scripts/lib/claude-commands.js +56 -0
- package/scripts/lib/module-keys.js +54 -0
- package/scripts/sync-version.js +63 -0
- package/scripts/upgrade.js +369 -20
- package/scripts/validate.js +46 -21
- package/skills/qa-ado-integration/SKILL.md +19 -19
- package/skills/qa-ado-integration/references/scripts-and-config.md +6 -6
- package/skills/qa-automation/SKILL.md +49 -27
- package/skills/qa-automation/references/config-checklist.md +4 -4
- package/skills/qa-automation/references/dom-inspection-template.js +7 -7
- package/skills/qa-automation/references/patterns.md +20 -20
- package/skills/qa-automation/references/pom-template.md +161 -0
- package/skills/qa-maintenance/SKILL.md +11 -11
- package/skills/qa-maintenance/references/update-rules.md +8 -8
- package/skills/qa-module-analysis/SKILL.md +20 -19
- package/skills/qa-module-analysis/references/exploration-checklist.md +4 -4
- package/skills/qa-module-analysis/references/output-verification.md +69 -0
- package/skills/qa-module-analysis/references/spec-file-formats.md +14 -14
- package/skills/qa-spec-generation/SKILL.md +13 -7
- package/skills/qa-test-cases/SKILL.md +11 -81
- package/skills/qa-test-cases/references/test-case-template.md +5 -5
- package/skills/qa-test-plan/SKILL.md +22 -86
- package/skills/qa-test-plan/references/plan-de-pruebas-template.md +24 -24
- package/skills/qa-test-stabilization/SKILL.md +59 -30
- package/skills/qa-test-stabilization/references/classification-protocol.md +35 -35
- package/templates/agent-next-steps.md +10 -10
- package/templates/agents/qa-analisis.md +142 -0
- package/templates/agents/qa-asesoria.md +48 -0
- package/templates/agents/qa-informe-resultados.md +236 -0
- package/templates/agents/qa-investigador.md +143 -0
- package/templates/agents/qa-plan.md +131 -0
- package/templates/agents/qa-reporte-ejecucion.md +138 -0
- package/templates/automation-scaffold/.env.example +36 -6
- package/templates/automation-scaffold/fixtures/auth.ts +27 -5
- package/templates/automation-scaffold/fixtures/base.ts +23 -1
- package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
- package/templates/automation-scaffold/global-setup.ts +117 -48
- package/templates/automation-scaffold/playwright.config.ts +72 -32
- package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
- package/templates/automation-scaffold/scripts/lane-config.js +269 -0
- package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
- package/templates/defect-report.md +17 -3
- package/templates/execution-report.md +9 -9
- package/templates/integration-scaffold/.env.example +1 -1
- package/templates/integration-scaffold/global-setup.ts +21 -1
- package/templates/integration-scaffold/playwright.config.ts +15 -1
- package/templates/qa-framework.instructions.md +9 -8
- package/templates/qa-framework.rules.md +53 -0
- package/templates/qa-readme.md +8 -8
- package/templates/session-summary.md +2 -2
- package/templates/specification/00-inventory.md +3 -3
- package/templates/specification/01-business-rules.md +6 -6
- package/templates/specification/02-workflows.md +1 -1
- package/templates/specification/03-roles-permissions.md +1 -1
- package/templates/specification/04-test-data.md +8 -8
- package/templates/specification/05-test-scenarios.md +5 -5
- package/templates/test-case.md +3 -3
- package/templates/test-data-guidelines.md +7 -7
- package/templates/test-plan-sprint.md +13 -13
- package/templates/test-plan.md +3 -3
- package/skills/qa-test-plan/references/priority-and-feasibility.md +0 -159
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/lib/claude-agents.js - Generate .claude/agents/qa-*.md sprint-cycle subagents
|
|
5
|
+
*
|
|
6
|
+
* Optional ANALISIS/PLAN mode, gated by integrations.azureDevOps.sprintCycle.enabled in
|
|
7
|
+
* qa-framework.config.json. Claude Code only - subagents with per-agent tools/model
|
|
8
|
+
* frontmatter have no GitHub Copilot equivalent, so no .github/ counterpart is generated.
|
|
9
|
+
*
|
|
10
|
+
* Placeholders are resolved from the project config, falling back to neutral defaults
|
|
11
|
+
* when a field (or the whole config section) is absent.
|
|
12
|
+
*
|
|
13
|
+
* Template syntax:
|
|
14
|
+
* {{NAME}} placeholder, replaced in a single pass (values are
|
|
15
|
+
* inserted verbatim and never re-expanded)
|
|
16
|
+
* {{#if FLAG}}...{{/if}} kept only when FLAG is true (not nestable)
|
|
17
|
+
* {{#unless FLAG}}...{{/unless}} kept only when FLAG is false (not nestable)
|
|
18
|
+
*
|
|
19
|
+
* Any {{...}} token left after rendering is a template bug and throws.
|
|
20
|
+
*
|
|
21
|
+
* buildAgentContent() throws per agent, so callers can generate the agents that are
|
|
22
|
+
* valid, report the one that is not, and exit non-zero.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
const { laneTableFromConfig, DEFAULT_LANE_TABLE } = require('../../templates/automation-scaffold/scripts/lane-config');
|
|
29
|
+
|
|
30
|
+
const AGENT_NAMES = [
|
|
31
|
+
'qa-analisis',
|
|
32
|
+
'qa-plan',
|
|
33
|
+
'qa-asesoria',
|
|
34
|
+
'qa-informe-resultados',
|
|
35
|
+
'qa-reporte-ejecucion',
|
|
36
|
+
'qa-investigador',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const DEFAULT_SPRINT_CYCLE = {
|
|
40
|
+
sprintDurationDays: 8,
|
|
41
|
+
manualTestingTimeboxDays: 2,
|
|
42
|
+
dateFormat: 'dd-mm-aaaa',
|
|
43
|
+
timezone: 'America/Santiago',
|
|
44
|
+
approvalThresholdPercent: 85,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const DEFAULT_TC_ID_PATTERN = 'TC-{MODULE}-{SUBMODULE}-{NUM}';
|
|
48
|
+
const NOT_CONFIGURED = '(no configurado: ver qa/qa-framework.config.json)';
|
|
49
|
+
|
|
50
|
+
const MCP_NAMESPACE_PATTERN = /^mcp__[A-Za-z0-9_-]+__$/;
|
|
51
|
+
const DEFECT_ID_PREFIX_PATTERN = /^[A-Z0-9-]+$/;
|
|
52
|
+
const DECIMAL_SEPARATORS = [',', '.'];
|
|
53
|
+
|
|
54
|
+
// Playwright MCP tools granted to qa-investigador per lane namespace, in this order.
|
|
55
|
+
const BROWSER_TOOLS = [
|
|
56
|
+
'browser_navigate',
|
|
57
|
+
'browser_snapshot',
|
|
58
|
+
'browser_click',
|
|
59
|
+
'browser_type',
|
|
60
|
+
'browser_fill_form',
|
|
61
|
+
'browser_select_option',
|
|
62
|
+
'browser_hover',
|
|
63
|
+
'browser_press_key',
|
|
64
|
+
'browser_wait_for',
|
|
65
|
+
'browser_evaluate',
|
|
66
|
+
'browser_network_requests',
|
|
67
|
+
'browser_console_messages',
|
|
68
|
+
'browser_take_screenshot',
|
|
69
|
+
'browser_find',
|
|
70
|
+
'browser_close',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const INVESTIGATOR_BASE_TOOLS = ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'];
|
|
74
|
+
|
|
75
|
+
const LOCALE_LANGUAGE_LABELS = {
|
|
76
|
+
es: 'espanol',
|
|
77
|
+
'es-cl': 'espanol de Chile',
|
|
78
|
+
'es-mx': 'espanol de Mexico',
|
|
79
|
+
'es-ar': 'espanol de Argentina',
|
|
80
|
+
en: 'English',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Tokens whose value depends on the lane table. They are resolved only for templates
|
|
84
|
+
// that use them, so an invalid lane config fails qa-investigador and nothing else.
|
|
85
|
+
const LANE_TOKENS = ['{{INVESTIGATOR_TOOLS}}', '{{LANE_MCP_TABLE}}', 'HAS_MCP_LANES'];
|
|
86
|
+
|
|
87
|
+
function isSprintCycleEnabled(config) {
|
|
88
|
+
return Boolean(config?.integrations?.azureDevOps?.sprintCycle?.enabled);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveLocaleLanguageLabel(locale) {
|
|
92
|
+
if (!locale) return 'espanol';
|
|
93
|
+
return LOCALE_LANGUAGE_LABELS[locale.toLowerCase()] ?? locale;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isValidApprovalThreshold(value) {
|
|
97
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 && value < 100;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveApprovalThresholdPercent(sprintCycle) {
|
|
101
|
+
const value = sprintCycle.approvalThresholdPercent;
|
|
102
|
+
// Invalid values are reported by `qa-framework validate`; generation keeps the default.
|
|
103
|
+
return isValidApprovalThreshold(value) ? value : DEFAULT_SPRINT_CYCLE.approvalThresholdPercent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatRatio(percent) {
|
|
107
|
+
// Rounded to strip binary float noise (e.g. 57 / 100), always with a dot.
|
|
108
|
+
return String(Number((percent / 100).toFixed(6)));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveDecimalSeparator(config) {
|
|
112
|
+
const explicit = config?.conventions?.decimalSeparator;
|
|
113
|
+
if (DECIMAL_SEPARATORS.includes(explicit)) return explicit;
|
|
114
|
+
const locale = config?.conventions?.locale ?? config?.conventions?.language;
|
|
115
|
+
if (!locale) return ',';
|
|
116
|
+
return String(locale).toLowerCase().startsWith('es') ? ',' : '.';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveDefectIdPrefix(config) {
|
|
120
|
+
const explicit = config?.conventions?.defectIdPrefix;
|
|
121
|
+
if (typeof explicit === 'string' && DEFECT_ID_PREFIX_PATTERN.test(explicit)) return explicit;
|
|
122
|
+
return config?.project?.name ? config.project.name.toUpperCase().replace(/[^A-Z0-9]/g, '-') : 'PROJ';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveProjectField(value) {
|
|
126
|
+
if (typeof value !== 'string' || value.trim() === '' || value.includes('{{')) return NOT_CONFIGURED;
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* MCP-capable lanes in config order: [{ id, mcpNamespace }].
|
|
132
|
+
*
|
|
133
|
+
* Validation is delegated to laneTableFromConfig (the same reader the automation
|
|
134
|
+
* scaffold uses). Order comes from the config array, not from the table object,
|
|
135
|
+
* because numeric-like lane ids would otherwise be reordered by the JS engine.
|
|
136
|
+
* Throws on an invalid lane block or an mcpNamespace that is not a tool prefix.
|
|
137
|
+
*/
|
|
138
|
+
function resolveMcpLanes(config) {
|
|
139
|
+
const table = laneTableFromConfig(config);
|
|
140
|
+
if (table === DEFAULT_LANE_TABLE) return [];
|
|
141
|
+
|
|
142
|
+
const lanes = [];
|
|
143
|
+
for (const [index, entry] of config.parallelLanes.lanes.entries()) {
|
|
144
|
+
const lane = table[entry.id];
|
|
145
|
+
if (!lane || lane.mcpNamespace === null) continue;
|
|
146
|
+
if (!MCP_NAMESPACE_PATTERN.test(lane.mcpNamespace)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`parallelLanes.lanes[${index}].mcpNamespace must match ${MCP_NAMESPACE_PATTERN} ` +
|
|
149
|
+
`(e.g. "mcp__playwright-qauser__"). Got: ${JSON.stringify(lane.mcpNamespace)}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
lanes.push({ id: entry.id, mcpNamespace: lane.mcpNamespace });
|
|
153
|
+
}
|
|
154
|
+
return lanes;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildLanePlaceholders(config) {
|
|
158
|
+
const mcpLanes = resolveMcpLanes(config);
|
|
159
|
+
|
|
160
|
+
const tools = [...INVESTIGATOR_BASE_TOOLS];
|
|
161
|
+
for (const { mcpNamespace } of mcpLanes) {
|
|
162
|
+
for (const tool of BROWSER_TOOLS) tools.push(`${mcpNamespace}${tool}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tableLines = mcpLanes.length
|
|
166
|
+
? [
|
|
167
|
+
'| Lane | Servidor MCP | Prefijo de herramientas |',
|
|
168
|
+
'| --- | --- | --- |',
|
|
169
|
+
...mcpLanes.map(({ id, mcpNamespace }) =>
|
|
170
|
+
`| ${id} | ${mcpNamespace.replace(/^mcp__/, '').replace(/__$/, '')} | \`${mcpNamespace}\` |`
|
|
171
|
+
),
|
|
172
|
+
]
|
|
173
|
+
: [];
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
placeholders: {
|
|
177
|
+
'{{INVESTIGATOR_TOOLS}}': tools.join(', '),
|
|
178
|
+
'{{LANE_MCP_TABLE}}': tableLines.join('\n'),
|
|
179
|
+
},
|
|
180
|
+
flags: { HAS_MCP_LANES: mcpLanes.length > 0 },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildPlaceholders(config) {
|
|
185
|
+
const sprintCycle = { ...DEFAULT_SPRINT_CYCLE, ...(config?.integrations?.azureDevOps?.sprintCycle ?? {}) };
|
|
186
|
+
const project = config?.project ?? {};
|
|
187
|
+
const approvalPercent = resolveApprovalThresholdPercent(sprintCycle);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
'{{PROJECT_NAME}}': project.name ?? '{{PROJECT_NAME}}',
|
|
191
|
+
'{{PROJECT_DISPLAY_NAME}}': project.displayName ?? project.name ?? '{{PROJECT_DISPLAY_NAME}}',
|
|
192
|
+
'{{SPRINT_DURATION_DAYS}}': String(sprintCycle.sprintDurationDays),
|
|
193
|
+
'{{MANUAL_TESTING_TIMEBOX_DAYS}}': String(sprintCycle.manualTestingTimeboxDays),
|
|
194
|
+
'{{DATE_FORMAT}}': sprintCycle.dateFormat,
|
|
195
|
+
'{{TIMEZONE}}': sprintCycle.timezone,
|
|
196
|
+
'{{LOCALE_LANGUAGE_LABEL}}': resolveLocaleLanguageLabel(config?.conventions?.locale ?? config?.conventions?.language),
|
|
197
|
+
'{{DEFECT_ID_PREFIX}}': resolveDefectIdPrefix(config),
|
|
198
|
+
'{{APPROVAL_THRESHOLD_PERCENT}}': String(approvalPercent).replace('.', resolveDecimalSeparator(config)),
|
|
199
|
+
'{{APPROVAL_THRESHOLD_RATIO}}': formatRatio(approvalPercent),
|
|
200
|
+
'{{DECIMAL_SEPARATOR}}': resolveDecimalSeparator(config),
|
|
201
|
+
'{{TC_ID_PATTERN}}': config?.conventions?.testCaseIdPattern || DEFAULT_TC_ID_PATTERN,
|
|
202
|
+
'{{QA_BASE_URL}}': resolveProjectField(project.qaBaseUrl),
|
|
203
|
+
'{{LOGIN_PATH}}': resolveProjectField(project.loginPath),
|
|
204
|
+
'{{TECH_STACK}}': resolveProjectField(project.techStack),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Renders conditional blocks, then placeholders, then guards against leftovers.
|
|
210
|
+
*
|
|
211
|
+
* A block whose opening and closing tags sit on their own lines is removed together
|
|
212
|
+
* with those lines, so dropping a block never leaves an extra blank line behind.
|
|
213
|
+
*/
|
|
214
|
+
function renderTemplate(template, placeholders, flags, agentName) {
|
|
215
|
+
let content = template;
|
|
216
|
+
|
|
217
|
+
for (const [keyword, keepWhen] of [['if', true], ['unless', false]]) {
|
|
218
|
+
const ownLine = new RegExp(
|
|
219
|
+
`^[ \\t]*\\{\\{#${keyword} ([A-Z0-9_]+)\\}\\}[ \\t]*\\r?\\n([\\s\\S]*?)^[ \\t]*\\{\\{\\/${keyword}\\}\\}[ \\t]*(?:\\r?\\n|$)`,
|
|
220
|
+
'gm'
|
|
221
|
+
);
|
|
222
|
+
const inline = new RegExp(`\\{\\{#${keyword} ([A-Z0-9_]+)\\}\\}([\\s\\S]*?)\\{\\{\\/${keyword}\\}\\}`, 'g');
|
|
223
|
+
const choose = (match, flag, body) => (Boolean(flags[flag]) === keepWhen ? body : '');
|
|
224
|
+
content = content.replace(ownLine, choose).replace(inline, choose);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const leftovers = new Set();
|
|
228
|
+
content = content.replace(/\{\{[#/]?[A-Za-z0-9_ ]+\}\}/g, (token) => {
|
|
229
|
+
if (Object.prototype.hasOwnProperty.call(placeholders, token)) return placeholders[token];
|
|
230
|
+
leftovers.add(token);
|
|
231
|
+
return token;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (leftovers.size) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Template for agent ${agentName} has unresolved tokens: ${[...leftovers].join(', ')}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return content;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildAgentContent(agentName, templatesDir, config) {
|
|
243
|
+
const templatePath = path.join(templatesDir, 'agents', `${agentName}.md`);
|
|
244
|
+
try {
|
|
245
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
246
|
+
const placeholders = buildPlaceholders(config);
|
|
247
|
+
let flags = {};
|
|
248
|
+
if (LANE_TOKENS.some((token) => template.includes(token))) {
|
|
249
|
+
const lane = buildLanePlaceholders(config);
|
|
250
|
+
Object.assign(placeholders, lane.placeholders);
|
|
251
|
+
flags = lane.flags;
|
|
252
|
+
}
|
|
253
|
+
return renderTemplate(template, placeholders, flags, agentName);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err.message.startsWith(`Cannot generate agent ${agentName}`)) throw err;
|
|
256
|
+
throw new Error(`Cannot generate agent ${agentName}: ${err.message.replace(/^\[lane-config\] /, '')}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Renders every agent, never letting one failure stop the others.
|
|
262
|
+
* Returns [{ agentName, fileName, content }] or [{ agentName, fileName, error }].
|
|
263
|
+
*/
|
|
264
|
+
function renderAllAgents(templatesDir, config) {
|
|
265
|
+
return AGENT_NAMES.map((agentName) => {
|
|
266
|
+
const fileName = agentFileName(agentName);
|
|
267
|
+
try {
|
|
268
|
+
return { agentName, fileName, content: buildAgentContent(agentName, templatesDir, config) };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return { agentName, fileName, error };
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Present-but-invalid values of the config keys the agents consume.
|
|
277
|
+
* Absent keys are never reported. Returns an array of messages.
|
|
278
|
+
*/
|
|
279
|
+
function validateAgentConfig(config) {
|
|
280
|
+
const errors = [];
|
|
281
|
+
const conventions = config?.conventions ?? {};
|
|
282
|
+
const sprintCycle = config?.integrations?.azureDevOps?.sprintCycle ?? {};
|
|
283
|
+
|
|
284
|
+
if ('approvalThresholdPercent' in sprintCycle && !isValidApprovalThreshold(sprintCycle.approvalThresholdPercent)) {
|
|
285
|
+
errors.push(
|
|
286
|
+
'integrations.azureDevOps.sprintCycle.approvalThresholdPercent must be a number greater than 0 and less than 100. ' +
|
|
287
|
+
`Got: ${JSON.stringify(sprintCycle.approvalThresholdPercent)}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if ('decimalSeparator' in conventions && !DECIMAL_SEPARATORS.includes(conventions.decimalSeparator)) {
|
|
291
|
+
errors.push(`conventions.decimalSeparator must be "," or ".". Got: ${JSON.stringify(conventions.decimalSeparator)}`);
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
'defectIdPrefix' in conventions &&
|
|
295
|
+
(typeof conventions.defectIdPrefix !== 'string' || !DEFECT_ID_PREFIX_PATTERN.test(conventions.defectIdPrefix))
|
|
296
|
+
) {
|
|
297
|
+
errors.push(
|
|
298
|
+
`conventions.defectIdPrefix must match ${DEFECT_ID_PREFIX_PATTERN} (uppercase letters, digits, "-"). ` +
|
|
299
|
+
`Got: ${JSON.stringify(conventions.defectIdPrefix)}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Lanes are only checked when the generator actually consumes them: sprint agents on
|
|
304
|
+
// and a lane block that laneTableFromConfig does not short-circuit (enabled !== false).
|
|
305
|
+
// A disabled block, or a project without sprint agents, never fails validate here.
|
|
306
|
+
const block = config?.parallelLanes;
|
|
307
|
+
const lanesInUse =
|
|
308
|
+
isSprintCycleEnabled(config) && block !== undefined && block !== null && block.enabled !== false;
|
|
309
|
+
if (!lanesInUse) return errors;
|
|
310
|
+
|
|
311
|
+
const lanes = block.lanes;
|
|
312
|
+
if (Array.isArray(lanes)) {
|
|
313
|
+
lanes.forEach((lane, index) => {
|
|
314
|
+
const ns = lane && typeof lane === 'object' ? lane.mcpNamespace : undefined;
|
|
315
|
+
if (ns !== undefined && ns !== null && (typeof ns !== 'string' || !MCP_NAMESPACE_PATTERN.test(ns))) {
|
|
316
|
+
errors.push(
|
|
317
|
+
`parallelLanes.lanes[${index}].mcpNamespace must match ${MCP_NAMESPACE_PATTERN} or be null. Got: ${JSON.stringify(ns)}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
laneTableFromConfig(config);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
errors.push(`parallelLanes: ${err.message.replace(/^\[lane-config\] /, '')}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return errors;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function agentFileName(agentName) {
|
|
332
|
+
return `${agentName}.md`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
AGENT_NAMES,
|
|
337
|
+
BROWSER_TOOLS,
|
|
338
|
+
DEFAULT_SPRINT_CYCLE,
|
|
339
|
+
isSprintCycleEnabled,
|
|
340
|
+
buildPlaceholders,
|
|
341
|
+
resolveMcpLanes,
|
|
342
|
+
renderTemplate,
|
|
343
|
+
buildAgentContent,
|
|
344
|
+
renderAllAgents,
|
|
345
|
+
validateAgentConfig,
|
|
346
|
+
agentFileName,
|
|
347
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/lib/claude-commands.js - Generate .claude/commands/qa-{name}.md thin wrappers
|
|
5
|
+
*
|
|
6
|
+
* Each generated command points back at the corresponding SKILL.md as the single
|
|
7
|
+
* source of truth (no content duplication, no drift). Skills are discovered
|
|
8
|
+
* dynamically from the filesystem, never hardcoded, so a newly added skill
|
|
9
|
+
* under skills/ is picked up automatically by init.js and upgrade.js.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Stage prerequisites mirrored from templates/qa-framework.instructions.md pipeline table.
|
|
16
|
+
// Skills not listed here (e.g. a future addition) still get a valid command with no
|
|
17
|
+
// prerequisite line, and this codebase does not need updating for the command to work.
|
|
18
|
+
const STAGE_PREREQUISITES = {
|
|
19
|
+
'qa-module-analysis': 'Prerequisite: None - this is the first stage of the QA pipeline.',
|
|
20
|
+
'qa-spec-generation': 'Prerequisite: `00-inventory.md` exists.',
|
|
21
|
+
'qa-test-plan': 'Prerequisite: `05-test-scenarios.md` exists.',
|
|
22
|
+
'qa-test-cases': 'Prerequisite: Test plan exists.',
|
|
23
|
+
'qa-automation': 'Prerequisite: Specs approved, no PENDING-CODE.',
|
|
24
|
+
'qa-test-stabilization': 'Prerequisite: Failing or flaky tests exist in `qa/07-automation/e2e/`.',
|
|
25
|
+
'qa-maintenance': 'Prerequisite: Application change delivered.',
|
|
26
|
+
'qa-ado-integration': 'Prerequisite: ADO enabled in `qa/qa-framework.config.json`.',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function buildCommandContent(skillName) {
|
|
30
|
+
const prerequisite = STAGE_PREREQUISITES[skillName];
|
|
31
|
+
const lines = [
|
|
32
|
+
`Read the full skill at \`.github/skills/${skillName}/SKILL.md\` FIRST, then follow its instructions exactly.`,
|
|
33
|
+
];
|
|
34
|
+
if (prerequisite) {
|
|
35
|
+
lines.push('', prerequisite);
|
|
36
|
+
}
|
|
37
|
+
return `${lines.join('\n')}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function discoverSkillNames(skillsSrcDir) {
|
|
41
|
+
if (!fs.existsSync(skillsSrcDir)) return [];
|
|
42
|
+
return fs.readdirSync(skillsSrcDir, { withFileTypes: true })
|
|
43
|
+
.filter((entry) => entry.isDirectory())
|
|
44
|
+
.map((entry) => entry.name)
|
|
45
|
+
.sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function commandFileName(skillName) {
|
|
49
|
+
return `${skillName}.md`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
buildCommandContent,
|
|
54
|
+
discoverSkillNames,
|
|
55
|
+
commandFileName,
|
|
56
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/lib/module-keys.js - the one place that knows how a module or submodule
|
|
5
|
+
* key maps to a folder name.
|
|
6
|
+
*
|
|
7
|
+
* Two layouts coexist on purpose, and they are NOT the same string:
|
|
8
|
+
*
|
|
9
|
+
* specs qa/01-specifications/module-{name}/submodule-{name}/
|
|
10
|
+
* automation qa/07-automation/e2e/tests/{name}/{name}.spec.ts
|
|
11
|
+
*
|
|
12
|
+
* The prefixed form is what every skill under skills/, docs/folder-structure-guide.md
|
|
13
|
+
* and the "specPath" field of qa-framework.config.json declare. The bare form is what
|
|
14
|
+
* "automationPath" declares and what the live consuming projects actually contain.
|
|
15
|
+
* init.js used to write the bare form in both places, so a freshly scaffolded project
|
|
16
|
+
* violated the spec-path convention its own agent instructions impose.
|
|
17
|
+
*
|
|
18
|
+
* Both helpers are idempotent: a config that already spells the key as
|
|
19
|
+
* "module-suppliers" must not become "module-module-suppliers", and a key written
|
|
20
|
+
* bare must not lose a leading segment that merely looks like a prefix.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Prefix `key` with `prefix-` unless it already carries it. */
|
|
24
|
+
function withPrefix(prefix, key) {
|
|
25
|
+
const marker = `${prefix}-`;
|
|
26
|
+
return key.startsWith(marker) ? key : marker + key;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Strip a leading `prefix-` from `key` if present. */
|
|
30
|
+
function withoutPrefix(prefix, key) {
|
|
31
|
+
const marker = `${prefix}-`;
|
|
32
|
+
return key.startsWith(marker) ? key.slice(marker.length) : key;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Folder name for a module's spec tree: module-{name}. */
|
|
36
|
+
const moduleSpecDirName = key => withPrefix('module', key);
|
|
37
|
+
|
|
38
|
+
/** Folder name for a submodule's spec tree: submodule-{name}. */
|
|
39
|
+
const submoduleSpecDirName = key => withPrefix('submodule', key);
|
|
40
|
+
|
|
41
|
+
/** Bare module name used by automation paths and TC ids. */
|
|
42
|
+
const moduleBareName = key => withoutPrefix('module', key);
|
|
43
|
+
|
|
44
|
+
/** Bare submodule name used by automation paths and TC ids. */
|
|
45
|
+
const submoduleBareName = key => withoutPrefix('submodule', key);
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
withPrefix,
|
|
49
|
+
withoutPrefix,
|
|
50
|
+
moduleSpecDirName,
|
|
51
|
+
submoduleSpecDirName,
|
|
52
|
+
moduleBareName,
|
|
53
|
+
submoduleBareName,
|
|
54
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
8
|
+
const pkg = require(path.join(rootDir, 'package.json'));
|
|
9
|
+
const version = pkg.version;
|
|
10
|
+
|
|
11
|
+
const targets = [
|
|
12
|
+
path.join(rootDir, 'README.md'),
|
|
13
|
+
path.join(rootDir, '.github', 'copilot-instructions.md'),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// qa-framework.config.json is published and is the template init.js copies into every
|
|
17
|
+
// consuming project, where its frameworkVersion is injected into {{VERSION}} of the
|
|
18
|
+
// generated agent instructions. Left out of this sync it silently drifted to 1.10.0
|
|
19
|
+
// while the package shipped 1.12.0, so every fresh install told its agent the wrong
|
|
20
|
+
// version. It is patched by field, not by the loose vX.Y.Z pattern used for prose.
|
|
21
|
+
const jsonTargets = [
|
|
22
|
+
{ file: path.join(rootDir, 'qa-framework.config.json'), field: 'frameworkVersion' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// templates/qa-framework.instructions.md and templates/qa-framework.rules.md contain a
|
|
26
|
+
// literal {{VERSION}} placeholder substituted by init.js/upgrade.js in the consumer
|
|
27
|
+
// project at install/upgrade time. That placeholder is NOT a hardcoded version string
|
|
28
|
+
// and must never be touched here.
|
|
29
|
+
|
|
30
|
+
const versionPattern = /v\d+\.\d+\.\d+/;
|
|
31
|
+
|
|
32
|
+
for (const { file: filePath, field } of jsonTargets) {
|
|
33
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
34
|
+
const fieldPattern = new RegExp(
|
|
35
|
+
`("${field}"\\s*:\\s*")\\d+\\.\\d+\\.\\d+(")`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!fieldPattern.test(original)) {
|
|
39
|
+
throw new Error(`sync-version: no "${field}" version field in ${filePath}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const updated = original.replace(fieldPattern, `$1${version}$2`);
|
|
43
|
+
|
|
44
|
+
if (updated !== original) {
|
|
45
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
46
|
+
console.log(`sync-version: updated ${path.relative(rootDir, filePath)} ${field} -> ${version}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const filePath of targets) {
|
|
51
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
52
|
+
|
|
53
|
+
if (!versionPattern.test(original)) {
|
|
54
|
+
throw new Error(`sync-version: no version string found in ${filePath}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const updated = original.replace(versionPattern, `v${version}`);
|
|
58
|
+
|
|
59
|
+
if (updated !== original) {
|
|
60
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
61
|
+
console.log(`sync-version: updated ${path.relative(rootDir, filePath)} -> v${version}`);
|
|
62
|
+
}
|
|
63
|
+
}
|