@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,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-forbidden-chars.js
|
|
4
|
+
*
|
|
5
|
+
* Enforces the BLOCKING character-safety rule declared in
|
|
6
|
+
* .github/copilot-instructions.md: generated content must never carry typographic
|
|
7
|
+
* characters that survive a copy-paste but break on the way to a terminal, a CSV,
|
|
8
|
+
* a PowerShell script or an Azure DevOps field.
|
|
9
|
+
*
|
|
10
|
+
* Forbidden: em-dash (U+2014), en-dash (U+2013), horizontal ellipsis (U+2026),
|
|
11
|
+
* smart quotes (U+201C, U+201D, U+2018, U+2019) and arrows (U+2190-U+21FF).
|
|
12
|
+
* Each has an ASCII equivalent that renders identically everywhere.
|
|
13
|
+
*
|
|
14
|
+
* Latin Extended (U+00C0-U+024F) is explicitly NOT flagged: Spanish accents,
|
|
15
|
+
* n with tilde and u/o with umlaut are required orthography, not decoration.
|
|
16
|
+
* Emoji are not flagged either - they are outside the forbidden set.
|
|
17
|
+
*
|
|
18
|
+
* Unlike check-spanish-accents.js this scans .js and .ts as well as .md, because
|
|
19
|
+
* the generators under scripts/ embed these characters in template literals and
|
|
20
|
+
* console output that propagate into every consuming project.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node scripts/check-forbidden-chars.js <file|dir> [...] check paths
|
|
24
|
+
* node scripts/check-forbidden-chars.js --staged check staged files
|
|
25
|
+
* node scripts/check-forbidden-chars.js --report <paths> print findings, never fail
|
|
26
|
+
*
|
|
27
|
+
* Exit codes: 0 = clean, 1 = at least one forbidden character, 2 = usage error.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const { execFileSync } = require('child_process');
|
|
35
|
+
|
|
36
|
+
// '.example' covers .env.example, which ships to consuming projects like any other
|
|
37
|
+
// template. A BOM or an em-dash there is not cosmetic: some .env parsers read the
|
|
38
|
+
// first key as QA_BASE_URL and silently fail to find it.
|
|
39
|
+
const EXTENSIONS = ['.md', '.js', '.ts', '.ps1', '.example'];
|
|
40
|
+
|
|
41
|
+
// Code point -> { name, replacement }. Arrows are handled by range below.
|
|
42
|
+
const FORBIDDEN = {
|
|
43
|
+
0x2014: { name: 'em dash', replacement: ' - ' },
|
|
44
|
+
0x2013: { name: 'en dash', replacement: ' - ' },
|
|
45
|
+
0x2026: { name: 'horizontal ellipsis', replacement: '...' },
|
|
46
|
+
0x201c: { name: 'left double quotation mark', replacement: '"' },
|
|
47
|
+
0x201d: { name: 'right double quotation mark', replacement: '"' },
|
|
48
|
+
0x2018: { name: 'left single quotation mark', replacement: "'" },
|
|
49
|
+
0x2019: { name: 'right single quotation mark', replacement: "'" },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ARROW_START = 0x2190;
|
|
53
|
+
const ARROW_END = 0x21ff;
|
|
54
|
+
|
|
55
|
+
// The two arrows with an obvious ASCII spelling; every other arrow in the block
|
|
56
|
+
// has no single equivalent, so the report asks for a rewrite instead of guessing.
|
|
57
|
+
const ARROW_REPLACEMENTS = {
|
|
58
|
+
0x2190: '<-',
|
|
59
|
+
0x2192: '->',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function describe(codePoint) {
|
|
63
|
+
if (FORBIDDEN[codePoint]) return FORBIDDEN[codePoint];
|
|
64
|
+
if (codePoint >= ARROW_START && codePoint <= ARROW_END) {
|
|
65
|
+
return {
|
|
66
|
+
name: 'arrow',
|
|
67
|
+
replacement: ARROW_REPLACEMENTS[codePoint] || 'an ASCII arrow such as -> or <-',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hex(codePoint) {
|
|
74
|
+
return 'U+' + codePoint.toString(16).toUpperCase().padStart(4, '0');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Scan one file's text. Returns an array of findings; column is 1-based and
|
|
79
|
+
* counted in code points, so an astral character earlier on the line does not
|
|
80
|
+
* shift the reported position.
|
|
81
|
+
*/
|
|
82
|
+
function scanText(text, file) {
|
|
83
|
+
const findings = [];
|
|
84
|
+
const lines = text.split(/\r?\n/);
|
|
85
|
+
|
|
86
|
+
lines.forEach((line, lineIndex) => {
|
|
87
|
+
let column = 0;
|
|
88
|
+
for (const char of line) {
|
|
89
|
+
column += 1;
|
|
90
|
+
const codePoint = char.codePointAt(0);
|
|
91
|
+
const match = describe(codePoint);
|
|
92
|
+
if (!match) continue;
|
|
93
|
+
findings.push({
|
|
94
|
+
file,
|
|
95
|
+
line: lineIndex + 1,
|
|
96
|
+
column,
|
|
97
|
+
char,
|
|
98
|
+
codePoint,
|
|
99
|
+
name: match.name,
|
|
100
|
+
replacement: match.replacement,
|
|
101
|
+
text: line.trim(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return findings;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function scanFile(file) {
|
|
110
|
+
return scanText(fs.readFileSync(file, 'utf8'), file);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Archived copies of content this framework shipped in an earlier release. Their bytes
|
|
114
|
+
// are the point: test/upgrade.test.js matches them against SHIPPED_SCAFFOLD_HASHES to
|
|
115
|
+
// prove a real legacy installation is recognised as pristine. Rewriting a character in
|
|
116
|
+
// one of them changes its hash and silently voids that proof, so they are read-only
|
|
117
|
+
// history rather than authored content and the rule does not apply to them.
|
|
118
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'legacy-v1.11.3']);
|
|
119
|
+
|
|
120
|
+
function collect(target) {
|
|
121
|
+
const stat = fs.statSync(target);
|
|
122
|
+
if (stat.isFile()) return EXTENSIONS.includes(path.extname(target)) ? [target] : [];
|
|
123
|
+
return fs.readdirSync(target).flatMap((entry) => {
|
|
124
|
+
if (SKIP_DIRS.has(entry)) return [];
|
|
125
|
+
return collect(path.join(target, entry));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stagedFiles() {
|
|
130
|
+
const out = execFileSync(
|
|
131
|
+
'git',
|
|
132
|
+
['diff', '--cached', '--name-only', '--diff-filter=ACM'],
|
|
133
|
+
{ encoding: 'utf8' }
|
|
134
|
+
);
|
|
135
|
+
return out
|
|
136
|
+
.split('\n')
|
|
137
|
+
.map((s) => s.trim())
|
|
138
|
+
.filter((s) => s && EXTENSIONS.includes(path.extname(s)) && fs.existsSync(s));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function main(argv) {
|
|
142
|
+
const reportOnly = argv.includes('--report');
|
|
143
|
+
const args = argv.filter((a) => a !== '--report');
|
|
144
|
+
|
|
145
|
+
let files;
|
|
146
|
+
if (args.includes('--staged')) {
|
|
147
|
+
files = stagedFiles();
|
|
148
|
+
} else if (args.length) {
|
|
149
|
+
files = args.flatMap(collect);
|
|
150
|
+
} else {
|
|
151
|
+
console.error('usage: check-forbidden-chars.js <file|dir>... | --staged [--report]');
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const findings = files.flatMap(scanFile);
|
|
156
|
+
|
|
157
|
+
if (findings.length) {
|
|
158
|
+
const stream = reportOnly ? console.log : console.error;
|
|
159
|
+
stream(`\nForbidden characters (${findings.length} occurrence(s)):\n`);
|
|
160
|
+
for (const f of findings) {
|
|
161
|
+
stream(` ${f.file}:${f.line}:${f.column} "${f.char}" ${hex(f.codePoint)} ${f.name}`);
|
|
162
|
+
stream(` use ${f.replacement}`);
|
|
163
|
+
stream(` ${f.text}`);
|
|
164
|
+
}
|
|
165
|
+
if (!reportOnly) {
|
|
166
|
+
stream(
|
|
167
|
+
'\nThese characters break on the way to a terminal, a CSV export or an Azure' +
|
|
168
|
+
'\nDevOps field. Replace them with their ASCII equivalents rather than' +
|
|
169
|
+
'\nbypassing this check. Spanish accents are permitted and are never flagged.\n'
|
|
170
|
+
);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (reportOnly) {
|
|
177
|
+
console.log(`check-forbidden-chars: ${files.length} file(s) checked, no findings.`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(`check-forbidden-chars: ${files.length} file(s) checked, no violations.`);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
|
186
|
+
|
|
187
|
+
module.exports = { scanText, scanFile, main, EXTENSIONS, FORBIDDEN, ARROW_START, ARROW_END };
|
|
@@ -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, renderAllAgents } = 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\`.
|
|
@@ -192,13 +195,26 @@ const SPEC_FILES = [
|
|
|
192
195
|
|
|
193
196
|
const templateDir = path.resolve(__dirname, '..', 'templates', 'specification');
|
|
194
197
|
|
|
198
|
+
// Spec folders carry the module-/submodule- prefix; automation folders and TC ids
|
|
199
|
+
// use the bare key. See scripts/lib/module-keys.js for why the two differ.
|
|
200
|
+
const {
|
|
201
|
+
moduleSpecDirName,
|
|
202
|
+
submoduleSpecDirName,
|
|
203
|
+
moduleBareName,
|
|
204
|
+
submoduleBareName,
|
|
205
|
+
} = require('./lib/module-keys');
|
|
206
|
+
|
|
195
207
|
for (const mod of modules) {
|
|
196
208
|
const moduleKey = mod.key ?? mod.name.toLowerCase().replace(/\s+/g, '-');
|
|
209
|
+
const moduleDirName = moduleSpecDirName(moduleKey);
|
|
210
|
+
const moduleName = moduleBareName(moduleKey);
|
|
197
211
|
const submodules = mod.submodules ?? [];
|
|
198
212
|
|
|
199
213
|
for (const sub of submodules) {
|
|
200
214
|
const subKey = sub.key ?? sub.name.toLowerCase().replace(/\s+/g, '-');
|
|
201
|
-
const
|
|
215
|
+
const subDirName = submoduleSpecDirName(subKey);
|
|
216
|
+
const subName = submoduleBareName(subKey);
|
|
217
|
+
const subDir = path.join(qaRoot, '01-specifications', moduleDirName, subDirName);
|
|
202
218
|
fs.mkdirSync(subDir, { recursive: true });
|
|
203
219
|
|
|
204
220
|
for (const specFile of SPEC_FILES) {
|
|
@@ -210,8 +226,8 @@ for (const mod of modules) {
|
|
|
210
226
|
content = content
|
|
211
227
|
.replace(/\{\{MODULE_NAME\}\}/g, mod.name)
|
|
212
228
|
.replace(/\{\{SUBMODULE_NAME\}\}/g, sub.name)
|
|
213
|
-
.replace(/\{\{MODULE\}\}/g,
|
|
214
|
-
.replace(/\{\{SUB\}\}/g,
|
|
229
|
+
.replace(/\{\{MODULE\}\}/g, moduleName.toUpperCase())
|
|
230
|
+
.replace(/\{\{SUB\}\}/g, subName.toUpperCase());
|
|
215
231
|
fs.writeFileSync(dest, content, 'utf8');
|
|
216
232
|
} else {
|
|
217
233
|
fs.writeFileSync(dest, `# ${specFile}\n\n> Auto-generated placeholder\n`, 'utf8');
|
|
@@ -223,11 +239,11 @@ for (const mod of modules) {
|
|
|
223
239
|
}
|
|
224
240
|
|
|
225
241
|
// Create automation spec stub in e2e/tests/{module}/
|
|
226
|
-
const e2eDir = path.join(qaRoot, '07-automation', 'e2e', 'tests',
|
|
242
|
+
const e2eDir = path.join(qaRoot, '07-automation', 'e2e', 'tests', moduleName);
|
|
227
243
|
fs.mkdirSync(e2eDir, { recursive: true });
|
|
228
|
-
const specTs = path.join(e2eDir, `${
|
|
244
|
+
const specTs = path.join(e2eDir, `${subName}.spec.ts`);
|
|
229
245
|
if (!fs.existsSync(specTs)) {
|
|
230
|
-
fs.writeFileSync(specTs, specScaffold(mod.name, sub.name,
|
|
246
|
+
fs.writeFileSync(specTs, specScaffold(mod.name, sub.name, moduleName, subName), 'utf8');
|
|
231
247
|
console.log(` [created] ${path.relative(cwd, specTs)}`);
|
|
232
248
|
}
|
|
233
249
|
}
|
|
@@ -245,6 +261,19 @@ for (const file of ['playwright.config.ts', 'global-setup.ts', '.env.example', '
|
|
|
245
261
|
console.log(` [created] ${path.relative(cwd, dest)}`);
|
|
246
262
|
}
|
|
247
263
|
}
|
|
264
|
+
// Lane scripts: the lane lock plus its config reader and global-setup guards. Plain
|
|
265
|
+
// CommonJS so they run as a CLI and stay testable with `node --test` without pulling
|
|
266
|
+
// in a TypeScript test runner.
|
|
267
|
+
const laneScriptsDir = path.join(e2eScaffoldDir, 'scripts');
|
|
268
|
+
fs.mkdirSync(laneScriptsDir, { recursive: true });
|
|
269
|
+
for (const file of ['lane-config.js', 'lane-lock.js', 'global-setup-guards.js']) {
|
|
270
|
+
const dest = path.join(laneScriptsDir, file);
|
|
271
|
+
if (!fs.existsSync(dest)) {
|
|
272
|
+
fs.copyFileSync(path.join(scaffoldSrc, 'scripts', file), dest);
|
|
273
|
+
console.log(` [created] ${path.relative(cwd, dest)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
248
277
|
const fixturesDir = path.join(e2eScaffoldDir, 'fixtures');
|
|
249
278
|
fs.mkdirSync(fixturesDir, { recursive: true });
|
|
250
279
|
for (const file of ['auth.ts', 'test-helpers.ts', 'base.ts']) {
|
|
@@ -283,7 +312,7 @@ const adoDir = path.join(qaRoot, '08-azure-integration');
|
|
|
283
312
|
writeIfMissing(path.join(adoDir, 'README.md'), `# ADO Integration\n\nSee keber/qa-framework integrations/ado-powershell/ for setup instructions.\n`);
|
|
284
313
|
writeIfMissing(path.join(adoDir, 'module-registry.json'), JSON.stringify({ modules: [] }, null, 2));
|
|
285
314
|
|
|
286
|
-
// --- Skills
|
|
315
|
+
// --- Skills -> .github/skills/ ---
|
|
287
316
|
const skillsSrc = path.resolve(__dirname, '..', 'skills');
|
|
288
317
|
const skillsDest = path.join(cwd, '.github', 'skills');
|
|
289
318
|
fs.mkdirSync(skillsDest, { recursive: true });
|
|
@@ -298,7 +327,7 @@ if (fs.existsSync(skillsSrc)) {
|
|
|
298
327
|
}
|
|
299
328
|
}
|
|
300
329
|
|
|
301
|
-
// --- QA structure guide
|
|
330
|
+
// --- QA structure guide -> qa/QA-STRUCTURE-GUIDE.md ---
|
|
302
331
|
const structureGuideSrc = path.resolve(__dirname, '..', 'docs', 'folder-structure-guide.md');
|
|
303
332
|
const structureGuideDest = path.join(qaRoot, 'QA-STRUCTURE-GUIDE.md');
|
|
304
333
|
if (fs.existsSync(structureGuideSrc)) {
|
|
@@ -314,7 +343,40 @@ const copilotInstrContent = fs.readFileSync(instrTemplatePath, 'utf8')
|
|
|
314
343
|
.replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
|
|
315
344
|
writeIfMissing(copilotInstrPath, copilotInstrContent);
|
|
316
345
|
|
|
317
|
-
// ---
|
|
346
|
+
// --- .claude/commands/qa-*.md - thin wrappers pointing at .github/skills/ ---
|
|
347
|
+
const claudeCommandsDest = path.join(cwd, '.claude', 'commands');
|
|
348
|
+
fs.mkdirSync(claudeCommandsDest, { recursive: true });
|
|
349
|
+
for (const skillName of discoverSkillNames(skillsSrc)) {
|
|
350
|
+
writeIfMissing(path.join(claudeCommandsDest, commandFileName(skillName)), buildCommandContent(skillName));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// --- .claude/rules/qa-framework.md - always-loaded rules (Claude Code equivalent of applyTo: '**') ---
|
|
354
|
+
const claudeRulesPath = path.join(cwd, '.claude', 'rules', 'qa-framework.md');
|
|
355
|
+
const rulesTemplatePath = path.resolve(__dirname, '..', 'templates', 'qa-framework.rules.md');
|
|
356
|
+
const claudeRulesContent = fs.readFileSync(rulesTemplatePath, 'utf8')
|
|
357
|
+
.replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
|
|
358
|
+
writeIfMissing(claudeRulesPath, claudeRulesContent);
|
|
359
|
+
|
|
360
|
+
// --- .claude/agents/qa-*.md - optional ANALISIS/PLAN sprint-cycle mode (Claude Code only) ---
|
|
361
|
+
// Gated by integrations.azureDevOps.sprintCycle.enabled. No .github/ equivalent: Claude
|
|
362
|
+
// Code subagents with per-agent tools/model frontmatter have no Copilot counterpart.
|
|
363
|
+
// Each agent is rendered independently: an invalid lane config fails qa-investigador
|
|
364
|
+
// only, the other agents are still written, and init exits 1 at the end.
|
|
365
|
+
const agentFailures = [];
|
|
366
|
+
if (isSprintCycleEnabled(config)) {
|
|
367
|
+
const templatesDir = path.resolve(__dirname, '..', 'templates');
|
|
368
|
+
const claudeAgentsDest = path.join(cwd, '.claude', 'agents');
|
|
369
|
+
fs.mkdirSync(claudeAgentsDest, { recursive: true });
|
|
370
|
+
for (const agent of renderAllAgents(templatesDir, config)) {
|
|
371
|
+
if (agent.error) {
|
|
372
|
+
agentFailures.push(agent);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
writeIfMissing(path.join(claudeAgentsDest, agent.fileName), agent.content);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// --- AGENT-NEXT-STEPS.md - readable by the agent after install ---
|
|
318
380
|
const nextStepsContent = `# ✅ @keber/qa-framework installed successfully
|
|
319
381
|
|
|
320
382
|
> This file was generated automatically by the postinstall script.
|
|
@@ -324,6 +386,7 @@ const nextStepsContent = `# ✅ @keber/qa-framework installed successfully
|
|
|
324
386
|
|
|
325
387
|
- \`qa/\` folder structure with spec templates and agent instructions
|
|
326
388
|
- \`.github/instructions/qa-framework.instructions.md\` with QA agent behavior rules (framework-owned, safe to upgrade)
|
|
389
|
+
- \`.claude/rules/qa-framework.md\` and \`.claude/commands/qa-*.md\` - equivalent Claude Code artifacts (framework-owned, safe to upgrade)
|
|
327
390
|
|
|
328
391
|
## Required next steps
|
|
329
392
|
|
|
@@ -362,12 +425,21 @@ const adoQaInstalledFinal = fs.existsSync(path.join(cwd, 'node_modules', '@keber
|
|
|
362
425
|
const azureReporterInstalled = fs.existsSync(path.join(cwd, 'node_modules', '@alex_neo', 'playwright-azure-reporter'));
|
|
363
426
|
|
|
364
427
|
console.log('');
|
|
365
|
-
console.log(' @keber/qa-framework
|
|
428
|
+
console.log(' @keber/qa-framework - scaffold complete');
|
|
366
429
|
console.log(' ----------------------------------------');
|
|
367
430
|
console.log(' Installed:');
|
|
368
431
|
console.log(' qa/ QA directory structure + spec templates');
|
|
369
432
|
console.log(' .github/skills/ QA agent skills (8 stages)');
|
|
370
433
|
console.log(' .github/instructions/ qa-framework.instructions.md');
|
|
434
|
+
console.log(' .claude/commands/ qa-*.md (Claude Code slash commands)');
|
|
435
|
+
console.log(' .claude/rules/ qa-framework.md (Claude Code always-loaded rules)');
|
|
436
|
+
if (isSprintCycleEnabled(config)) {
|
|
437
|
+
const failedAgents = new Set(agentFailures.map((a) => a.agentName));
|
|
438
|
+
const generatedAgents = AGENT_NAMES.filter((name) => !failedAgents.has(name));
|
|
439
|
+
if (generatedAgents.length) {
|
|
440
|
+
console.log(` .claude/agents/ ${generatedAgents.join('/')}.md (ANALISIS/PLAN mode)`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
371
443
|
console.log('');
|
|
372
444
|
console.log(' Optional integrations:');
|
|
373
445
|
if (playwrightInstalled) {
|
|
@@ -390,6 +462,23 @@ console.log('');
|
|
|
390
462
|
console.log(' Next: edit qa/qa-framework.config.json, then read qa/AGENT-NEXT-STEPS.md');
|
|
391
463
|
console.log('');
|
|
392
464
|
|
|
465
|
+
if (agentFailures.length) {
|
|
466
|
+
if (skipIfExists) {
|
|
467
|
+
// Postinstall mode: a non-zero exit would make `npm install` of this package fail,
|
|
468
|
+
// which blocks the user from ever reaching the fix. Report loudly, exit 0.
|
|
469
|
+
for (const { agentName, fileName, error } of agentFailures) {
|
|
470
|
+
console.error(`[qa-framework/init] WARNING: agent ${agentName} (.claude/agents/${fileName}) was not generated. ${error.message}`);
|
|
471
|
+
}
|
|
472
|
+
console.error('[qa-framework/init] WARNING: fix qa-framework.config.json (run "npx qa-framework validate"), then run "npx qa-framework upgrade" to generate the missing agent.');
|
|
473
|
+
} else {
|
|
474
|
+
for (const { fileName, error } of agentFailures) {
|
|
475
|
+
console.error(`[qa-framework/init] ERROR: .claude/agents/${fileName} was not generated. ${error.message}`);
|
|
476
|
+
}
|
|
477
|
+
console.error('[qa-framework/init] Fix qa-framework.config.json (run "qa-framework validate") and run init or upgrade again.');
|
|
478
|
+
process.exitCode = 1;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
393
482
|
// -----------------------------------------------------------------------
|
|
394
483
|
// Helpers
|
|
395
484
|
// -----------------------------------------------------------------------
|