@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.
Files changed (109) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +42 -2
  3. package/docs/architecture.md +30 -26
  4. package/docs/decisions/lane-heartbeat-caller.md +86 -0
  5. package/docs/final-report.md +23 -23
  6. package/docs/folder-structure-guide.md +100 -80
  7. package/docs/installation.md +42 -7
  8. package/docs/skills-architecture.md +23 -23
  9. package/docs/spec-driven-philosophy.md +9 -9
  10. package/docs/usage-with-agent.md +88 -9
  11. package/docs/visuals/diag-01-ejecutivo-1.png +0 -0
  12. package/docs/visuals/diag-02-tactico-1.png +0 -0
  13. package/docs/visuals/diag-03-tecnico-1.png +0 -0
  14. package/docs/visuals/info-01-ejecutivo-1.png +0 -0
  15. package/docs/visuals/info-02-tactico-1.png +0 -0
  16. package/docs/visuals/info-03-tecnico-1.png +0 -0
  17. package/docs/visuals/pres-01-ejecutivo-1.png +0 -0
  18. package/docs/visuals/pres-02-tactico-1.png +0 -0
  19. package/docs/visuals/pres-03-tecnico-1.png +0 -0
  20. package/docs/visuals/qa-01-problema-1.png +0 -0
  21. package/docs/visuals/qa-02-que-es-1.png +0 -0
  22. package/docs/visuals/qa-03-pipeline-1.png +0 -0
  23. package/docs/visuals/qa-04-6-archivos-1.png +0 -0
  24. package/docs/visuals/qa-05-humano-1.png +0 -0
  25. package/docs/visuals/qa-06-agente-1.png +0 -0
  26. package/docs/visuals/qa-07-skills-arch-1.png +0 -0
  27. package/docs/visuals/qa-08-traceability-1.png +0 -0
  28. package/docs/visuals/qa-en-01-problem-1.png +0 -0
  29. package/docs/visuals/qa-en-02-what-is-1.png +0 -0
  30. package/docs/visuals/qa-en-03-pipeline-1.png +0 -0
  31. package/docs/visuals/qa-en-04-6files-1.png +0 -0
  32. package/docs/visuals/qa-en-05-human-1.png +0 -0
  33. package/docs/visuals/qa-en-06-agent-1.png +0 -0
  34. package/docs/visuals/qa-en-07-skills-arch-1.png +0 -0
  35. package/docs/visuals/qa-en-08-traceability-1.png +0 -0
  36. package/examples/module-example/suppliers/00-inventory.md +1 -1
  37. package/examples/module-example/suppliers/suppliers-create.spec.ts +8 -8
  38. package/integrations/ado-powershell/README.md +1 -1
  39. package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
  40. package/integrations/playwright/README.md +4 -4
  41. package/package.json +7 -3
  42. package/qa-framework.config.json +25 -3
  43. package/scripts/check-forbidden-chars.js +187 -0
  44. package/scripts/check-spanish-accents.js +159 -0
  45. package/scripts/cli.js +1 -1
  46. package/scripts/generate.js +2 -2
  47. package/scripts/init.js +101 -12
  48. package/scripts/lib/claude-agents.js +347 -0
  49. package/scripts/lib/claude-commands.js +56 -0
  50. package/scripts/lib/module-keys.js +54 -0
  51. package/scripts/sync-version.js +63 -0
  52. package/scripts/upgrade.js +369 -20
  53. package/scripts/validate.js +46 -21
  54. package/skills/qa-ado-integration/SKILL.md +19 -19
  55. package/skills/qa-ado-integration/references/scripts-and-config.md +6 -6
  56. package/skills/qa-automation/SKILL.md +49 -27
  57. package/skills/qa-automation/references/config-checklist.md +4 -4
  58. package/skills/qa-automation/references/dom-inspection-template.js +7 -7
  59. package/skills/qa-automation/references/patterns.md +20 -20
  60. package/skills/qa-automation/references/pom-template.md +161 -0
  61. package/skills/qa-maintenance/SKILL.md +11 -11
  62. package/skills/qa-maintenance/references/update-rules.md +8 -8
  63. package/skills/qa-module-analysis/SKILL.md +20 -19
  64. package/skills/qa-module-analysis/references/exploration-checklist.md +4 -4
  65. package/skills/qa-module-analysis/references/output-verification.md +69 -0
  66. package/skills/qa-module-analysis/references/spec-file-formats.md +14 -14
  67. package/skills/qa-spec-generation/SKILL.md +13 -7
  68. package/skills/qa-test-cases/SKILL.md +11 -81
  69. package/skills/qa-test-cases/references/test-case-template.md +5 -5
  70. package/skills/qa-test-plan/SKILL.md +22 -86
  71. package/skills/qa-test-plan/references/plan-de-pruebas-template.md +24 -24
  72. package/skills/qa-test-stabilization/SKILL.md +59 -30
  73. package/skills/qa-test-stabilization/references/classification-protocol.md +35 -35
  74. package/templates/agent-next-steps.md +10 -10
  75. package/templates/agents/qa-analisis.md +142 -0
  76. package/templates/agents/qa-asesoria.md +48 -0
  77. package/templates/agents/qa-informe-resultados.md +236 -0
  78. package/templates/agents/qa-investigador.md +143 -0
  79. package/templates/agents/qa-plan.md +131 -0
  80. package/templates/agents/qa-reporte-ejecucion.md +138 -0
  81. package/templates/automation-scaffold/.env.example +36 -6
  82. package/templates/automation-scaffold/fixtures/auth.ts +27 -5
  83. package/templates/automation-scaffold/fixtures/base.ts +23 -1
  84. package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
  85. package/templates/automation-scaffold/global-setup.ts +117 -48
  86. package/templates/automation-scaffold/playwright.config.ts +72 -32
  87. package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
  88. package/templates/automation-scaffold/scripts/lane-config.js +269 -0
  89. package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
  90. package/templates/defect-report.md +17 -3
  91. package/templates/execution-report.md +9 -9
  92. package/templates/integration-scaffold/.env.example +1 -1
  93. package/templates/integration-scaffold/global-setup.ts +21 -1
  94. package/templates/integration-scaffold/playwright.config.ts +15 -1
  95. package/templates/qa-framework.instructions.md +9 -8
  96. package/templates/qa-framework.rules.md +53 -0
  97. package/templates/qa-readme.md +8 -8
  98. package/templates/session-summary.md +2 -2
  99. package/templates/specification/00-inventory.md +3 -3
  100. package/templates/specification/01-business-rules.md +6 -6
  101. package/templates/specification/02-workflows.md +1 -1
  102. package/templates/specification/03-roles-permissions.md +1 -1
  103. package/templates/specification/04-test-data.md +8 -8
  104. package/templates/specification/05-test-scenarios.md +5 -5
  105. package/templates/test-case.md +3 -3
  106. package/templates/test-data-guidelines.md +7 -7
  107. package/templates/test-plan-sprint.md +13 -13
  108. package/templates/test-plan.md +3 -3
  109. package/skills/qa-test-plan/references/priority-and-feasibility.md +0 -159
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * scripts/upgrade.js — Upgrade framework-owned files in an existing project
3
+ * scripts/upgrade.js - Upgrade framework-owned files in an existing project
4
4
  *
