@keber/qa-framework 1.11.3 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +2 -2
  2. package/docs/architecture.md +30 -26
  3. package/docs/decisions/lane-heartbeat-caller.md +86 -0
  4. package/docs/final-report.md +23 -23
  5. package/docs/folder-structure-guide.md +100 -80
  6. package/docs/installation.md +35 -7
  7. package/docs/skills-architecture.md +23 -23
  8. package/docs/spec-driven-philosophy.md +9 -9
  9. package/docs/usage-with-agent.md +83 -9
  10. package/docs/visuals/diag-01-ejecutivo-1.png +0 -0
  11. package/docs/visuals/diag-02-tactico-1.png +0 -0
  12. package/docs/visuals/diag-03-tecnico-1.png +0 -0
  13. package/docs/visuals/info-01-ejecutivo-1.png +0 -0
  14. package/docs/visuals/info-02-tactico-1.png +0 -0
  15. package/docs/visuals/info-03-tecnico-1.png +0 -0
  16. package/docs/visuals/pres-01-ejecutivo-1.png +0 -0
  17. package/docs/visuals/pres-02-tactico-1.png +0 -0
  18. package/docs/visuals/pres-03-tecnico-1.png +0 -0
  19. package/docs/visuals/qa-01-problema-1.png +0 -0
  20. package/docs/visuals/qa-02-que-es-1.png +0 -0
  21. package/docs/visuals/qa-03-pipeline-1.png +0 -0
  22. package/docs/visuals/qa-04-6-archivos-1.png +0 -0
  23. package/docs/visuals/qa-05-humano-1.png +0 -0
  24. package/docs/visuals/qa-06-agente-1.png +0 -0
  25. package/docs/visuals/qa-07-skills-arch-1.png +0 -0
  26. package/docs/visuals/qa-08-traceability-1.png +0 -0
  27. package/docs/visuals/qa-en-01-problem-1.png +0 -0
  28. package/docs/visuals/qa-en-02-what-is-1.png +0 -0
  29. package/docs/visuals/qa-en-03-pipeline-1.png +0 -0
  30. package/docs/visuals/qa-en-04-6files-1.png +0 -0
  31. package/docs/visuals/qa-en-05-human-1.png +0 -0
  32. package/docs/visuals/qa-en-06-agent-1.png +0 -0
  33. package/docs/visuals/qa-en-07-skills-arch-1.png +0 -0
  34. package/docs/visuals/qa-en-08-traceability-1.png +0 -0
  35. package/examples/module-example/suppliers/00-inventory.md +1 -1
  36. package/examples/module-example/suppliers/suppliers-create.spec.ts +8 -8
  37. package/integrations/ado-powershell/README.md +1 -1
  38. package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
  39. package/integrations/playwright/README.md +4 -4
  40. package/package.json +7 -3
  41. package/qa-framework.config.json +23 -2
  42. package/scripts/check-forbidden-chars.js +187 -0
  43. package/scripts/check-spanish-accents.js +159 -0
  44. package/scripts/cli.js +1 -1
  45. package/scripts/generate.js +2 -2
  46. package/scripts/init.js +55 -7
  47. package/scripts/lib/claude-agents.js +78 -0
  48. package/scripts/lib/claude-commands.js +56 -0
  49. package/scripts/sync-version.js +36 -0
  50. package/scripts/upgrade.js +356 -20
  51. package/scripts/validate.js +17 -17
  52. package/skills/qa-ado-integration/SKILL.md +19 -19
  53. package/skills/qa-ado-integration/references/scripts-and-config.md +6 -6
  54. package/skills/qa-automation/SKILL.md +49 -27
  55. package/skills/qa-automation/references/config-checklist.md +4 -4
  56. package/skills/qa-automation/references/dom-inspection-template.js +7 -7
  57. package/skills/qa-automation/references/patterns.md +20 -20
  58. package/skills/qa-automation/references/pom-template.md +161 -0
  59. package/skills/qa-maintenance/SKILL.md +11 -11
  60. package/skills/qa-maintenance/references/update-rules.md +8 -8
  61. package/skills/qa-module-analysis/SKILL.md +20 -19
  62. package/skills/qa-module-analysis/references/exploration-checklist.md +4 -4
  63. package/skills/qa-module-analysis/references/output-verification.md +69 -0
  64. package/skills/qa-module-analysis/references/spec-file-formats.md +14 -14
  65. package/skills/qa-spec-generation/SKILL.md +13 -7
  66. package/skills/qa-test-cases/SKILL.md +11 -81
  67. package/skills/qa-test-cases/references/test-case-template.md +5 -5
  68. package/skills/qa-test-plan/SKILL.md +22 -86
  69. package/skills/qa-test-plan/references/plan-de-pruebas-template.md +24 -24
  70. package/skills/qa-test-stabilization/SKILL.md +59 -30
  71. package/skills/qa-test-stabilization/references/classification-protocol.md +35 -35
  72. package/templates/agent-next-steps.md +10 -10
  73. package/templates/agents/qa-analisis.md +142 -0
  74. package/templates/agents/qa-asesoria.md +48 -0
  75. package/templates/agents/qa-informe-resultados.md +234 -0
  76. package/templates/agents/qa-plan.md +131 -0
  77. package/templates/automation-scaffold/.env.example +36 -6
  78. package/templates/automation-scaffold/fixtures/auth.ts +27 -5
  79. package/templates/automation-scaffold/fixtures/base.ts +23 -1
  80. package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
  81. package/templates/automation-scaffold/global-setup.ts +117 -48
  82. package/templates/automation-scaffold/playwright.config.ts +72 -32
  83. package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
  84. package/templates/automation-scaffold/scripts/lane-config.js +269 -0
  85. package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
  86. package/templates/defect-report.md +17 -3
  87. package/templates/execution-report.md +9 -9
  88. package/templates/integration-scaffold/.env.example +1 -1
  89. package/templates/integration-scaffold/global-setup.ts +21 -1
  90. package/templates/integration-scaffold/playwright.config.ts +15 -1
  91. package/templates/qa-framework.instructions.md +8 -7
  92. package/templates/qa-framework.rules.md +53 -0
  93. package/templates/qa-readme.md +8 -8
  94. package/templates/session-summary.md +2 -2
  95. package/templates/specification/00-inventory.md +3 -3
  96. package/templates/specification/01-business-rules.md +6 -6
  97. package/templates/specification/02-workflows.md +1 -1
  98. package/templates/specification/03-roles-permissions.md +1 -1
  99. package/templates/specification/04-test-data.md +8 -8
  100. package/templates/specification/05-test-scenarios.md +5 -5
  101. package/templates/test-case.md +3 -3
  102. package/templates/test-data-guidelines.md +7 -7
  103. package/templates/test-plan-sprint.md +13 -13
  104. package/templates/test-plan.md +3 -3
  105. package/skills/qa-test-plan/references/priority-and-feasibility.md +0 -159
