@keber/qa-framework 1.10.0 → 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.
Files changed (113) hide show
  1. package/CHANGELOG.md +63 -18
  2. package/README.md +2 -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 +106 -80
  7. package/docs/installation.md +35 -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 +83 -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/pipelines/azure-pipeline-qa.yml +92 -25
  40. package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
  41. package/integrations/playwright/README.md +128 -4
  42. package/package.json +9 -6
  43. package/qa-framework.config.json +24 -3
  44. package/scripts/check-forbidden-chars.js +187 -0
  45. package/scripts/check-spanish-accents.js +159 -0
  46. package/scripts/cli.js +1 -1
  47. package/scripts/generate.js +2 -2
  48. package/scripts/init.js +108 -45
  49. package/scripts/lib/claude-agents.js +78 -0
  50. package/scripts/lib/claude-commands.js +56 -0
  51. package/scripts/sync-version.js +36 -0
  52. package/scripts/upgrade.js +461 -30
  53. package/scripts/validate.js +17 -17
  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 +234 -0
  78. package/templates/agents/qa-plan.md +131 -0
  79. package/templates/automation-scaffold/.env.example +52 -3
  80. package/templates/automation-scaffold/fixtures/auth.ts +27 -5
  81. package/templates/automation-scaffold/fixtures/base.ts +130 -0
  82. package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
  83. package/templates/automation-scaffold/global-setup.ts +196 -37
  84. package/templates/automation-scaffold/playwright.config.ts +74 -10
  85. package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
  86. package/templates/automation-scaffold/scripts/lane-config.js +269 -0
  87. package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
  88. package/templates/defect-report.md +18 -4
  89. package/templates/execution-report.md +9 -9
  90. package/templates/integration-scaffold/.env.example +40 -0
  91. package/templates/integration-scaffold/README.md +77 -0
  92. package/templates/integration-scaffold/global-setup.ts +119 -0
  93. package/templates/integration-scaffold/package.json +15 -0
  94. package/templates/integration-scaffold/playwright.config.ts +92 -0
  95. package/templates/integration-scaffold/tests/example.spec.ts +43 -0
  96. package/templates/memory/ci-pipeline-findings.md +36 -0
  97. package/templates/memory/data-volatility-strategies.md +70 -0
  98. package/templates/memory/e2e-stabilization-patterns.md +54 -0
  99. package/templates/qa-framework.instructions.md +16 -7
  100. package/templates/qa-framework.rules.md +53 -0
  101. package/templates/qa-readme.md +26 -5
  102. package/templates/session-summary.md +2 -2
  103. package/templates/specification/00-inventory.md +3 -3
  104. package/templates/specification/01-business-rules.md +6 -6
  105. package/templates/specification/02-workflows.md +1 -1
  106. package/templates/specification/03-roles-permissions.md +1 -1
  107. package/templates/specification/04-test-data.md +8 -8
  108. package/templates/specification/05-test-scenarios.md +5 -5
  109. package/templates/test-case.md +3 -3
  110. package/templates/test-data-guidelines.md +7 -7
  111. package/templates/test-plan-sprint.md +13 -13
  112. package/templates/test-plan.md +3 -3
  113. 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 { AGENT_NAMES, isSprintCycleEnabled, buildAgentContent, agentFileName } = require('./lib/claude-agents');
81
+
49
82
  const args = process.argv.slice(2);
50
83
  const dryRun = args.includes('--dry-run');
51
84
 