5
5
  * Usage:
6
6
  * qa-framework upgrade
@@ -27,7 +27,37 @@
27
27
  * - qa/02-test-plans/manual/ -> qa/02-test-plans/sprints/legacy-manual/
28
28
  * - qa/02-test-plans/*.md -> qa/02-test-plans/sprints/legacy/
29
29
  * - Creates qa/02-test-plans/sprints/ if absent
30
- * - Creates qa/03-test-cases/README.md optional marker if absent *
30
+ * - Creates qa/03-test-cases/README.md optional marker if absent
31
+ *
32
+ * MIGRATES (v1.11.x -> v1.12.0, lane-aware automation scaffold):
33
+ * Installs the lane scaffold only where it cannot break working user code.
34
+ * A file counts as framework-owned ("pristine") when its normalised sha256
35
+ * matches one this framework actually shipped; otherwise it is user-owned.
36
+ *
37
+ * ATOMIC GROUP (all installed, or none - config/global-setup require() the scripts):
38
+ * - qa/07-automation/e2e/scripts/lane-config.js (new in v1.12.0)
39
+ * - qa/07-automation/e2e/scripts/lane-lock.js (new in v1.12.0)
40
+ * - qa/07-automation/e2e/scripts/global-setup-guards.js (new in v1.12.0)
41
+ * - qa/07-automation/e2e/playwright.config.ts
42
+ * - qa/07-automation/e2e/global-setup.ts
43
+ * If any member is user-owned the whole group is skipped with a warning and
44
+ * the upgrade continues; the project keeps its working setup rather than
45
+ * being left half-migrated.
46
+ *
47
+ * PER FILE (leaf modules - a mixed state is still coherent):
48
+ * - qa/07-automation/e2e/fixtures/{auth,base,test-helpers}.ts
49
+ * - qa/07-automation/integration/{playwright.config.ts,global-setup.ts}
50
+ * - .env.example in both scaffolds (reference template only; your .env
51
+ * is never touched)
52
+ * Pristine -> overwritten. User-owned -> kept, with a warning.
53
+ *
54
+ * REPORTS only (never patched):
55
+ * - .fill(password) in any .ts under e2e/ or integration/ - leaks the
56
+ * password into Playwright traces. See "Pattern 7" in
57
+ * .github/skills/qa-automation/references/patterns.md. Not auto-fixed:
58
+ * the surrounding code differs per project and a bad rewrite of a
59
+ * working login is worse than the finding.
60
+ *
31
61
  * NEVER touches (project-owned):
32
62
  * - qa/01-specifications/ <- Your specs
33
63
  * - qa/02-test-plans/ <- Your test plans
@@ -46,6 +76,9 @@
46
76
  const fs = require('fs');
47
77
  const path = require('path');
48
78
 
79
+ const { buildCommandContent, discoverSkillNames, commandFileName } = require('./lib/claude-commands');
80
+ const { isSprintCycleEnabled, renderAllAgents } = require('./lib/claude-agents');
81
+
49
82
  const args = process.argv.slice(2);
50
83
  const dryRun = args.includes('--dry-run');
51
84
 
@@ -70,25 +103,72 @@ const githubDir = path.join(cwd, '.github');
70
103
  const skillsDest = path.join(githubDir, 'skills');
71
104
  const skillsSrc = path.resolve(__dirname, '..', 'skills');
72
105
 
106
+ // Normalised sha256 of every version of these scaffold files that this framework has
107
+ // shipped (v1.8.0 through v1.11.3). A project file matching one of these was written
108
+ // by the framework and never edited, so v1.12.0 may safely replace it. Anything else
109
+ // is user-owned. Regenerate by hashing `git show <tag>:templates/<key>` for each tag.
110
+ //
111
+ // Deliberately hash-based rather than version-based: the frameworkVersion recorded in
112
+ // real installations is unreliable (observed values include 1.1.3 and 1.0.0, neither
113
+ // of which corresponds to a released scaffold).
114
+ const SHIPPED_SCAFFOLD_HASHES = {
115
+ 'automation-scaffold/playwright.config.ts': [
116
+ '948635c118553e293447930ba3dcff9155446718554dd3bbcffac2720baebba1',
117
+ 'f145fda0a7215942b5431a779188609f8958e51a048b161ede605576efba3ef8',
118
+ ],
119
+ 'automation-scaffold/global-setup.ts': [
120
+ '077580914881ee71ac082d47579dc4cffa01e8348b0744fced6b8de2863e0bd1',
121
+ '7b3e7ca54929e525055d19ba8cd9643de722c7092c67b1a2e273072dd712ec7a',
122
+ ],
123
+ 'automation-scaffold/.env.example': [
124
+ '18fab7c165085a4a585a6911b42c49b97177265d46261c8362c3508a7436c65c',
125
+ '29dc3a97e42350fbe7a16681837ba8db2a539574f8aa86fe269204bc3081bff9',
126
+ ],
127
+ 'automation-scaffold/fixtures/auth.ts': [
128
+ 'a856df26d80d521a0ad349879096698191ed6a2f919fac96d85980ab1d435c3e',
129
+ ],
130
+ 'automation-scaffold/fixtures/base.ts': [
131
+ '7c52ea17fad732825e327af59a7f0f3bf53db166f7191932ae51c97fd2825a39',
132
+ ],
133
+ 'automation-scaffold/fixtures/test-helpers.ts': [
134
+ 'e2298972dea50e7df54265a7a94a232c0ff35c969f1b8778929079cfa2a1a541',
135
+ ],
136
+ // Introduced in v1.12.0 - never shipped before, so an existing copy in a project is
137
+ // always user-authored (two live installations hand-wrote their own lane-lock.js).
138
+ 'automation-scaffold/scripts/lane-config.js': [],
139
+ 'automation-scaffold/scripts/lane-lock.js': [],
140
+ 'automation-scaffold/scripts/global-setup-guards.js': [],
141
+ 'integration-scaffold/playwright.config.ts': [
142
+ '7276ed4fda85f80e37381257bc559f4bb48055d359858f69f6b838f3671a9e2d',
143
+ ],
144
+ 'integration-scaffold/global-setup.ts': [
145
+ 'f63f9588c4746a16e46345d1d4b781f2905b652481d563cfc075d9d8c8481bde',
146
+ ],
147
+ 'integration-scaffold/.env.example': [
148
+ '671461d7dc9883505cec314aefa1129a336b6a03b31a0eb2eb2ede2adda7c4ac',
149
+ ],
150
+ };
151
+
73
152
  const updated = [];
74
153
  const skipped = [];
75
154
  const warnings = [];
155
+ const errors = [];
76
156
 
77
157
  console.log(`\n[qa-framework/upgrade] ${dryRun ? '(dry-run) ' : ''}Upgrading framework-owned files`);
78
158
  console.log(`[qa-framework/upgrade] Project root: ${cwd}\n`);
79
159
 
80
160
  // ---------------------------------------------------------------------------
81
- // 1. .github/skills/ — always overwrite (framework-owned)
161
+ // 1. .github/skills/ - always overwrite (framework-owned)
82
162
  // ---------------------------------------------------------------------------
83
163
  if (!fs.existsSync(skillsSrc)) {
84
- warnings.push('skills/ source directory not found in package — skipping skill install');
164
+ warnings.push('skills/ source directory not found in package - skipping skill install');
85
165
  } else {
86
166
  fs.mkdirSync(skillsDest, { recursive: true });
87
167
  copyDirForce(skillsSrc, skillsDest);
88
168
  }
89
169
 
90
170
  // ---------------------------------------------------------------------------
91
- // 2. .github/instructions/qa-framework.instructions.md — overwrite (framework-owned)
171
+ // 2. .github/instructions/qa-framework.instructions.md - overwrite (framework-owned)
92
172
  // ---------------------------------------------------------------------------
93
173
  const copilotInstrPath = path.join(githubDir, 'instructions', 'qa-framework.instructions.md');
94
174
  const instrTemplatePath = path.resolve(__dirname, '..', 'templates', 'qa-framework.instructions.md');
@@ -96,13 +176,46 @@ const copilotContent = fs.readFileSync(instrTemplatePath, 'utf8')
96
176
  .replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
97
177
  forceWrite(copilotInstrPath, copilotContent);
98
178
 
179
+ // ---------------------------------------------------------------------------
180
+ // 2a. .claude/commands/qa-*.md and .claude/rules/qa-framework.md - overwrite (framework-owned)
181
+ // ---------------------------------------------------------------------------
182
+ const claudeCommandsDest = path.join(cwd, '.claude', 'commands');
183
+ for (const skillName of discoverSkillNames(skillsSrc)) {
184
+ forceWrite(path.join(claudeCommandsDest, commandFileName(skillName)), buildCommandContent(skillName));
185
+ }
186
+
187
+ const claudeRulesPath = path.join(cwd, '.claude', 'rules', 'qa-framework.md');
188
+ const rulesTemplatePath = path.resolve(__dirname, '..', 'templates', 'qa-framework.rules.md');
189
+ const claudeRulesContent = fs.readFileSync(rulesTemplatePath, 'utf8')
190
+ .replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
191
+ forceWrite(claudeRulesPath, claudeRulesContent);
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // 2c. .claude/agents/qa-*.md - optional ANALISIS/PLAN sprint-cycle mode (Claude Code only)
195
+ // Gated by integrations.azureDevOps.sprintCycle.enabled. Framework-owned when the
196
+ // flag is on; never generated (and never deleted if it was hand-authored) otherwise.
197
+ // ---------------------------------------------------------------------------
198
+ if (isSprintCycleEnabled(config)) {
199
+ const templatesDir = path.resolve(__dirname, '..', 'templates');
200
+ const claudeAgentsDest = path.join(cwd, '.claude', 'agents');
201
+ // Rendered per agent: one invalid agent (e.g. qa-investigador with a bad lane config)
202
+ // is reported and left untouched on disk, the others are still upgraded.
203
+ for (const agent of renderAllAgents(templatesDir, config)) {
204
+ if (agent.error) {
205
+ errors.push(`.claude/agents/${agent.fileName} was not generated. ${agent.error.message}`);
206
+ continue;
207
+ }
208
+ forceWrite(path.join(claudeAgentsDest, agent.fileName), agent.content);
209
+ }
210
+ }
211
+
99
212
  // ---------------------------------------------------------------------------
100
213
  // 2b. Migration: strip QA Framework section from old copilot-instructions.md
101
214
  //
102
215
  // Handles all cases:
103
- // A. File has custom instructions + QA section → keep custom, strip QA section
104
- // B. File has ONLY QA section (any version) → delete the file
105
- // C. File does not mention QA Framework → leave untouched
216
+ // A. File has custom instructions + QA section -> keep custom, strip QA section
217
+ // B. File has ONLY QA section (any version) -> delete the file
218
+ // C. File does not mention QA Framework -> leave untouched
106
219
  //
107
220
  // The QA section always starts with "# QA Framework Instructions" across all
108
221
  // previous versions, so that heading is the reliable split point.
@@ -118,22 +231,22 @@ if (fs.existsSync(oldCopilotPath)) {
118
231
  const before = oldContent.slice(0, qaIdx).replace(/\s*\n---\s*$/, '').trim();
119
232
 
120
233
  if (before.length === 0) {
121
- // Case B: file contained only QA Framework content → delete it
234
+ // Case B: file contained only QA Framework content -> delete it
122
235
  if (!dryRun) fs.unlinkSync(oldCopilotPath);
123
236
  updated.push(oldCopilotPath);
124
- console.log(` [deleted] .github/copilot-instructions.md (contained only QA Framework rules — now in .github/instructions/qa-framework.instructions.md)`);
237
+ console.log(` [deleted] .github/copilot-instructions.md (contained only QA Framework rules - now in .github/instructions/qa-framework.instructions.md)`);
125
238
  } else {
126
- // Case A: file had custom content too → write back only the custom part
239
+ // Case A: file had custom content too -> write back only the custom part
127
240
  if (!dryRun) fs.writeFileSync(oldCopilotPath, before + '\n', 'utf8');
128
241
  updated.push(oldCopilotPath);
129
- console.log(` [cleaned] .github/copilot-instructions.md — removed QA Framework section, kept custom instructions`);
242
+ console.log(` [cleaned] .github/copilot-instructions.md - removed QA Framework section, kept custom instructions`);
130
243
  }
131
244
  }
132
- // Case C: no QA marker found → leave untouched (no log noise)
245
+ // Case C: no QA marker found -> leave untouched (no log noise)
133
246
  }
134
247
 
135
248
  // ---------------------------------------------------------------------------
136
- // 3. qa/QA-STRUCTURE-GUIDE.md — overwrite (framework doc)
249
+ // 3. qa/QA-STRUCTURE-GUIDE.md - overwrite (framework doc)
137
250
  // ---------------------------------------------------------------------------
138
251
  const structureGuideSrc = path.resolve(__dirname, '..', 'docs', 'folder-structure-guide.md');
139
252
  const structureGuideDest = path.join(qaRoot, 'QA-STRUCTURE-GUIDE.md');
@@ -220,7 +333,7 @@ if (fs.existsSync(e2eDir)) {
220
333
  console.log(` [migrated] e2e/${entry.name}/ -> e2e/tests/${entry.name}/`);
221
334
  } else {
222
335
  warnings.push(
223
- `Cannot migrate e2e/${entry.name}/ — target e2e/tests/${entry.name}/ already exists. Merge manually.`
336
+ `Cannot migrate e2e/${entry.name}/ - target e2e/tests/${entry.name}/ already exists. Merge manually.`
224
337
  );
225
338
  }
226
339
  }
@@ -275,7 +388,7 @@ if (fs.existsSync(playwrightConfigPath)) {
275
388
  if (/testDir\s*:\s*['"]\.['"]/.test(cfg)) {
276
389
  cfg = cfg.replace(/testDir\s*:\s*['"]\.['"]/g, "testDir: './tests'");
277
390
  cfgChanged = true;
278
- console.log(` [patched] e2e/playwright.config.ts — testDir: '.' -> './tests'`);
391
+ console.log(` [patched] e2e/playwright.config.ts - testDir: '.' -> './tests'`);
279
392
  }
280
393
 
281
394
  // Inject testIgnore after testDir line if not present
@@ -285,7 +398,7 @@ if (fs.existsSync(playwrightConfigPath)) {
285
398
  "$1 testIgnore: ['**/helpers/debug/**', '**/seeds/**'],\n"
286
399
  );
287
400
  cfgChanged = true;
288
- console.log(` [patched] e2e/playwright.config.ts — added testIgnore`);
401
+ console.log(` [patched] e2e/playwright.config.ts - added testIgnore`);
289
402
  }
290
403
 
291
404
  if (cfgChanged) {
@@ -344,7 +457,7 @@ if (fs.existsSync(testPlansDir)) {
344
457
  updated.push(newFile);
345
458
  console.log(` [migrated] 02-test-plans/${entry.name} -> 02-test-plans/sprints/legacy/${entry.name}`);
346
459
  } else {
347
- warnings.push(`Cannot migrate 02-test-plans/${entry.name} — target already exists. Move manually.`);
460
+ warnings.push(`Cannot migrate 02-test-plans/${entry.name} - target already exists. Move manually.`);
348
461
  }
349
462
  }
350
463
  }
@@ -363,7 +476,7 @@ if (!fs.existsSync(testCasesReadme)) {
363
476
  if (!dryRun) {
364
477
  fs.mkdirSync(path.dirname(testCasesReadme), { recursive: true });
365
478
  fs.writeFileSync(testCasesReadme,
366
- '# 03-test-cases/ — Optional Standalone Test Cases\n\n' +
479
+ '# 03-test-cases/ - Optional Standalone Test Cases\n\n' +
367
480
  '> **v1.7.0+:** The primary location for test cases (with detailed steps) is now\n' +
368
481
  '> `qa/02-test-plans/sprints/Sprint-{N}/Plan-de-Pruebas-{project}-Sprint-{N}-{module}.md`.\n' +
369
482
  '>\n' +
@@ -441,6 +554,146 @@ if (fs.existsSync(memoryDir)) {
441
554
  }
442
555
  }
443
556
 
557
+
558
+ // ---------------------------------------------------------------------------
559
+ // 9. Migration v1.11.x -> v1.12.0: lane-aware automation scaffold
560
+ //
561
+ // v1.12.0 rewrote the automation scaffold around a lane table: playwright.config.ts
562
+ // and global-setup.ts now require() three CommonJS helpers under e2e/scripts/.
563
+ // init.js installs those for NEW projects only, so existing installations never
564
+ // receive them. This section carries them across.
565
+ //
566
+ // Classification per file (the governing rule is: never break working user code):
567
+ //
568
+ // a) Lane scripts (scripts/lane-config.js, lane-lock.js, global-setup-guards.js)
569
+ // plus playwright.config.ts and global-setup.ts form ONE ATOMIC GROUP,
570
+ // because config/global-setup require() the scripts by path. Installing a
571
+ // subset yields a project where Playwright cannot start at all. The group is
572
+ // installed only when EVERY member is either absent or provably pristine;
573
+ // otherwise the whole group is skipped with a warning and the rest of the
574
+ // upgrade continues. This is a coherent skip, not an abort: the project keeps
575
+ // the working setup it already had, so it is never left half-migrated.
576
+ //
577
+ // b) fixtures/{auth,base,test-helpers}.ts are per-file. They are leaf modules -
578
+ // nothing in the scaffold require()s them by path - so a mixed state is
579
+ // still coherent.
580
+ //
581
+ // c) .env.example is a reference template, never live config (the real file is
582
+ // .env, which this migration never touches). Overwritten when pristine.
583
+ //
584
+ // "Pristine" means the file's normalised sha256 matches a hash this framework
585
+ // actually shipped in some earlier release (SHIPPED_SCAFFOLD_HASHES). Anything
586
+ // else is user-owned and is only ever warned about. The project's recorded
587
+ // frameworkVersion is deliberately NOT used: live installations report values
588
+ // like 1.1.3 and 1.0.0 that do not correspond to any real scaffold release.
589
+ //
590
+ // Files that stay user-owned are additionally scanned for the trace-safety
591
+ // defect (.fill(password) leaks the password into Playwright traces). That is
592
+ // reported with file and line only - never patched, because the surrounding
593
+ // shapes differ per project (passwordSelector vs PASSWORD_SEL) and a bad regex
594
+ // edit to a working login is exactly the breakage this section must avoid.
595
+ // ---------------------------------------------------------------------------
596
+ const AUTOMATION_LANE_SCRIPTS = ['lane-config.js', 'lane-lock.js', 'global-setup-guards.js'];
597
+ const AUTOMATION_FIXTURES = ['auth.ts', 'base.ts', 'test-helpers.ts'];
598
+
599
+ // A bare e2e/ directory is not evidence of an automation setup: section 5 above
600
+ // creates e2e/tests/{helpers/debug,seeds}/ unconditionally. Only migrate a project
601
+ // that actually has a scaffold, i.e. one of the files this migration would replace.
602
+ const hasAutomationScaffold = ['playwright.config.ts', 'global-setup.ts', 'package.json']
603
+ .some((f) => fs.existsSync(path.join(e2eDir, f)));
604
+
605
+ if (hasAutomationScaffold) {
606
+ const scaffoldSrc = path.resolve(__dirname, '..', 'templates', 'automation-scaffold');
607
+
608
+ // --- (a) the atomic lane group -------------------------------------------
609
+ const groupMembers = [
610
+ ...AUTOMATION_LANE_SCRIPTS.map((f) => ({
611
+ rel: `scripts/${f}`,
612
+ src: path.join(scaffoldSrc, 'scripts', f),
613
+ dest: path.join(e2eDir, 'scripts', f),
614
+ key: `automation-scaffold/scripts/${f}`,
615
+ })),
616
+ ...['playwright.config.ts', 'global-setup.ts'].map((f) => ({
617
+ rel: f,
618
+ src: path.join(scaffoldSrc, f),
619
+ dest: path.join(e2eDir, f),
620
+ key: `automation-scaffold/${f}`,
621
+ })),
622
+ ];
623
+
624
+ const blockers = groupMembers.filter((m) => fs.existsSync(m.dest) && !isPristine(m.dest, m.key));
625
+
626
+ if (blockers.length === 0) {
627
+ for (const m of groupMembers) {
628
+ forceWrite(m.dest, fs.readFileSync(m.src, 'utf8'));
629
+ console.log(` [updated] e2e/${m.rel}`);
630
+ }
631
+ } else {
632
+ warnings.push(
633
+ `Lane-aware automation scaffold (v1.12.0) NOT installed - these files are user-owned:\n` +
634
+ blockers.map((m) => ` e2e/${m.rel}`).join('\n') +
635
+ `\n playwright.config.ts and global-setup.ts require() e2e/scripts/*.js, so the\n` +
636
+ ` group is installed all-or-nothing. Your current setup is left working and\n` +
637
+ ` untouched. To adopt lanes, merge these by hand from:\n` +
638
+ ` node_modules/@keber/qa-framework/templates/automation-scaffold/`
639
+ );
640
+ for (const m of groupMembers) skipped.push(m.dest);
641
+ }
642
+
643
+ // --- (b) fixtures, per file ----------------------------------------------
644
+ for (const file of AUTOMATION_FIXTURES) {
645
+ const dest = path.join(e2eDir, 'fixtures', file);
646
+ const key = `automation-scaffold/fixtures/${file}`;
647
+ if (!fs.existsSync(dest) || isPristine(dest, key)) {
648
+ forceWrite(dest, fs.readFileSync(path.join(scaffoldSrc, 'fixtures', file), 'utf8'));
649
+ console.log(` [updated] e2e/fixtures/${file}`);
650
+ } else {
651
+ skipped.push(dest);
652
+ warnings.push(
653
+ `e2e/fixtures/${file} is user-owned - kept as is (v1.12.0 version not applied).`
654
+ );
655
+ }
656
+ }
657
+
658
+ // --- (c) .env.example ------------------------------------------------------
659
+ upgradeReferenceEnvExample(
660
+ path.join(e2eDir, '.env.example'),
661
+ path.join(scaffoldSrc, '.env.example'),
662
+ 'automation-scaffold/.env.example',
663
+ 'e2e/.env.example'
664
+ );
665
+
666
+ // --- trace safety on everything left user-owned ---------------------------
667
+ reportTraceSafety(e2eDir);
668
+ }
669
+
670
+ // --- integration scaffold: no lane coupling, so purely per-file -------------
671
+ // Same reasoning as above: require a real scaffold file, not just the folder.
672
+ const hasIntegrationScaffold = ['playwright.config.ts', 'global-setup.ts', 'package.json']
673
+ .some((f) => fs.existsSync(path.join(integrationDir, f)));
674
+
675
+ if (hasIntegrationScaffold) {
676
+ const integrationSrc = path.resolve(__dirname, '..', 'templates', 'integration-scaffold');
677
+ for (const file of ['playwright.config.ts', 'global-setup.ts']) {
678
+ const dest = path.join(integrationDir, file);
679
+ const key = `integration-scaffold/${file}`;
680
+ if (!fs.existsSync(dest) || isPristine(dest, key)) {
681
+ forceWrite(dest, fs.readFileSync(path.join(integrationSrc, file), 'utf8'));
682
+ console.log(` [updated] integration/${file}`);
683
+ } else {
684
+ skipped.push(dest);
685
+ warnings.push(`integration/${file} is user-owned - kept as is (v1.12.0 version not applied).`);
686
+ }
687
+ }
688
+ upgradeReferenceEnvExample(
689
+ path.join(integrationDir, '.env.example'),
690
+ path.join(integrationSrc, '.env.example'),
691
+ 'integration-scaffold/.env.example',
692
+ 'integration/.env.example'
693
+ );
694
+ reportTraceSafety(integrationDir);
695
+ }
696
+
444
697
  // ---------------------------------------------------------------------------
445
698
  // Summary
446
699
  // ---------------------------------------------------------------------------
@@ -451,13 +704,19 @@ if (updated.length) {
451
704
  }
452
705
  if (skipped.length) {
453
706
  console.log('\n Skipped (already up to date):');
454
- for (const f of skipped) console.log(` — ${path.relative(cwd, f)}`);
707
+ for (const f of skipped) console.log(` - ${path.relative(cwd, f)}`);
455
708
  }
456
709
  if (warnings.length) {
457
710
  console.log('\n Warnings:');
458
711
  for (const w of warnings) console.log(` ⚠️ ${w}`);
459
712
  }
460
713
 
714
+ if (errors.length) {
715
+ for (const e of errors) console.error(`[qa-framework/upgrade] ERROR: ${e}`);
716
+ console.error('[qa-framework/upgrade] Fix qa-framework.config.json (run "qa-framework validate") and run upgrade again.');
717
+ process.exitCode = 1;
718
+ }
719
+
461
720
  console.log('\n[qa-framework/upgrade] Done.\n');
462
721
 
463
722
  // -----------------------------------------------------------------------
@@ -491,3 +750,93 @@ function copyDirForce(srcDir, destDir) {
491
750
  }
492
751
  }
493
752
 
753
+
754
+ // Normalised content hash: strips a UTF-8 BOM and CRLF so that a file which only
755
+ // differs by line endings or a BOM (both of which tooling rewrites silently) is
756
+ // still recognised as the framework's own output.
757
+ function scaffoldHash(content) {
758
+ const normalised = content.replace(/^/, '').replace(/\r\n/g, '\n');
759
+ return require('crypto').createHash('sha256').update(normalised, 'utf8').digest('hex');
760
+ }
761
+
762
+ // A file is "pristine" when its content is byte-identical (after normalisation) to
763
+ // something this framework actually shipped. A key with no recorded hashes - a file
764
+ // introduced in the current release - can never be pristine, so an existing copy is
765
+ // always treated as user-owned.
766
+ function isPristine(filePath, key) {
767
+ let content;
768
+ try {
769
+ content = fs.readFileSync(filePath, 'utf8');
770
+ } catch {
771
+ return false;
772
+ }
773
+ const hash = scaffoldHash(content);
774
+
775
+ // Already identical to what this release ships: nothing to do, and certainly not a
776
+ // user edit. Computed rather than tabulated so the table never goes stale on release.
777
+ try {
778
+ const current = path.resolve(__dirname, '..', 'templates', ...key.split('/'));
779
+ if (fs.existsSync(current) && scaffoldHash(fs.readFileSync(current, 'utf8')) === hash) return true;
780
+ } catch {
781
+ /* fall through to the historical table */
782
+ }
783
+
784
+ const known = SHIPPED_SCAFFOLD_HASHES[key];
785
+ return Boolean(known) && known.includes(hash);
786
+ }
787
+
788
+ // .env.example is a reference template, never live configuration - the real values
789
+ // live in .env, which is never touched here. Overwrite it when pristine; when the
790
+ // user has edited it, keep theirs and say so.
791
+ function upgradeReferenceEnvExample(dest, src, key, label) {
792
+ if (!fs.existsSync(src)) return;
793
+ if (!fs.existsSync(dest) || isPristine(dest, key)) {
794
+ forceWrite(dest, fs.readFileSync(src, 'utf8'));
795
+ console.log(` [updated] ${label}`);
796
+ } else {
797
+ skipped.push(dest);
798
+ warnings.push(
799
+ `${label} is user-owned - kept as is. New v1.12.0 keys may be missing; compare against\n` +
800
+ ` node_modules/@keber/qa-framework/templates/${key}`
801
+ );
802
+ }
803
+ }
804
+
805
+ // Trace safety (see skills/qa-automation/references/patterns.md, "Pattern 7"):
806
+ // page.locator(...).fill(password) records the password in the Playwright trace.
807
+ // Report location only. The surrounding code differs per project, so an automated
808
+ // rewrite of a working login is more dangerous than the finding itself.
809
+ function reportTraceSafety(rootDir) {
810
+ const offenders = [];
811
+ const FILL_PASSWORD = /\.fill\(\s*(?:password|pwd|[A-Za-z_$]*(?:[Pp]ass(?:word)?|PASSWORD)[A-Za-z_$]*)\s*\)/;
812
+
813
+ (function walk(dir) {
814
+ let entries;
815
+ try {
816
+ entries = fs.readdirSync(dir, { withFileTypes: true });
817
+ } catch {
818
+ return;
819
+ }
820
+ for (const entry of entries) {
821
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
822
+ const full = path.join(dir, entry.name);
823
+ if (entry.isDirectory()) {
824
+ walk(full);
825
+ } else if (/\.ts$/.test(entry.name)) {
826
+ const lines = fs.readFileSync(full, 'utf8').split(/\r?\n/);
827
+ lines.forEach((line, i) => {
828
+ if (FILL_PASSWORD.test(line)) offenders.push(`${path.relative(cwd, full)}:${i + 1}`);
829
+ });
830
+ }
831
+ }
832
+ })(rootDir);
833
+
834
+ if (offenders.length) {
835
+ warnings.push(
836
+ `Trace safety: the password is written with .fill(), which stores it in the\n` +
837
+ ` Playwright trace. Replace with the page.evaluate() form ("Pattern 7" in\n` +
838
+ ` .github/skills/qa-automation/references/patterns.md):\n` +
839
+ offenders.map((o) => ` ${o}`).join('\n')
840
+ );
841
+ }
842
+ }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * scripts/validate.js — Validate qa/ folder structure and conventions
3
+ * scripts/validate.js - Validate qa/ folder structure and conventions
4
4
  *