@@ -1,12 +1,14 @@
1
1
  # docs/usage-with-agent.md
2
2
 
3
- ## Using `qa-framework` with an IDE Agent (GitHub Copilot)
3
+ ## Using `qa-framework` with an IDE Agent (GitHub Copilot or Claude Code)
4
4
 
5
5
  ---
6
6
 
7
7
  ## Overview
8
8
 
9
- This framework is designed so that an IDE agent (such as GitHub Copilot in VS Code) can:
9
+ `init` and `upgrade` always generate artifacts for both GitHub Copilot and Claude Code
10
+ in parallel - there is no agent detection. This framework is designed so that either
11
+ IDE agent can:
10
12
 
11
13
  1. Understand the QA structure by reading the agent instruction files
12
14
  2. Navigate the `qa/` directory predictably
@@ -42,11 +44,11 @@ This framework is designed so that an IDE agent (such as GitHub Copilot in VS Co
42
44
 
43
45
  ---
44
46
 
45
- ## Setting Up Agent Instructions in VS Code
47
+ ## Setting Up Agent Instructions in VS Code (GitHub Copilot)
46
48
 
47
- ### Option A — Workspace instructions file (recommended)
49
+ ### Option A - Workspace instructions file (recommended)
48
50
 
49
- `init` generates `.github/copilot-instructions.md` automatically. It contains the full
51
+ `init` generates `.github/instructions/qa-framework.instructions.md` automatically. It contains the full
50
52
  pipeline sequencer with 11 agent behavior rules, the QA pipeline table, and ADO detection.
51
53
  No manual setup needed.
52
54
 
@@ -56,7 +58,7 @@ To regenerate after an upgrade:
56
58
  npx qa-framework upgrade
57
59
  ```
58
60
 
59
- ### Option B — Reference skills directly in the agent conversation
61
+ ### Option B - Reference skills directly in the agent conversation
60
62
 
61
63
  Paste a skill path into the Copilot chat:
62
64
 
@@ -64,7 +66,7 @@ Paste a skill path into the Copilot chat:
64
66
  Read .github/skills/qa-module-analysis/SKILL.md and analyze the module at {URL}
65
67
  ```
66
68
 
67
- ### Option C — Individual task prompts
69
+ ### Option C - Individual task prompts
68
70
 
69
71
  For each major QA task, ask the agent to load the relevant skill:
70
72
 
@@ -75,6 +77,77 @@ tests for the submodule {name}
75
77
 
76
78
  ---
77
79
 
80
+ ## Setting Up Agent Instructions in Claude Code
81
+
82
+ `init` and `upgrade` generate two Claude-Code-native artifacts automatically, no manual
83
+ setup needed:
84
+
85
+ - `.claude/rules/qa-framework.md` - the Claude Code equivalent of the Copilot
86
+ `.instructions.md` file. It has no frontmatter, which means it loads unconditionally in
87
+ every session (the same effect as `applyTo: '**'`). It contains the same 11 agent
88
+ behavior rules and pipeline table as the Copilot instructions, with every skill
89
+ reference expressed as a `/qa-{name}` slash command instead of "load this file".
90
+ - `.claude/commands/qa-{name}.md` - one thin wrapper per skill, generated dynamically
91
+ from whatever folders exist under `.github/skills/` at install/upgrade time. Each
92
+ wrapper is a single instruction: `Read the full skill at .github/skills/qa-{name}/SKILL.md
93
+ FIRST, then follow its instructions exactly`, plus the stage prerequisite. There is zero
94
+ duplication of skill content and therefore zero drift - if a skill is updated, every
95
+ command that points to it picks up the change with no regeneration needed.
96
+
97
+ ### Using a command
98
+
99
+ In a Claude Code session, invoke the pipeline stage directly:
100
+
101
+ ```
102
+ /qa-module-analysis
103
+ ```
104
+
105
+ Claude reads `.github/skills/qa-module-analysis/SKILL.md` and follows it exactly, the
106
+ same way the Copilot pipeline table routes to that file.
107
+
108
+ ### Regenerating after an upgrade
109
+
110
+ ```bash
111
+ npx qa-framework upgrade
112
+ ```
113
+
114
+ This refreshes both `.claude/commands/qa-*.md` and `.claude/rules/qa-framework.md` the
115
+ same safe way it refreshes the Copilot artifacts (framework-owned, always overwritten
116
+ with the current version; nothing else under `.claude/` is touched).
117
+
118
+ ---
119
+
120
+ ## Optional: ANALISIS/PLAN Mode (Claude Code only, ADO-gated)
121
+
122
+ Some teams run a sprint-centric manual testing workflow in parallel with the 6-stage
123
+ pipeline: an analysis document, a test plan trace-linked to Azure DevOps work items,
124
+ ad-hoc QA advisory chat, and a sprint closing results report. This mode is:
125
+
126
+ - **Optional** - off by default.
127
+ - **Gated** by `integrations.azureDevOps.sprintCycle.enabled` in
128
+ `qa/qa-framework.config.json` (it requires Azure DevOps enabled, since three of the
129
+ four agents depend on ADO work items or an ADO-generated execution report).
130
+ - **Claude Code only, by design, not by omission.** Each agent is a Claude Code subagent
131
+ with its own `tools`/`model` frontmatter (e.g. `qa-asesoria` intentionally has no
132
+ `Write`/`Bash`). GitHub Copilot has no equivalent primitive, so no `.github/` artifact
133
+ is generated for this mode.
134
+
135
+ When enabled, `init`/`upgrade` generate four subagents under `.claude/agents/`:
136
+
137
+ | Agent | Purpose |
138
+ |---|---|
139
+ | `qa-analisis.md` | Analysis document: test universe, automation feasibility, P0-P3 prioritization, excluded universe, traceability matrix |
140
+ | `qa-plan.md` | Default mode: executive summary + a trace-linked test case table, executable within the configured manual-testing timebox |
141
+ | `qa-asesoria.md` | Chat-only QA advisory (no `Write`/`Bash`) - answers point questions, redirects to `qa-plan`/`qa-analisis` when a full document is actually needed |
142
+ | `qa-informe-resultados.md` | Narrative sprint closing report built from an execution report already produced by the `qa-ado-integration` skill; updates incrementally, never overwrites prior sections |
143
+
144
+ Sprint duration, manual-testing timebox, date format, and timezone are parameterized
145
+ from `integrations.azureDevOps.sprintCycle` (with neutral defaults - 8-day sprint,
146
+ 2-day timebox, `dd-mm-aaaa`, `America/Santiago` - when a field is omitted). See
147
+ [docs/installation.md](installation.md) for the config shape.
148
+
149
+ ---
150
+
78
151
  ## Typical Agent Session Workflow
79
152
 
80
153
  ### Session 1: Initial Module Analysis
@@ -124,7 +197,7 @@ tests for the submodule {name}
124
197
 
125
198
  ### DO
126
199
 
127
- - Read `qa/AGENT-NEXT-STEPS.md` at the start of each conversation (auto-enforced by copilot-instructions.md)
200
+ - Read `qa/AGENT-NEXT-STEPS.md` at the start of each conversation (auto-enforced by `.github/instructions/qa-framework.instructions.md` for Copilot, or `.claude/rules/qa-framework.md` for Claude Code)
128
201
  - Load the relevant skill from `.github/skills/` before starting any QA task
129
202
  - Save all artifacts in the exact path specified by `qa/QA-STRUCTURE-GUIDE.md`
130
203
  - Use TC-ID, RN-ID, FL-ID naming consistently
@@ -181,7 +254,8 @@ This performs:
181
254
  ## Continuing from a Previous Session
182
255
 
183
256
  The agent reads `qa/AGENT-NEXT-STEPS.md` automatically at the start of every conversation
184
- (enforced by `.github/copilot-instructions.md` rule 0). For additional context:
257
+ (enforced by rule 0 in `.github/instructions/qa-framework.instructions.md` for Copilot,
258
+ or `.claude/rules/qa-framework.md` for Claude Code). For additional context:
185
259
 
186
260
  ```
187
261
  "Read qa/README.md and qa/memory/INDEX.md, then tell me where we left off."
Binary file
Binary file
Binary file
@@ -1,4 +1,4 @@
1
- # Suppliers — Inventory
1
+ # Suppliers - Inventory
2
2
 
3
3
  **Module**: Suppliers (SUP)
4
4
  **Submodule**: Create / Edit / List
@@ -15,16 +15,16 @@
15
15
 
16
16
  import { test, expect } from '@playwright/test';
17
17
 
18
- // EXEC_IDX: unique per minute-window — prevents data collisions between runs
18
+ // EXEC_IDX: unique per minute-window - prevents data collisions between runs
19
19
  const EXEC_IDX = Math.floor(Date.now() / 60_000) % 100_000;
20
20
 
21
21
  test.describe('Suppliers > Create @P0', () => {
22
22
 
23
23
  /**
24
- * [TC-SUP-CR-001] Create supplier — happy path @P0
24
+ * [TC-SUP-CR-001] Create supplier - happy path @P0
25
25
  * Verifies the complete create flow for a valid supplier.
26
26
  */
27
- test('[TC-SUP-CR-001] Create supplier — happy path @P0', async ({ page }) => {
27
+ test('[TC-SUP-CR-001] Create supplier - happy path @P0', async ({ page }) => {
28
28
  const supplierName = `QA-Supplier-${EXEC_IDX}`;
29
29
  const supplierRut = `${EXEC_IDX}-K`;
30
30
  const supplierEmail = `qa-supplier-${EXEC_IDX}@example.com`;
@@ -57,10 +57,10 @@ test.describe('Suppliers > Create @P0', () => {
57
57
  });
58
58
 
59
59
  /**
60
- * [TC-SUP-CR-002] Create supplier — required fields validation @P0
60
+ * [TC-SUP-CR-002] Create supplier - required fields validation @P0
61
61
  * Verifies that the form prevents submission when required fields are empty.
62
62
  */
63
- test('[TC-SUP-CR-002] Create supplier — required fields validation @P0', async ({ page }) => {
63
+ test('[TC-SUP-CR-002] Create supplier - required fields validation @P0', async ({ page }) => {
64
64
  await page.goto('/suppliers');
65
65
  await page.locator('button:has-text("New Supplier")').click();
66
66
  await expect(page.locator('.supplier-modal')).toBeVisible();
@@ -79,12 +79,12 @@ test.describe('Suppliers > Create @P0', () => {
79
79
  });
80
80
 
81
81
  /**
82
- * [TC-SUP-CR-003] Create supplier — duplicate RUT rejected @P0
82
+ * [TC-SUP-CR-003] Create supplier - duplicate RUT rejected @P0
83
83
  *
84
- * NOTE: test.skip active — DEF-001: Duplicate RUT check not enforced server-side.
84
+ * NOTE: test.skip active - DEF-001: Duplicate RUT check not enforced server-side.
85
85
  * Reactivate when ADO #99001 is resolved.
86
86
  */
87
- test('[TC-SUP-CR-003] Create supplier — duplicate RUT rejected @P0', async ({ page }) => {
87
+ test('[TC-SUP-CR-003] Create supplier - duplicate RUT rejected @P0', async ({ page }) => {
88
88
  test.skip(true,
89
89
  'DEF-001: Duplicate RUT validation not enforced. Reactivate when ADO #99001 is resolved.'
90
90
  );
@@ -1,4 +1,4 @@
1
- # Integration — ADO PowerShell Scripts
1
+ # Integration - ADO PowerShell Scripts
2
2
 
3
3
  PowerShell scripts for Azure DevOps Test Plan management.
4
4
  All scripts are parameterized and project-agnostic.
@@ -61,7 +61,7 @@ foreach ($entry in $mapping) {
61
61
  Select-Object -First 1
62
62
 
63
63
  if (-not $specFile) {
64
- Write-Warning " [NOT FOUND] $($entry.specFile) — skipping WI $adoId"
64
+ Write-Warning " [NOT FOUND] $($entry.specFile) - skipping WI $adoId"
65
65
  $skipped++
66
66
  continue
67
67
  }
@@ -84,7 +84,7 @@ foreach ($entry in $mapping) {
84
84
 
85
85
  if ($PSCmdlet.ShouldProcess("WI #$adoId", "Update title to: $newTitle")) {
86
86
  Invoke-RestMethod -Method Patch -Uri $patchUrl -Headers $headers -Body $body | Out-Null
87
- Write-Host " [SYNCED] WI #$adoId → $newTitle"
87
+ Write-Host " [SYNCED] WI #$adoId -> $newTitle"
88
88
  $synced++
89
89
  }
90
90
  }
@@ -1,4 +1,4 @@
1
- # Integration — Playwright
1
+ # Integration - Playwright
2
2
 
3
3
  This directory documents how `@playwright/test` is configured within the
4
4
  `keber/qa-framework` opinionated setup.
@@ -40,9 +40,9 @@ suite execution.
40
40
 
41
41
  ## Debugging checklist
42
42
 
43
- 1. `PWDEBUG=1 npx playwright test` — step through in inspector
44
- 2. `--headed` — watch the browser
45
- 3. `--trace on` — record full trace; open with `npx playwright show-trace`
43
+ 1. `PWDEBUG=1 npx playwright test` - step through in inspector
44
+ 2. `--headed` - watch the browser
45
+ 3. `--trace on` - record full trace; open with `npx playwright show-trace`
46
46
  4. Increase `actionTimeout` if the app has slow server-side rendering
47
47
  5. Add `await page.waitForLoadState('networkidle')` before assertions on
48
48
  dynamically loaded content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keber/qa-framework",
3
- "version": "1.11.3",
3
+ "version": "1.12.0",
4
4
  "description": "Reusable spec-driven QA framework for IDE-agent-assisted automated testing. Installable as an npm package. Provides structure, templates, agent instructions, and optional integrations for Playwright and Azure DevOps.",
5
5
  "keywords": [
6
6
  "qa",
@@ -21,7 +21,11 @@
21
21
  "postinstall": "node scripts/init.js --skip-if-exists",
22
22
  "init": "node scripts/cli.js init",
23
23
  "generate": "node scripts/cli.js generate",
24
- "validate": "node scripts/cli.js validate"
24
+ "validate": "node scripts/cli.js validate",
25
+ "check-accents": "node scripts/check-spanish-accents.js",
26
+ "check-chars": "node scripts/check-forbidden-chars.js",
27
+ "test": "node --test test/*.test.js",
28
+ "version": "node scripts/sync-version.js && git add README.md .github/copilot-instructions.md"
25
29
  },
26
30
  "files": [
27
31
  "agent-instructions/",
@@ -36,7 +40,7 @@
36
40
  "qa-framework.config.json"
37
41
  ],
38
42
  "engines": {
39
- "node": ">=18"
43
+ "node": ">=20"
40
44
  },
41
45
  "peerDependencies": {
42
46
  "@keber/ado-qa": ">=1.0.0",
@@ -27,7 +27,7 @@
27
27
  "testCaseIdPattern": "TC-{MODULE}-{SUBMODULE}-{NUM}",
28
28
  "businessRuleIdPattern": "RN-{MODULE}-{NUM}",
29
29
  "workflowIdPattern": "FL-{MODULE}-{NUM}",
30
- "defectIdPattern": "DEF-{NUM}",
30
+ "defectIdPattern": "DEF-{PREFIX}-{NUM}",
31
31
  "testNamingPattern": "[{ID}] {title} @{priority}",
32
32
  "timestampFormat": "YYYY-MM-DD_HH-MM-SS",
33
33
  "tcPriorityLevels": ["P0", "P1", "P2", "P3"]
@@ -42,6 +42,20 @@
42
42
  }
43
43
  ],
44
44
 
45
+ "parallelLanes": {
46
+ "enabled": false,
47
+ "lanes": [
48
+ {
49
+ "id": "1",
50
+ "account": "QA_USER",
51
+ "storageState": ".auth/user-default.json",
52
+ "mcpNamespace": null,
53
+ "runnerProject": "chromium",
54
+ "capabilities": []
55
+ }
56
+ ]
57
+ },
58
+
45
59
  "integrations": {
46
60
  "playwright": {
47
61
  "enabled": false,
@@ -69,7 +83,14 @@
69
83
  "variableGroup": "{{ADO_VARIABLE_GROUP}}",
70
84
  "pipelineFile": "qa/08-azure-integration/pipelines/azure-pipeline-qa.yml",
71
85
  "moduleRegistry": "qa/08-azure-integration/module-registry.json",
72
- "reporterPackage": "@alex_neo/playwright-azure-reporter"
86
+ "reporterPackage": "@alex_neo/playwright-azure-reporter",
87
+ "sprintCycle": {
88
+ "enabled": false,
89
+ "sprintDurationDays": 8,
90
+ "manualTestingTimeboxDays": 2,
91
+ "dateFormat": "dd-mm-aaaa",
92
+ "timezone": "America/Santiago"
93
+ }
73
94
  }
74
95
  },
75
96
 
@@ -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 };