@keber/qa-framework 1.11.3 → 1.12.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/README.md +2 -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 +35 -7
- package/docs/skills-architecture.md +23 -23
- package/docs/spec-driven-philosophy.md +9 -9
- package/docs/usage-with-agent.md +83 -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 +23 -2
- 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 +55 -7
- package/scripts/lib/claude-agents.js +78 -0
- package/scripts/lib/claude-commands.js +56 -0
- package/scripts/sync-version.js +36 -0
- package/scripts/upgrade.js +356 -20
- package/scripts/validate.js +17 -17
- 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 +234 -0
- package/templates/agents/qa-plan.md +131 -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 +8 -7
- 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,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-spanish-accents.js
|
|
4
|
+
*
|
|
5
|
+
* Fails when a Markdown file whose prose is Spanish carries no accented characters.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: a spec written as "Codigo" while the UI renders "Código" produces a
|
|
8
|
+
* page-object selector that never matches. In one project that cost 24 of 39 failing smoke
|
|
9
|
+
* tests and two wrong mitigations (raising a timeout, forcing HTTP/1.1) before the real
|
|
10
|
+
* cause was found, because the failure presents as a timeout, not as a text mismatch.
|
|
11
|
+
* Accent stripping is a defect, not a house style - and its measured incidence in
|
|
12
|
+
* agent-generated artifacts is high enough that honor-system compliance does not hold.
|
|
13
|
+
*
|
|
14
|
+
* The hard part is not detecting missing accents; it is deciding which files are Spanish.
|
|
15
|
+
* A naive "every .md must contain accents" rule fails every legitimately English artifact
|
|
16
|
+
* the framework ships. Counting Spanish marker words in raw text does not separate them
|
|
17
|
+
* either: an English document about a Spanish-language project quotes enough Spanish paths,
|
|
18
|
+
* identifiers and snippets to cross any absolute threshold that still catches short
|
|
19
|
+
* Spanish files.
|
|
20
|
+
*
|
|
21
|
+
* So this classifies on the density of Spanish function words in PROSE ONLY, after
|
|
22
|
+
* stripping fenced code, inline code, link targets, HTML tags and table pipes. English
|
|
23
|
+
* prose that merely cites Spanish identifiers scores low because those citations live in
|
|
24
|
+
* the stripped regions; real Spanish prose scores high because its function words are
|
|
25
|
+
* spread through the sentences themselves.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node scripts/check-spanish-accents.js <file|dir> [...] check paths
|
|
29
|
+
* node scripts/check-spanish-accents.js --staged check staged .md files
|
|
30
|
+
* node scripts/check-spanish-accents.js --report <paths> print scores, never fail
|
|
31
|
+
*
|
|
32
|
+
* Exit codes: 0 = clean, 1 = at least one Spanish file has no accents, 2 = usage error.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
'use strict';
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const { execFileSync } = require('child_process');
|
|
40
|
+
|
|
41
|
+
// Function words that are unavoidable in real Spanish prose and rare in English text.
|
|
42
|
+
// Deliberately excludes "no", "a", "en", "es" and other tokens that collide with English
|
|
43
|
+
// or with code, and excludes accented forms so a stripped file is not penalised twice.
|
|
44
|
+
const MARKERS = [
|
|
45
|
+
'que', 'para', 'con', 'por', 'los', 'las', 'del', 'una', 'como', 'pero',
|
|
46
|
+
'este', 'esta', 'estos', 'estas', 'sin', 'sobre', 'desde', 'cuando', 'donde',
|
|
47
|
+
'cada', 'todo', 'todos', 'toda', 'todas', 'entre', 'hasta', 'porque', 'segun',
|
|
48
|
+
'debe', 'deben', 'ser', 'son', 'hay', 'muy', 'ya', 'si', 'lo', 'le', 'se',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const ACCENTED = /[áéíóúÁÉÍÓÚñÑüÜ]/;
|
|
52
|
+
|
|
53
|
+
// A file is Spanish when at least this share of its prose words are Spanish markers.
|
|
54
|
+
// Calibrated against this package's own corpus: Spanish agent templates land near 0.13,
|
|
55
|
+
// English skill files and the FRAMEWORK-FIXES reports stay below 0.03.
|
|
56
|
+
const DENSITY_THRESHOLD = 0.07;
|
|
57
|
+
// Below this many prose words the density estimate is too noisy to act on.
|
|
58
|
+
const MIN_PROSE_WORDS = 60;
|
|
59
|
+
|
|
60
|
+
/** Remove every region where Spanish tokens are citations rather than prose. */
|
|
61
|
+
function extractProse(markdown) {
|
|
62
|
+
return markdown
|
|
63
|
+
.replace(/^---\n[\s\S]*?\n---/, ' ') // YAML frontmatter
|
|
64
|
+
.replace(/```[\s\S]*?```/g, ' ') // fenced code
|
|
65
|
+
.replace(/~~~[\s\S]*?~~~/g, ' ') // fenced code, alt syntax
|
|
66
|
+
.replace(/`[^`\n]*`/g, ' ') // inline code
|
|
67
|
+
.replace(/\]\([^)]*\)/g, '] ') // link targets, keep link text
|
|
68
|
+
.replace(/<[^>\n]*>/g, ' ') // HTML tags and <placeholders>
|
|
69
|
+
.replace(/\{\{[^}]*\}\}/g, ' ') // {{TEMPLATE_PLACEHOLDERS}}
|
|
70
|
+
.replace(/\|/g, ' ') // table pipes
|
|
71
|
+
.replace(/^\s{4,}\S.*$/gm, ' ') // indented code blocks
|
|
72
|
+
.replace(/[A-Za-z0-9_.\-]+\/[A-Za-z0-9_./\-]+/g, ' ') // bare paths
|
|
73
|
+
.replace(/[#*_>[\]]/g, ' '); // remaining markdown punctuation
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function score(markdown) {
|
|
77
|
+
const prose = extractProse(markdown);
|
|
78
|
+
const words = prose.toLowerCase().match(/[a-zà-ÿ]+/g) || [];
|
|
79
|
+
const markers = words.filter((w) => MARKERS.includes(w)).length;
|
|
80
|
+
const density = words.length ? markers / words.length : 0;
|
|
81
|
+
return {
|
|
82
|
+
words: words.length,
|
|
83
|
+
markers,
|
|
84
|
+
density,
|
|
85
|
+
isSpanish: words.length >= MIN_PROSE_WORDS && density >= DENSITY_THRESHOLD,
|
|
86
|
+
hasAccents: ACCENTED.test(markdown),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function collect(target) {
|
|
91
|
+
const stat = fs.statSync(target);
|
|
92
|
+
if (stat.isFile()) return target.endsWith('.md') ? [target] : [];
|
|
93
|
+
return fs.readdirSync(target).flatMap((entry) => {
|
|
94
|
+
if (entry === 'node_modules' || entry === '.git') return [];
|
|
95
|
+
return collect(path.join(target, entry));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function stagedMarkdown() {
|
|
100
|
+
const out = execFileSync(
|
|
101
|
+
'git',
|
|
102
|
+
['diff', '--cached', '--name-only', '--diff-filter=ACM', '--', '*.md'],
|
|
103
|
+
{ encoding: 'utf8' }
|
|
104
|
+
);
|
|
105
|
+
return out.split('\n').map((s) => s.trim()).filter((s) => s && fs.existsSync(s));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function main(argv) {
|
|
109
|
+
const reportOnly = argv.includes('--report');
|
|
110
|
+
const args = argv.filter((a) => a !== '--report');
|
|
111
|
+
|
|
112
|
+
let files;
|
|
113
|
+
if (args.includes('--staged')) {
|
|
114
|
+
files = stagedMarkdown();
|
|
115
|
+
} else if (args.length) {
|
|
116
|
+
files = args.flatMap(collect);
|
|
117
|
+
} else {
|
|
118
|
+
console.error('usage: check-spanish-accents.js <file|dir>... | --staged [--report]');
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const failures = [];
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
const result = score(fs.readFileSync(file, 'utf8'));
|
|
125
|
+
if (reportOnly) {
|
|
126
|
+
console.log(
|
|
127
|
+
`${result.isSpanish ? 'ES' : 'en'} density=${result.density.toFixed(3)} ` +
|
|
128
|
+
`words=${String(result.words).padStart(5)} accents=${result.hasAccents ? 'yes' : 'NO '} ${file}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (result.isSpanish && !result.hasAccents) failures.push({ file, ...result });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (reportOnly) return 0;
|
|
135
|
+
|
|
136
|
+
if (failures.length) {
|
|
137
|
+
console.error('\nSpanish Markdown without accented characters:\n');
|
|
138
|
+
for (const f of failures) {
|
|
139
|
+
console.error(` ${f.file}`);
|
|
140
|
+
console.error(
|
|
141
|
+
` ${f.markers} Spanish markers in ${f.words} prose words ` +
|
|
142
|
+
`(density ${f.density.toFixed(3)}), 0 accented characters.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
console.error(
|
|
146
|
+
'\nSpanish prose must carry its accents: a spec saying "Codigo" does not match a UI' +
|
|
147
|
+
'\nthat renders "Código", and the resulting selector failure looks like a timeout.' +
|
|
148
|
+
'\nFix the orthography rather than bypassing this check.\n'
|
|
149
|
+
);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(`check-spanish-accents: ${files.length} file(s) checked, no violations.`);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
|
158
|
+
|
|
159
|
+
module.exports = { score, extractProse, MARKERS, DENSITY_THRESHOLD };
|
package/scripts/cli.js
CHANGED
package/scripts/generate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* scripts/generate.js
|
|
3
|
+
* scripts/generate.js - Generate a qa artifact from a template
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* qa-framework generate <artifact> [options]
|
|
@@ -39,7 +39,7 @@ const outputDir = getArg('--output') ? path.resolve(process.cwd(), getArg
|
|
|
39
39
|
const templateDir = path.resolve(__dirname, '..', 'templates');
|
|
40
40
|
|
|
41
41
|
const ARTIFACTS = {
|
|
42
|
-
spec: null, // special
|
|
42
|
+
spec: null, // special - copies all 6 files
|
|
43
43
|
'test-plan': path.join(templateDir, 'test-plan.md'),
|
|
44
44
|
'test-case': path.join(templateDir, 'test-case.md'),
|
|
45
45
|
'execution-report': path.join(templateDir, 'execution-report.md'),
|
package/scripts/init.js
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
|
+
const { buildCommandContent, discoverSkillNames, commandFileName } = require('./lib/claude-commands');
|
|
19
|
+
const { AGENT_NAMES, isSprintCycleEnabled, buildAgentContent, agentFileName } = require('./lib/claude-agents');
|
|
20
|
+
|
|
18
21
|
// --- Parse args ---
|
|
19
22
|
const args = process.argv.slice(2);
|
|
20
23
|
const configFlag = args.indexOf('--config');
|
|
@@ -126,7 +129,7 @@ if (fs.existsSync(qaReadmeTemplate)) {
|
|
|
126
129
|
// --- qa/memory/INDEX.md ---
|
|
127
130
|
const memoryDir = path.join(qaRoot, 'memory');
|
|
128
131
|
writeIfMissing(path.join(memoryDir, 'INDEX.md'),
|
|
129
|
-
`# Memory Index
|
|
132
|
+
`# Memory Index - ${config.project?.displayName ?? config.project?.name ?? 'Project'}
|
|
130
133
|
|
|
131
134
|
> The agent reads this file first before loading any memory file.
|
|
132
135
|
> Add a row here whenever you create or update a file in this directory.
|
|
@@ -146,7 +149,7 @@ for (const file of ['ci-pipeline-findings.md', 'e2e-stabilization-patterns.md',
|
|
|
146
149
|
|
|
147
150
|
// --- 03-test-cases README (optional directory marker) ---
|
|
148
151
|
writeIfMissing(path.join(qaRoot, '03-test-cases', 'README.md'),
|
|
149
|
-
`# 03-test-cases/
|
|
152
|
+
`# 03-test-cases/ - Optional Standalone Test Cases
|
|
150
153
|
|
|
151
154
|
> **v1.7.0+:** The primary location for test cases (with detailed steps) is now
|
|
152
155
|
> \`qa/02-test-plans/sprints/Sprint-{N}/Plan-de-Pruebas-{project}-Sprint-{N}-{module}.md\`.
|
|
@@ -198,7 +201,7 @@ for (const mod of modules) {
|
|
|
198
201
|
|
|
199
202
|
for (const sub of submodules) {
|
|
200
203
|
const subKey = sub.key ?? sub.name.toLowerCase().replace(/\s+/g, '-');
|
|
201
|
-
const subDir = path.join(qaRoot, moduleKey, subKey);
|
|
204
|
+
const subDir = path.join(qaRoot, '01-specifications', moduleKey, subKey);
|
|
202
205
|
fs.mkdirSync(subDir, { recursive: true });
|
|
203
206
|
|
|
204
207
|
for (const specFile of SPEC_FILES) {
|
|
@@ -245,6 +248,19 @@ for (const file of ['playwright.config.ts', 'global-setup.ts', '.env.example', '
|
|
|
245
248
|
console.log(` [created] ${path.relative(cwd, dest)}`);
|
|
246
249
|
}
|
|
247
250
|
}
|
|
251
|
+
// Lane scripts: the lane lock plus its config reader and global-setup guards. Plain
|
|
252
|
+
// CommonJS so they run as a CLI and stay testable with `node --test` without pulling
|
|
253
|
+
// in a TypeScript test runner.
|
|
254
|
+
const laneScriptsDir = path.join(e2eScaffoldDir, 'scripts');
|
|
255
|
+
fs.mkdirSync(laneScriptsDir, { recursive: true });
|
|
256
|
+
for (const file of ['lane-config.js', 'lane-lock.js', 'global-setup-guards.js']) {
|
|
257
|
+
const dest = path.join(laneScriptsDir, file);
|
|
258
|
+
if (!fs.existsSync(dest)) {
|
|
259
|
+
fs.copyFileSync(path.join(scaffoldSrc, 'scripts', file), dest);
|
|
260
|
+
console.log(` [created] ${path.relative(cwd, dest)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
248
264
|
const fixturesDir = path.join(e2eScaffoldDir, 'fixtures');
|
|
249
265
|
fs.mkdirSync(fixturesDir, { recursive: true });
|
|
250
266
|
for (const file of ['auth.ts', 'test-helpers.ts', 'base.ts']) {
|
|
@@ -283,7 +299,7 @@ const adoDir = path.join(qaRoot, '08-azure-integration');
|
|
|
283
299
|
writeIfMissing(path.join(adoDir, 'README.md'), `# ADO Integration\n\nSee keber/qa-framework integrations/ado-powershell/ for setup instructions.\n`);
|
|
284
300
|
writeIfMissing(path.join(adoDir, 'module-registry.json'), JSON.stringify({ modules: [] }, null, 2));
|
|
285
301
|
|
|
286
|
-
// --- Skills
|
|
302
|
+
// --- Skills -> .github/skills/ ---
|
|
287
303
|
const skillsSrc = path.resolve(__dirname, '..', 'skills');
|
|
288
304
|
const skillsDest = path.join(cwd, '.github', 'skills');
|
|
289
305
|
fs.mkdirSync(skillsDest, { recursive: true });
|
|
@@ -298,7 +314,7 @@ if (fs.existsSync(skillsSrc)) {
|
|
|
298
314
|
}
|
|
299
315
|
}
|
|
300
316
|
|
|
301
|
-
// --- QA structure guide
|
|
317
|
+
// --- QA structure guide -> qa/QA-STRUCTURE-GUIDE.md ---
|
|
302
318
|
const structureGuideSrc = path.resolve(__dirname, '..', 'docs', 'folder-structure-guide.md');
|
|
303
319
|
const structureGuideDest = path.join(qaRoot, 'QA-STRUCTURE-GUIDE.md');
|
|
304
320
|
if (fs.existsSync(structureGuideSrc)) {
|
|
@@ -314,7 +330,33 @@ const copilotInstrContent = fs.readFileSync(instrTemplatePath, 'utf8')
|
|
|
314
330
|
.replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
|
|
315
331
|
writeIfMissing(copilotInstrPath, copilotInstrContent);
|
|
316
332
|
|
|
317
|
-
// ---
|
|
333
|
+
// --- .claude/commands/qa-*.md - thin wrappers pointing at .github/skills/ ---
|
|
334
|
+
const claudeCommandsDest = path.join(cwd, '.claude', 'commands');
|
|
335
|
+
fs.mkdirSync(claudeCommandsDest, { recursive: true });
|
|
336
|
+
for (const skillName of discoverSkillNames(skillsSrc)) {
|
|
337
|
+
writeIfMissing(path.join(claudeCommandsDest, commandFileName(skillName)), buildCommandContent(skillName));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- .claude/rules/qa-framework.md - always-loaded rules (Claude Code equivalent of applyTo: '**') ---
|
|
341
|
+
const claudeRulesPath = path.join(cwd, '.claude', 'rules', 'qa-framework.md');
|
|
342
|
+
const rulesTemplatePath = path.resolve(__dirname, '..', 'templates', 'qa-framework.rules.md');
|
|
343
|
+
const claudeRulesContent = fs.readFileSync(rulesTemplatePath, 'utf8')
|
|
344
|
+
.replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
|
|
345
|
+
writeIfMissing(claudeRulesPath, claudeRulesContent);
|
|
346
|
+
|
|
347
|
+
// --- .claude/agents/qa-*.md - optional ANALISIS/PLAN sprint-cycle mode (Claude Code only) ---
|
|
348
|
+
// Gated by integrations.azureDevOps.sprintCycle.enabled. No .github/ equivalent: Claude
|
|
349
|
+
// Code subagents with per-agent tools/model frontmatter have no Copilot counterpart.
|
|
350
|
+
if (isSprintCycleEnabled(config)) {
|
|
351
|
+
const templatesDir = path.resolve(__dirname, '..', 'templates');
|
|
352
|
+
const claudeAgentsDest = path.join(cwd, '.claude', 'agents');
|
|
353
|
+
fs.mkdirSync(claudeAgentsDest, { recursive: true });
|
|
354
|
+
for (const agentName of AGENT_NAMES) {
|
|
355
|
+
writeIfMissing(path.join(claudeAgentsDest, agentFileName(agentName)), buildAgentContent(agentName, templatesDir, config));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// --- AGENT-NEXT-STEPS.md - readable by the agent after install ---
|
|
318
360
|
const nextStepsContent = `# ✅ @keber/qa-framework installed successfully
|
|
319
361
|
|
|
320
362
|
> This file was generated automatically by the postinstall script.
|
|
@@ -324,6 +366,7 @@ const nextStepsContent = `# ✅ @keber/qa-framework installed successfully
|
|
|
324
366
|
|
|
325
367
|
- \`qa/\` folder structure with spec templates and agent instructions
|
|
326
368
|
- \`.github/instructions/qa-framework.instructions.md\` with QA agent behavior rules (framework-owned, safe to upgrade)
|
|
369
|
+
- \`.claude/rules/qa-framework.md\` and \`.claude/commands/qa-*.md\` - equivalent Claude Code artifacts (framework-owned, safe to upgrade)
|
|
327
370
|
|
|
328
371
|
## Required next steps
|
|
329
372
|
|
|
@@ -362,12 +405,17 @@ const adoQaInstalledFinal = fs.existsSync(path.join(cwd, 'node_modules', '@keber
|
|
|
362
405
|
const azureReporterInstalled = fs.existsSync(path.join(cwd, 'node_modules', '@alex_neo', 'playwright-azure-reporter'));
|
|
363
406
|
|
|
364
407
|
console.log('');
|
|
365
|
-
console.log(' @keber/qa-framework
|
|
408
|
+
console.log(' @keber/qa-framework - scaffold complete');
|
|
366
409
|
console.log(' ----------------------------------------');
|
|
367
410
|
console.log(' Installed:');
|
|
368
411
|
console.log(' qa/ QA directory structure + spec templates');
|
|
369
412
|
console.log(' .github/skills/ QA agent skills (8 stages)');
|
|
370
413
|
console.log(' .github/instructions/ qa-framework.instructions.md');
|
|
414
|
+
console.log(' .claude/commands/ qa-*.md (Claude Code slash commands)');
|
|
415
|
+
console.log(' .claude/rules/ qa-framework.md (Claude Code always-loaded rules)');
|
|
416
|
+
if (isSprintCycleEnabled(config)) {
|
|
417
|
+
console.log(' .claude/agents/ qa-analisis/qa-plan/qa-asesoria/qa-informe-resultados.md (ANALISIS/PLAN mode)');
|
|
418
|
+
}
|
|
371
419
|
console.log('');
|
|
372
420
|
console.log(' Optional integrations:');
|
|
373
421
|
if (playwrightInstalled) {
|
|
@@ -0,0 +1,78 @@
|
|
|
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 config.integrations.azureDevOps.sprintCycle, falling back
|
|
11
|
+
* to neutral defaults when a field (or the whole config section) is absent.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const AGENT_NAMES = ['qa-analisis', 'qa-plan', 'qa-asesoria', 'qa-informe-resultados'];
|
|
18
|
+
|
|
19
|
+
const DEFAULT_SPRINT_CYCLE = {
|
|
20
|
+
sprintDurationDays: 8,
|
|
21
|
+
manualTestingTimeboxDays: 2,
|
|
22
|
+
dateFormat: 'dd-mm-aaaa',
|
|
23
|
+
timezone: 'America/Santiago',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const LOCALE_LANGUAGE_LABELS = {
|
|
27
|
+
es: 'espanol',
|
|
28
|
+
'es-cl': 'espanol de Chile',
|
|
29
|
+
'es-mx': 'espanol de Mexico',
|
|
30
|
+
'es-ar': 'espanol de Argentina',
|
|
31
|
+
en: 'English',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function isSprintCycleEnabled(config) {
|
|
35
|
+
return Boolean(config?.integrations?.azureDevOps?.sprintCycle?.enabled);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveLocaleLanguageLabel(locale) {
|
|
39
|
+
if (!locale) return 'espanol';
|
|
40
|
+
return LOCALE_LANGUAGE_LABELS[locale.toLowerCase()] ?? locale;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildPlaceholders(config) {
|
|
44
|
+
const sprintCycle = { ...DEFAULT_SPRINT_CYCLE, ...(config?.integrations?.azureDevOps?.sprintCycle ?? {}) };
|
|
45
|
+
const project = config?.project ?? {};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
'{{PROJECT_NAME}}': project.name ?? '{{PROJECT_NAME}}',
|
|
49
|
+
'{{PROJECT_DISPLAY_NAME}}': project.displayName ?? project.name ?? '{{PROJECT_DISPLAY_NAME}}',
|
|
50
|
+
'{{SPRINT_DURATION_DAYS}}': String(sprintCycle.sprintDurationDays),
|
|
51
|
+
'{{MANUAL_TESTING_TIMEBOX_DAYS}}': String(sprintCycle.manualTestingTimeboxDays),
|
|
52
|
+
'{{DATE_FORMAT}}': sprintCycle.dateFormat,
|
|
53
|
+
'{{TIMEZONE}}': sprintCycle.timezone,
|
|
54
|
+
'{{LOCALE_LANGUAGE_LABEL}}': resolveLocaleLanguageLabel(config?.conventions?.locale ?? config?.conventions?.language),
|
|
55
|
+
'{{DEFECT_ID_PREFIX}}': config?.project?.name ? config.project.name.toUpperCase().replace(/[^A-Z0-9]/g, '-') : 'PROJ',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildAgentContent(agentName, templatesDir, config) {
|
|
60
|
+
const templatePath = path.join(templatesDir, 'agents', `${agentName}.md`);
|
|
61
|
+
let content = fs.readFileSync(templatePath, 'utf8');
|
|
62
|
+
const placeholders = buildPlaceholders(config);
|
|
63
|
+
for (const [placeholder, value] of Object.entries(placeholders)) {
|
|
64
|
+
content = content.split(placeholder).join(value);
|
|
65
|
+
}
|
|
66
|
+
return content;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function agentFileName(agentName) {
|
|
70
|
+
return `${agentName}.md`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
AGENT_NAMES,
|
|
75
|
+
isSprintCycleEnabled,
|
|
76
|
+
buildAgentContent,
|
|
77
|
+
agentFileName,
|
|
78
|
+
};
|
|
@@ -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,36 @@
|
|
|
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
|
+
// templates/qa-framework.instructions.md and templates/qa-framework.rules.md contain a
|
|
17
|
+
// literal {{VERSION}} placeholder substituted by init.js/upgrade.js in the consumer
|
|
18
|
+
// project at install/upgrade time. That placeholder is NOT a hardcoded version string
|
|
19
|
+
// and must never be touched here.
|
|
20
|
+
|
|
21
|
+
const versionPattern = /v\d+\.\d+\.\d+/;
|
|
22
|
+
|
|
23
|
+
for (const filePath of targets) {
|
|
24
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
25
|
+
|
|
26
|
+
if (!versionPattern.test(original)) {
|
|
27
|
+
throw new Error(`sync-version: no version string found in ${filePath}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const updated = original.replace(versionPattern, `v${version}`);
|
|
31
|
+
|
|
32
|
+
if (updated !== original) {
|
|
33
|
+
fs.writeFileSync(filePath, updated, 'utf8');
|
|
34
|
+
console.log(`sync-version: updated ${path.relative(rootDir, filePath)} -> v${version}`);
|
|
35
|
+
}
|
|
36
|
+
}
|