@@ -70,6 +103,52 @@ 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 = [];
@@ -78,32 +157,58 @@ console.log(`\n[qa-framework/upgrade] ${dryRun ? '(dry-run) ' : ''}Upgrading fra
78
157
  console.log(`[qa-framework/upgrade] Project root: ${cwd}\n`);
79
158
 
80
159
  // ---------------------------------------------------------------------------
81
- // 1. .github/skills/ — always overwrite (framework-owned)
160
+ // 1. .github/skills/ - always overwrite (framework-owned)
82
161
  // ---------------------------------------------------------------------------
83
162
  if (!fs.existsSync(skillsSrc)) {
84
- warnings.push('skills/ source directory not found in package — skipping skill install');
163
+ warnings.push('skills/ source directory not found in package - skipping skill install');
85
164
  } else {
86
165
  fs.mkdirSync(skillsDest, { recursive: true });
87
166
  copyDirForce(skillsSrc, skillsDest);
88
167
  }
89
168
 
90
169
  // ---------------------------------------------------------------------------
91
- // 2. .github/instructions/qa-framework.instructions.md — overwrite (framework-owned)
170
+ // 2. .github/instructions/qa-framework.instructions.md - overwrite (framework-owned)
92
171
  // ---------------------------------------------------------------------------
93
172
  const copilotInstrPath = path.join(githubDir, 'instructions', 'qa-framework.instructions.md');
94
173
  const instrTemplatePath = path.resolve(__dirname, '..', 'templates', 'qa-framework.instructions.md');
95
174
  const copilotContent = fs.readFileSync(instrTemplatePath, 'utf8')
96
- .replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0')
97
- .replace('{{ADO_SECTION}}', '');
175
+ .replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
98
176
  forceWrite(copilotInstrPath, copilotContent);
99
177
 
178
+ // ---------------------------------------------------------------------------
179
+ // 2a. .claude/commands/qa-*.md and .claude/rules/qa-framework.md - overwrite (framework-owned)
180
+ // ---------------------------------------------------------------------------
181
+ const claudeCommandsDest = path.join(cwd, '.claude', 'commands');
182
+ for (const skillName of discoverSkillNames(skillsSrc)) {
183
+ forceWrite(path.join(claudeCommandsDest, commandFileName(skillName)), buildCommandContent(skillName));
184
+ }
185
+
186
+ const claudeRulesPath = path.join(cwd, '.claude', 'rules', 'qa-framework.md');
187
+ const rulesTemplatePath = path.resolve(__dirname, '..', 'templates', 'qa-framework.rules.md');
188
+ const claudeRulesContent = fs.readFileSync(rulesTemplatePath, 'utf8')
189
+ .replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
190
+ forceWrite(claudeRulesPath, claudeRulesContent);
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // 2c. .claude/agents/qa-*.md - optional ANALISIS/PLAN sprint-cycle mode (Claude Code only)
194
+ // Gated by integrations.azureDevOps.sprintCycle.enabled. Framework-owned when the
195
+ // flag is on; never generated (and never deleted if it was hand-authored) otherwise.
196
+ // ---------------------------------------------------------------------------
197
+ if (isSprintCycleEnabled(config)) {
198
+ const templatesDir = path.resolve(__dirname, '..', 'templates');
199
+ const claudeAgentsDest = path.join(cwd, '.claude', 'agents');
200
+ for (const agentName of AGENT_NAMES) {
201
+ forceWrite(path.join(claudeAgentsDest, agentFileName(agentName)), buildAgentContent(agentName, templatesDir, config));
202
+ }
203
+ }
204
+
100
205
  // ---------------------------------------------------------------------------
101
206
  // 2b. Migration: strip QA Framework section from old copilot-instructions.md
102
207
  //
103
208
  // Handles all cases:
104
- // A. File has custom instructions + QA section → keep custom, strip QA section
105
- // B. File has ONLY QA section (any version) → delete the file
106
- // C. File does not mention QA Framework → leave untouched
209
+ // A. File has custom instructions + QA section -> keep custom, strip QA section
210
+ // B. File has ONLY QA section (any version) -> delete the file
211
+ // C. File does not mention QA Framework -> leave untouched
107
212
  //
108
213
  // The QA section always starts with "# QA Framework Instructions" across all
109
214
  // previous versions, so that heading is the reliable split point.
@@ -119,22 +224,22 @@ if (fs.existsSync(oldCopilotPath)) {
119
224
  const before = oldContent.slice(0, qaIdx).replace(/\s*\n---\s*$/, '').trim();
120
225
 
121
226
  if (before.length === 0) {
122
- // Case B: file contained only QA Framework content → delete it
227
+ // Case B: file contained only QA Framework content -> delete it
123
228
  if (!dryRun) fs.unlinkSync(oldCopilotPath);
124
229
  updated.push(oldCopilotPath);
125
- console.log(` [deleted] .github/copilot-instructions.md (contained only QA Framework rules — now in .github/instructions/qa-framework.instructions.md)`);
230
+ console.log(` [deleted] .github/copilot-instructions.md (contained only QA Framework rules - now in .github/instructions/qa-framework.instructions.md)`);
126
231
  } else {
127
- // Case A: file had custom content too → write back only the custom part
232
+ // Case A: file had custom content too -> write back only the custom part
128
233
  if (!dryRun) fs.writeFileSync(oldCopilotPath, before + '\n', 'utf8');
129
234
  updated.push(oldCopilotPath);
130
- console.log(` [cleaned] .github/copilot-instructions.md — removed QA Framework section, kept custom instructions`);
235
+ console.log(` [cleaned] .github/copilot-instructions.md - removed QA Framework section, kept custom instructions`);
131
236
  }
132
237
  }
133
- // Case C: no QA marker found → leave untouched (no log noise)
238
+ // Case C: no QA marker found -> leave untouched (no log noise)
134
239
  }
135
240
 
136
241
  // ---------------------------------------------------------------------------
137
- // 3. qa/QA-STRUCTURE-GUIDE.md — overwrite (framework doc)
242
+ // 3. qa/QA-STRUCTURE-GUIDE.md - overwrite (framework doc)
138
243
  // ---------------------------------------------------------------------------
139
244
  const structureGuideSrc = path.resolve(__dirname, '..', 'docs', 'folder-structure-guide.md');
140
245
  const structureGuideDest = path.join(qaRoot, 'QA-STRUCTURE-GUIDE.md');
@@ -175,6 +280,22 @@ for (const file of scaffoldFiles) {
175
280
  }
176
281
  }
177
282
 
283
+ // 5a.1. Ensure the refreshed fixtures scaffold exists.
284
+ const scaffoldFixturesDir = path.join(e2eDir, 'fixtures');
285
+ const fixtureFiles = ['auth.ts', 'test-helpers.ts', 'base.ts'];
286
+ for (const file of fixtureFiles) {
287
+ const oldPath = path.join(automationDir, 'fixtures', file);
288
+ const newPath = path.join(scaffoldFixturesDir, file);
289
+ if (fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
290
+ if (!dryRun) {
291
+ fs.mkdirSync(scaffoldFixturesDir, { recursive: true });
292
+ fs.renameSync(oldPath, newPath);
293
+ }
294
+ updated.push(newPath);
295
+ console.log(` [migrated] fixtures/${file} -> e2e/fixtures/${file}`);
296
+ }
297
+ }
298
+
178
299
  // 5b. fixtures/: 07-automation/fixtures/ -> 07-automation/e2e/fixtures/
179
300
  const oldFixturesDir = path.join(automationDir, 'fixtures');
180
301
  const newFixturesDir = path.join(e2eDir, 'fixtures');
@@ -205,23 +326,40 @@ if (fs.existsSync(e2eDir)) {
205
326
  console.log(` [migrated] e2e/${entry.name}/ -> e2e/tests/${entry.name}/`);
206
327
  } else {
207
328
  warnings.push(
208
- `Cannot migrate e2e/${entry.name}/ — target e2e/tests/${entry.name}/ already exists. Merge manually.`
329
+ `Cannot migrate e2e/${entry.name}/ - target e2e/tests/${entry.name}/ already exists. Merge manually.`
209
330
  );
210
331
  }
211
332
  }
212
333
  }
213
334
 
214
- // 5d. Create integration/ and load/ placeholders if missing.
215
- const integrationReadme = path.join(automationDir, 'integration', 'README.md');
216
- const loadReadme = path.join(automationDir, 'load', 'README.md');
335
+ // 5d. Create integration scaffold if missing (uses template; existing files are never overwritten).
336
+ const integrationDir = path.join(automationDir, 'integration');
337
+ const integrationScaffold = path.resolve(__dirname, '..', 'templates', 'integration-scaffold');
338
+ const integrationReadme = path.join(integrationDir, 'README.md');
339
+ const loadReadme = path.join(automationDir, 'load', 'README.md');
217
340
  if (!fs.existsSync(integrationReadme)) {
218
341
  if (!dryRun) {
219
- fs.mkdirSync(path.dirname(integrationReadme), { recursive: true });
220
- fs.writeFileSync(integrationReadme,
221
- '# Integration Tests\n\n> Placeholder for API/integration tests (k6, JMeter, Azure Load Testing, etc.).\n> Each tool gets its own subdirectory.\n', 'utf8');
342
+ fs.mkdirSync(integrationDir, { recursive: true });
343
+ for (const file of ['package.json', 'playwright.config.ts', 'global-setup.ts', 'README.md', '.env.example']) {
344
+ const dest = path.join(integrationDir, file);
345
+ if (!fs.existsSync(dest)) {
346
+ fs.copyFileSync(path.join(integrationScaffold, file), dest);
347
+ updated.push(dest);
348
+ console.log(` [created] ${path.relative(cwd, dest)}`);
349
+ }
350
+ }
351
+ const integrationTestsDir = path.join(integrationDir, 'tests');
352
+ fs.mkdirSync(integrationTestsDir, { recursive: true });
353
+ const exampleSpec = path.join(integrationTestsDir, 'example.spec.ts');
354
+ if (!fs.existsSync(exampleSpec)) {
355
+ fs.copyFileSync(path.join(integrationScaffold, 'tests', 'example.spec.ts'), exampleSpec);
356
+ updated.push(exampleSpec);
357
+ console.log(` [created] ${path.relative(cwd, exampleSpec)}`);
358
+ }
359
+ } else {
360
+ updated.push(integrationReadme);
361
+ console.log(` [created] ${path.relative(cwd, integrationReadme)}`);
222
362
  }
223
- updated.push(integrationReadme);
224
- console.log(` [created] ${path.relative(cwd, integrationReadme)}`);
225
363
  }
226
364
  if (!fs.existsSync(loadReadme)) {
227
365
  if (!dryRun) {
@@ -243,7 +381,7 @@ if (fs.existsSync(playwrightConfigPath)) {
243
381
  if (/testDir\s*:\s*['"]\.['"]/.test(cfg)) {
244
382
  cfg = cfg.replace(/testDir\s*:\s*['"]\.['"]/g, "testDir: './tests'");
245
383
  cfgChanged = true;
246
- console.log(` [patched] e2e/playwright.config.ts — testDir: '.' -> './tests'`);
384
+ console.log(` [patched] e2e/playwright.config.ts - testDir: '.' -> './tests'`);
247
385
  }
248
386
 
249
387
  // Inject testIgnore after testDir line if not present
@@ -253,7 +391,7 @@ if (fs.existsSync(playwrightConfigPath)) {
253
391
  "$1 testIgnore: ['**/helpers/debug/**', '**/seeds/**'],\n"
254
392
  );
255
393
  cfgChanged = true;
256
- console.log(` [patched] e2e/playwright.config.ts — added testIgnore`);
394
+ console.log(` [patched] e2e/playwright.config.ts - added testIgnore`);
257
395
  }
258
396
 
259
397
  if (cfgChanged) {
@@ -312,7 +450,7 @@ if (fs.existsSync(testPlansDir)) {
312
450
  updated.push(newFile);
313
451
  console.log(` [migrated] 02-test-plans/${entry.name} -> 02-test-plans/sprints/legacy/${entry.name}`);
314
452
  } else {
315
- warnings.push(`Cannot migrate 02-test-plans/${entry.name} — target already exists. Move manually.`);
453
+ warnings.push(`Cannot migrate 02-test-plans/${entry.name} - target already exists. Move manually.`);
316
454
  }
317
455
  }
318
456
  }
@@ -331,7 +469,7 @@ if (!fs.existsSync(testCasesReadme)) {
331
469
  if (!dryRun) {
332
470
  fs.mkdirSync(path.dirname(testCasesReadme), { recursive: true });
333
471
  fs.writeFileSync(testCasesReadme,
334
- '# 03-test-cases/ — Optional Standalone Test Cases\n\n' +
472
+ '# 03-test-cases/ - Optional Standalone Test Cases\n\n' +
335
473
  '> **v1.7.0+:** The primary location for test cases (with detailed steps) is now\n' +
336
474
  '> `qa/02-test-plans/sprints/Sprint-{N}/Plan-de-Pruebas-{project}-Sprint-{N}-{module}.md`.\n' +
337
475
  '>\n' +
@@ -346,6 +484,209 @@ if (!fs.existsSync(testCasesReadme)) {
346
484
  console.log(` [created] 03-test-cases/README.md`);
347
485
  }
348
486
 
487
+ // ---------------------------------------------------------------------------
488
+ // 7. Ensure 06-defects/disputed/ exists (added in v1.11.0)
489
+ // Safe: only creates if missing. Never touches existing files.
490
+ // ---------------------------------------------------------------------------
491
+ const defectsDir = path.join(qaRoot, '06-defects');
492
+ const disputedDir = path.join(defectsDir, 'disputed');
493
+ const disputedReadme = path.join(disputedDir, 'README.md');
494
+
495
+ if (fs.existsSync(defectsDir) && !fs.existsSync(disputedDir)) {
496
+ if (!dryRun) {
497
+ fs.mkdirSync(disputedDir, { recursive: true });
498
+ fs.writeFileSync(disputedReadme,
499
+ `# 06-defects/disputed/
500
+
501
+ Use this folder for defects where QA has reproducible evidence but the business or
502
+ development team disputes whether the observed behavior is actually a defect.
503
+
504
+ A defect stays here while the functional decision is pending. Once the team reaches
505
+ a formal conclusion, move the file to open/ (confirmed defect) or resolved/ (accepted
506
+ behavior or won't fix).
507
+ `, 'utf8');
508
+ }
509
+ updated.push(disputedDir);
510
+ console.log(` [created] 06-defects/disputed/`);
511
+ console.log(` [created] 06-defects/disputed/README.md`);
512
+ } else if (!fs.existsSync(disputedReadme) && fs.existsSync(disputedDir)) {
513
+ if (!dryRun) {
514
+ fs.writeFileSync(disputedReadme,
515
+ `# 06-defects/disputed/
516
+
517
+ Use this folder for defects where QA has reproducible evidence but the business or
518
+ development team disputes whether the observed behavior is actually a defect.
519
+
520
+ A defect stays here while the functional decision is pending. Once the team reaches
521
+ a formal conclusion, move the file to open/ (confirmed defect) or resolved/ (accepted
522
+ behavior or won't fix).
523
+ `, 'utf8');
524
+ }
525
+ updated.push(disputedReadme);
526
+ console.log(` [created] 06-defects/disputed/README.md`);
527
+ }
528
+
529
+ // ---------------------------------------------------------------------------
530
+ // 8. Seed qa/memory/ template files if missing (added in v1.11.0)
531
+ // Safe: only creates files that do not exist. Never overwrites user content.
532
+ // ---------------------------------------------------------------------------
533
+ const memoryDir = path.join(qaRoot, 'memory');
534
+ if (fs.existsSync(memoryDir)) {
535
+ const memorySrc = path.resolve(__dirname, '..', 'templates', 'memory');
536
+ for (const file of ['ci-pipeline-findings.md', 'e2e-stabilization-patterns.md', 'data-volatility-strategies.md']) {
537
+ const dest = path.join(memoryDir, file);
538
+ if (!fs.existsSync(dest)) {
539
+ if (!dryRun) {
540
+ fs.copyFileSync(path.join(memorySrc, file), dest);
541
+ }
542
+ updated.push(dest);
543
+ console.log(` [created] memory/${file}`);
544
+ } else {
545
+ skipped.push(dest);
546
+ }
547
+ }
548
+ }
549
+
550
+
551
+ // ---------------------------------------------------------------------------
552
+ // 9. Migration v1.11.x -> v1.12.0: lane-aware automation scaffold
553
+ //
554
+ // v1.12.0 rewrote the automation scaffold around a lane table: playwright.config.ts
555
+ // and global-setup.ts now require() three CommonJS helpers under e2e/scripts/.
556
+ // init.js installs those for NEW projects only, so existing installations never
557
+ // receive them. This section carries them across.
558
+ //
559
+ // Classification per file (the governing rule is: never break working user code):
560
+ //
561
+ // a) Lane scripts (scripts/lane-config.js, lane-lock.js, global-setup-guards.js)
562
+ // plus playwright.config.ts and global-setup.ts form ONE ATOMIC GROUP,
563
+ // because config/global-setup require() the scripts by path. Installing a
564
+ // subset yields a project where Playwright cannot start at all. The group is
565
+ // installed only when EVERY member is either absent or provably pristine;
566
+ // otherwise the whole group is skipped with a warning and the rest of the
567
+ // upgrade continues. This is a coherent skip, not an abort: the project keeps
568
+ // the working setup it already had, so it is never left half-migrated.
569
+ //
570
+ // b) fixtures/{auth,base,test-helpers}.ts are per-file. They are leaf modules -
571
+ // nothing in the scaffold require()s them by path - so a mixed state is
572
+ // still coherent.
573
+ //
574
+ // c) .env.example is a reference template, never live config (the real file is
575
+ // .env, which this migration never touches). Overwritten when pristine.
576
+ //
577
+ // "Pristine" means the file's normalised sha256 matches a hash this framework
578
+ // actually shipped in some earlier release (SHIPPED_SCAFFOLD_HASHES). Anything
579
+ // else is user-owned and is only ever warned about. The project's recorded
580
+ // frameworkVersion is deliberately NOT used: live installations report values
581
+ // like 1.1.3 and 1.0.0 that do not correspond to any real scaffold release.
582
+ //
583
+ // Files that stay user-owned are additionally scanned for the trace-safety
584
+ // defect (.fill(password) leaks the password into Playwright traces). That is
585
+ // reported with file and line only - never patched, because the surrounding
586
+ // shapes differ per project (passwordSelector vs PASSWORD_SEL) and a bad regex
587
+ // edit to a working login is exactly the breakage this section must avoid.
588
+ // ---------------------------------------------------------------------------
589
+ const AUTOMATION_LANE_SCRIPTS = ['lane-config.js', 'lane-lock.js', 'global-setup-guards.js'];
590
+ const AUTOMATION_FIXTURES = ['auth.ts', 'base.ts', 'test-helpers.ts'];
591
+
592
+ // A bare e2e/ directory is not evidence of an automation setup: section 5 above
593
+ // creates e2e/tests/{helpers/debug,seeds}/ unconditionally. Only migrate a project
594
+ // that actually has a scaffold, i.e. one of the files this migration would replace.
595
+ const hasAutomationScaffold = ['playwright.config.ts', 'global-setup.ts', 'package.json']
596
+ .some((f) => fs.existsSync(path.join(e2eDir, f)));
597
+
598
+ if (hasAutomationScaffold) {
599
+ const scaffoldSrc = path.resolve(__dirname, '..', 'templates', 'automation-scaffold');
600
+
601
+ // --- (a) the atomic lane group -------------------------------------------
602
+ const groupMembers = [
603
+ ...AUTOMATION_LANE_SCRIPTS.map((f) => ({
604
+ rel: `scripts/${f}`,
605
+ src: path.join(scaffoldSrc, 'scripts', f),
606
+ dest: path.join(e2eDir, 'scripts', f),
607
+ key: `automation-scaffold/scripts/${f}`,
608
+ })),
609
+ ...['playwright.config.ts', 'global-setup.ts'].map((f) => ({
610
+ rel: f,
611
+ src: path.join(scaffoldSrc, f),
612
+ dest: path.join(e2eDir, f),
613
+ key: `automation-scaffold/${f}`,
614
+ })),
615
+ ];
616
+
617
+ const blockers = groupMembers.filter((m) => fs.existsSync(m.dest) && !isPristine(m.dest, m.key));
618
+
619
+ if (blockers.length === 0) {
620
+ for (const m of groupMembers) {
621
+ forceWrite(m.dest, fs.readFileSync(m.src, 'utf8'));
622
+ console.log(` [updated] e2e/${m.rel}`);
623
+ }
624
+ } else {
625
+ warnings.push(
626
+ `Lane-aware automation scaffold (v1.12.0) NOT installed - these files are user-owned:\n` +
627
+ blockers.map((m) => ` e2e/${m.rel}`).join('\n') +
628
+ `\n playwright.config.ts and global-setup.ts require() e2e/scripts/*.js, so the\n` +
629
+ ` group is installed all-or-nothing. Your current setup is left working and\n` +
630
+ ` untouched. To adopt lanes, merge these by hand from:\n` +
631
+ ` node_modules/@keber/qa-framework/templates/automation-scaffold/`
632
+ );
633
+ for (const m of groupMembers) skipped.push(m.dest);
634
+ }
635
+
636
+ // --- (b) fixtures, per file ----------------------------------------------
637
+ for (const file of AUTOMATION_FIXTURES) {
638
+ const dest = path.join(e2eDir, 'fixtures', file);
639
+ const key = `automation-scaffold/fixtures/${file}`;
640
+ if (!fs.existsSync(dest) || isPristine(dest, key)) {
641
+ forceWrite(dest, fs.readFileSync(path.join(scaffoldSrc, 'fixtures', file), 'utf8'));
642
+ console.log(` [updated] e2e/fixtures/${file}`);
643
+ } else {
644
+ skipped.push(dest);
645
+ warnings.push(
646
+ `e2e/fixtures/${file} is user-owned - kept as is (v1.12.0 version not applied).`
647
+ );
648
+ }
649
+ }
650
+
651
+ // --- (c) .env.example ------------------------------------------------------
652
+ upgradeReferenceEnvExample(
653
+ path.join(e2eDir, '.env.example'),
654
+ path.join(scaffoldSrc, '.env.example'),
655
+ 'automation-scaffold/.env.example',
656
+ 'e2e/.env.example'
657
+ );
658
+
659
+ // --- trace safety on everything left user-owned ---------------------------
660
+ reportTraceSafety(e2eDir);
661
+ }
662
+
663
+ // --- integration scaffold: no lane coupling, so purely per-file -------------
664
+ // Same reasoning as above: require a real scaffold file, not just the folder.
665
+ const hasIntegrationScaffold = ['playwright.config.ts', 'global-setup.ts', 'package.json']
666
+ .some((f) => fs.existsSync(path.join(integrationDir, f)));
667
+
668
+ if (hasIntegrationScaffold) {
669
+ const integrationSrc = path.resolve(__dirname, '..', 'templates', 'integration-scaffold');
670
+ for (const file of ['playwright.config.ts', 'global-setup.ts']) {
671
+ const dest = path.join(integrationDir, file);
672
+ const key = `integration-scaffold/${file}`;
673
+ if (!fs.existsSync(dest) || isPristine(dest, key)) {
674
+ forceWrite(dest, fs.readFileSync(path.join(integrationSrc, file), 'utf8'));
675
+ console.log(` [updated] integration/${file}`);
676
+ } else {
677
+ skipped.push(dest);
678
+ warnings.push(`integration/${file} is user-owned - kept as is (v1.12.0 version not applied).`);
679
+ }
680
+ }
681
+ upgradeReferenceEnvExample(
682
+ path.join(integrationDir, '.env.example'),
683
+ path.join(integrationSrc, '.env.example'),
684
+ 'integration-scaffold/.env.example',
685
+ 'integration/.env.example'
686
+ );
687
+ reportTraceSafety(integrationDir);
688
+ }
689
+
349
690
  // ---------------------------------------------------------------------------
350
691
  // Summary
351
692
  // ---------------------------------------------------------------------------
@@ -356,7 +697,7 @@ if (updated.length) {
356
697
  }
357
698
  if (skipped.length) {
358
699
  console.log('\n Skipped (already up to date):');
359
- for (const f of skipped) console.log(` — ${path.relative(cwd, f)}`);
700
+ for (const f of skipped) console.log(` - ${path.relative(cwd, f)}`);
360
701
  }
361
702
  if (warnings.length) {
362
703
  console.log('\n Warnings:');
@@ -396,3 +737,93 @@ function copyDirForce(srcDir, destDir) {
396
737
  }
397
738
  }
398
739
 
740
+
741
+ // Normalised content hash: strips a UTF-8 BOM and CRLF so that a file which only
742
+ // differs by line endings or a BOM (both of which tooling rewrites silently) is
743
+ // still recognised as the framework's own output.
744
+ function scaffoldHash(content) {
745
+ const normalised = content.replace(/^/, '').replace(/\r\n/g, '\n');
746
+ return require('crypto').createHash('sha256').update(normalised, 'utf8').digest('hex');
747
+ }
748
+
749
+ // A file is "pristine" when its content is byte-identical (after normalisation) to
750
+ // something this framework actually shipped. A key with no recorded hashes - a file
751
+ // introduced in the current release - can never be pristine, so an existing copy is
752
+ // always treated as user-owned.
753
+ function isPristine(filePath, key) {
754
+ let content;
755
+ try {
756
+ content = fs.readFileSync(filePath, 'utf8');
757
+ } catch {
758
+ return false;
759
+ }
760
+ const hash = scaffoldHash(content);
761
+
762
+ // Already identical to what this release ships: nothing to do, and certainly not a
763
+ // user edit. Computed rather than tabulated so the table never goes stale on release.
764
+ try {
765
+ const current = path.resolve(__dirname, '..', 'templates', ...key.split('/'));
766
+ if (fs.existsSync(current) && scaffoldHash(fs.readFileSync(current, 'utf8')) === hash) return true;
767
+ } catch {
768
+ /* fall through to the historical table */
769
+ }
770
+
771
+ const known = SHIPPED_SCAFFOLD_HASHES[key];
772
+ return Boolean(known) && known.includes(hash);
773
+ }
774
+
775
+ // .env.example is a reference template, never live configuration - the real values
776
+ // live in .env, which is never touched here. Overwrite it when pristine; when the
777
+ // user has edited it, keep theirs and say so.
778
+ function upgradeReferenceEnvExample(dest, src, key, label) {
779
+ if (!fs.existsSync(src)) return;
780
+ if (!fs.existsSync(dest) || isPristine(dest, key)) {
781
+ forceWrite(dest, fs.readFileSync(src, 'utf8'));
782
+ console.log(` [updated] ${label}`);
783
+ } else {
784
+ skipped.push(dest);
785
+ warnings.push(
786
+ `${label} is user-owned - kept as is. New v1.12.0 keys may be missing; compare against\n` +
787
+ ` node_modules/@keber/qa-framework/templates/${key}`
788
+ );
789
+ }
790
+ }
791
+
792
+ // Trace safety (see skills/qa-automation/references/patterns.md, "Pattern 7"):
793
+ // page.locator(...).fill(password) records the password in the Playwright trace.
794
+ // Report location only. The surrounding code differs per project, so an automated
795
+ // rewrite of a working login is more dangerous than the finding itself.
796
+ function reportTraceSafety(rootDir) {
797
+ const offenders = [];
798
+ const FILL_PASSWORD = /\.fill\(\s*(?:password|pwd|[A-Za-z_$]*(?:[Pp]ass(?:word)?|PASSWORD)[A-Za-z_$]*)\s*\)/;
799
+
800
+ (function walk(dir) {
801
+ let entries;
802
+ try {
803
+ entries = fs.readdirSync(dir, { withFileTypes: true });
804
+ } catch {
805
+ return;
806
+ }
807
+ for (const entry of entries) {
808
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
809
+ const full = path.join(dir, entry.name);
810
+ if (entry.isDirectory()) {
811
+ walk(full);
812
+ } else if (/\.ts$/.test(entry.name)) {
813
+ const lines = fs.readFileSync(full, 'utf8').split(/\r?\n/);
814
+ lines.forEach((line, i) => {
815
+ if (FILL_PASSWORD.test(line)) offenders.push(`${path.relative(cwd, full)}:${i + 1}`);
816
+ });
817
+ }
818
+ }
819
+ })(rootDir);
820
+
821
+ if (offenders.length) {
822
+ warnings.push(
823
+ `Trace safety: the password is written with .fill(), which stores it in the\n` +
824
+ ` Playwright trace. Replace with the page.evaluate() form ("Pattern 7" in\n` +
825
+ ` .github/skills/qa-automation/references/patterns.md):\n` +
826
+ offenders.map((o) => ` ${o}`).join('\n')
827
+ );
828
+ }
829
+ }