5
5
  * Usage:
6
6
  * qa-framework validate
@@ -8,8 +8,11 @@
8
8
  * qa-framework validate --config ./my-config.json
9
9
  *
10
10
  * Checks:
11
+ * 0. Config values read by the sprint agents: approvalThresholdPercent (0 < x < 100),
12
+ * decimalSeparator, defectIdPrefix and, when sprint agents and lanes are enabled,
13
+ * parallelLanes / mcpNamespace (only present-but-invalid values are reported)
11
14
  * 1. Required top-level folders exist
12
- * 2. All submodule folders contain the 6 required spec files
15
+ * 2. All submodule folders under 01-specifications/ contain the 6 required spec files
13
16
  * 3. No plaintext credentials in spec files (basic scan)
14
17
  * 4. Test case naming convention (TC-NNN pattern in spec files)
15
18
  * 5. [--strict] Automation spec files exist for every submodule
@@ -20,19 +23,27 @@
20
23
 
21
24
  const fs = require('fs');
22
25
  const path = require('path');
26
+ const { moduleBareName, submoduleBareName } = require('./lib/module-keys');
23
27
 
24
28
  // --- Args ---
25
29
  const args = process.argv.slice(2);
26
30
  const strict = args.includes('--strict');
27
31
  const configFlag = args.indexOf('--config');
32
+ const { validateAgentConfig } = require('./lib/claude-agents');
33
+
34
+ // Same lookup order as init/upgrade: --config, then root, then qa/.
28
35
  const configPath = configFlag !== -1
29
36
  ? path.resolve(process.cwd(), args[configFlag + 1])
30
- : path.resolve(process.cwd(), 'qa-framework.config.json');
37
+ : [
38
+ path.resolve(process.cwd(), 'qa-framework.config.json'),
39
+ path.resolve(process.cwd(), 'qa', 'qa-framework.config.json'),
40
+ ].find((candidate) => fs.existsSync(candidate));
31
41
 
32
42
  // --- Load config (optional) ---
33
43
  let qaRoot = path.resolve(process.cwd(), 'qa');
34
- if (fs.existsSync(configPath)) {
35
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
44
+ let config = null;
45
+ if (configPath && fs.existsSync(configPath)) {
46
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
36
47
  qaRoot = path.resolve(process.cwd(), config.conventions?.qaRoot ?? 'qa');
37
48
  }
38
49
 
@@ -42,12 +53,21 @@ if (strict) console.log('[qa-framework/validate] Mode: STRICT');
42
53
  const errors = [];
43
54
  const warnings = [];
44
55
 
56
+ // -----------------------------------------------------------------------
57
+ // Check 0: Config values (only present-but-invalid values are reported)
58
+ // -----------------------------------------------------------------------
59
+ if (config) {
60
+ for (const message of validateAgentConfig(config)) {
61
+ errors.push(`Invalid config (${path.relative(process.cwd(), configPath)}): ${message}`);
62
+ }
63
+ }
64
+
45
65
  // -----------------------------------------------------------------------
46
66
  // Check 1: Required top-level folders
47
67
  // -----------------------------------------------------------------------
48
68
  const requiredFolders = [
49
69
  '00-standards',
50
- '05-test-plans',
70
+ '05-test-execution',
51
71
  '06-defects',
52
72
  '07-automation',
53
73
  ];
@@ -70,18 +90,18 @@ const SPEC_FILES = [
70
90
  '05-test-scenarios.md',
71
91
  ];
72
92
 
73
- const SKIP_DIRS = new Set([
74
- '00-guides', '00-standards', '05-test-plans',
75
- '06-defects', '07-automation', '08-azure-integration',
76
- ]);
93
+ // Specs live at qa/01-specifications/{module}/{submodule}/. Rooting the scan
94
+ // here keeps it at the right depth and means sibling folders such as
95
+ // 02-test-plans/ or memory/ are never mistaken for modules in the first place.
96
+ const specsRoot = path.join(qaRoot, '01-specifications');
77
97
 
78
- if (fs.existsSync(qaRoot)) {
79
- const topDirs = fs.readdirSync(qaRoot).filter(d => {
80
- return fs.statSync(path.join(qaRoot, d)).isDirectory() && !SKIP_DIRS.has(d);
81
- });
98
+ if (fs.existsSync(specsRoot)) {
99
+ const moduleDirs = fs.readdirSync(specsRoot).filter(d =>
100
+ fs.statSync(path.join(specsRoot, d)).isDirectory()
101
+ );
82
102
 
83
- for (const moduleDir of topDirs) {
84
- const modulePath = path.join(qaRoot, moduleDir);
103
+ for (const moduleDir of moduleDirs) {
104
+ const modulePath = path.join(specsRoot, moduleDir);
85
105
  const subDirs = fs.readdirSync(modulePath).filter(d =>
86
106
  fs.statSync(path.join(modulePath, d)).isDirectory()
87
107
  );
@@ -90,15 +110,20 @@ if (fs.existsSync(qaRoot)) {
90
110
  const subPath = path.join(modulePath, subDir);
91
111
  for (const specFile of SPEC_FILES) {
92
112
  if (!fs.existsSync(path.join(subPath, specFile))) {
93
- errors.push(`Missing spec file: qa/${moduleDir}/${subDir}/${specFile}`);
113
+ errors.push(`Missing spec file: qa/01-specifications/${moduleDir}/${subDir}/${specFile}`);
94
114
  }
95
115
  }
96
116
 
97
- // Strict: automation spec must exist
117
+ // Strict: automation spec must exist.
118
+ // Spec folders are module-{name}/submodule-{name}; e2e/tests/ is keyed by the
119
+ // bare name, so the prefixes are stripped before deriving the automation path.
120
+ // Without this the warning points at a folder no project has ever created.
98
121
  if (strict) {
99
- const specTs = path.join(qaRoot, '07-automation', 'e2e', moduleDir, `${subDir}.spec.ts`);
122
+ const moduleName = moduleBareName(moduleDir);
123
+ const subName = submoduleBareName(subDir);
124
+ const specTs = path.join(qaRoot, '07-automation', 'e2e', 'tests', moduleName, `${subName}.spec.ts`);
100
125
  if (!fs.existsSync(specTs)) {
101
- warnings.push(`[STRICT] No automation spec found: qa/07-automation/e2e/${moduleDir}/${subDir}.spec.ts`);
126
+ warnings.push(`[STRICT] No automation spec found: qa/07-automation/e2e/tests/${moduleName}/${subName}.spec.ts`);
102
127
  }
103
128
  }
104
129
  }
@@ -147,7 +172,7 @@ for (const f of specFiles) {
147
172
  // -----------------------------------------------------------------------
148
173
  console.log('');
149
174
  if (errors.length === 0 && warnings.length === 0) {
150
- console.log('✅ Validation passed — no issues found.');
175
+ console.log('✅ Validation passed - no issues found.');
151
176
  process.exit(0);
152
177
  }
153
178