qa-engineer 0.9.2 → 0.11.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 (147) hide show
  1. package/COMPATIBILITY.md +26 -12
  2. package/README.md +182 -84
  3. package/package.json +11 -20
  4. package/packages/engine/bin/qa-engine.mjs +560 -0
  5. package/packages/engine/lib/analysis/branding.mjs +187 -0
  6. package/packages/engine/lib/analysis/context.mjs +294 -0
  7. package/packages/engine/lib/analysis/contracts.mjs +149 -0
  8. package/packages/engine/lib/analysis/diff-guard.mjs +425 -0
  9. package/packages/engine/lib/analysis/discovery.mjs +220 -0
  10. package/packages/engine/lib/analysis/evidence.mjs +116 -0
  11. package/packages/engine/lib/analysis/har.mjs +124 -0
  12. package/packages/engine/lib/analysis/junit.mjs +126 -0
  13. package/packages/engine/lib/analysis/network.mjs +237 -0
  14. package/packages/engine/lib/analysis/redaction.mjs +127 -0
  15. package/packages/engine/lib/analysis/report-html.mjs +76 -0
  16. package/packages/engine/lib/analysis/taxonomy.mjs +109 -0
  17. package/packages/engine/lib/analysis/xml.mjs +153 -0
  18. package/packages/engine/lib/analysis/zip.mjs +107 -0
  19. package/packages/engine/lib/artifacts/manager.mjs +453 -0
  20. package/packages/engine/lib/artifacts/mime.mjs +109 -0
  21. package/packages/engine/lib/artifacts/zip-write.mjs +143 -0
  22. package/packages/engine/lib/diagnostics/engine.mjs +165 -0
  23. package/packages/engine/lib/diagnostics/internal-contracts.mjs +50 -0
  24. package/packages/engine/lib/diagnostics/prioritization.mjs +99 -0
  25. package/packages/engine/lib/diagnostics/repair.mjs +73 -0
  26. package/packages/engine/lib/diagnostics/root-cause.mjs +92 -0
  27. package/packages/engine/lib/diagnostics/timeline.mjs +101 -0
  28. package/packages/engine/lib/frameworks/junit-frameworks.mjs +48 -0
  29. package/packages/engine/lib/frameworks/playwright.mjs +158 -0
  30. package/packages/engine/lib/report/components/charts.mjs +424 -0
  31. package/packages/engine/lib/report/components/evidence.mjs +207 -0
  32. package/packages/engine/lib/report/components/findings.mjs +258 -0
  33. package/packages/engine/lib/report/components/nav.mjs +99 -0
  34. package/packages/engine/lib/report/components/primitives.mjs +246 -0
  35. package/packages/engine/lib/report/components/runtime.mjs +246 -0
  36. package/packages/engine/lib/report/components/timeline.mjs +65 -0
  37. package/packages/engine/lib/report/core/model.mjs +270 -0
  38. package/packages/engine/lib/report/core/normalize.mjs +226 -0
  39. package/packages/engine/lib/report/core/sections.mjs +978 -0
  40. package/packages/engine/lib/report/export/bundle.mjs +293 -0
  41. package/packages/engine/lib/report/export/html.mjs +183 -0
  42. package/packages/engine/lib/report/export/machine.mjs +290 -0
  43. package/packages/engine/lib/report/export/markdown.mjs +323 -0
  44. package/packages/engine/lib/report/schemas/qa-report.schema.json +555 -0
  45. package/packages/engine/lib/report/theme/css.mjs +529 -0
  46. package/packages/engine/lib/report/theme/tokens.mjs +137 -0
  47. package/packages/engine/lib/report/version.mjs +78 -0
  48. package/packages/engine/package.json +14 -0
  49. package/packages/installer/lib/agents/targets.mjs +90 -0
  50. package/packages/installer/lib/agents/user-level.mjs +80 -0
  51. package/packages/installer/lib/cli/flags.mjs +20 -2
  52. package/packages/installer/lib/commands/doctor.mjs +13 -20
  53. package/packages/installer/lib/commands/install.mjs +160 -91
  54. package/packages/installer/lib/commands/repair.mjs +10 -6
  55. package/packages/installer/lib/commands/self-test.mjs +4 -3
  56. package/packages/installer/lib/commands/uninstall.mjs +14 -8
  57. package/packages/installer/lib/commands/update.mjs +9 -4
  58. package/packages/installer/lib/commands/verify.mjs +13 -12
  59. package/packages/installer/lib/constants.mjs +13 -0
  60. package/packages/installer/lib/core/bundle.mjs +69 -92
  61. package/packages/installer/lib/core/conflict.mjs +5 -4
  62. package/packages/installer/lib/core/fs-safe.mjs +146 -6
  63. package/packages/installer/lib/core/integrity.mjs +59 -0
  64. package/packages/installer/lib/core/lockfile.mjs +19 -3
  65. package/packages/installer/lib/core/manifest.mjs +48 -57
  66. package/packages/installer/lib/core/plan.mjs +213 -0
  67. package/packages/installer/lib/core/qa-home.mjs +145 -0
  68. package/packages/installer/lib/core/scope.mjs +274 -0
  69. package/packages/installer/lib/core/validate-install.mjs +49 -31
  70. package/packages/installer/package.json +1 -1
  71. package/packages/installer/schemas/qa-lock.schema.json +119 -21
  72. package/shared/tooling/qa-tool.mjs +161 -0
  73. package/skills/qa-api/SKILL.md +5 -5
  74. package/skills/qa-api/references/deterministic-tooling.md +34 -32
  75. package/skills/qa-api/references/evidence-and-reporting.md +5 -5
  76. package/skills/qa-api/scripts/qa-tool.mjs +162 -0
  77. package/skills/qa-audit/SKILL.md +5 -5
  78. package/skills/qa-audit/references/deterministic-tooling.md +34 -32
  79. package/skills/qa-audit/references/evidence-and-reporting.md +5 -5
  80. package/skills/qa-audit/scripts/qa-tool.mjs +162 -0
  81. package/skills/qa-debug/SKILL.md +6 -6
  82. package/skills/qa-debug/references/deterministic-tooling.md +34 -32
  83. package/skills/qa-debug/references/diagnostic-engine.md +3 -3
  84. package/skills/qa-debug/references/evidence-and-reporting.md +5 -5
  85. package/skills/qa-debug/scripts/qa-tool.mjs +162 -0
  86. package/skills/qa-explore/SKILL.md +31 -15
  87. package/skills/qa-explore/contracts/explore-result.schema.json +517 -11
  88. package/skills/qa-explore/references/api-replay.md +40 -1
  89. package/skills/qa-explore/references/deterministic-tooling.md +34 -32
  90. package/skills/qa-explore/references/evidence-and-reporting.md +5 -5
  91. package/skills/qa-explore/references/report-pipeline.md +266 -96
  92. package/skills/qa-explore/scripts/qa-tool.mjs +162 -0
  93. package/skills/qa-fix/SKILL.md +4 -4
  94. package/skills/qa-fix/references/deterministic-tooling.md +34 -32
  95. package/skills/qa-fix/references/diagnostic-engine.md +3 -3
  96. package/skills/qa-fix/references/evidence-and-reporting.md +5 -5
  97. package/skills/qa-fix/scripts/qa-tool.mjs +162 -0
  98. package/skills/qa-flaky/SKILL.md +4 -4
  99. package/skills/qa-flaky/references/deterministic-tooling.md +34 -32
  100. package/skills/qa-flaky/references/evidence-and-reporting.md +5 -5
  101. package/skills/qa-flaky/scripts/qa-tool.mjs +162 -0
  102. package/skills/qa-generate/references/evidence-and-reporting.md +5 -5
  103. package/skills/qa-init/SKILL.md +4 -4
  104. package/skills/qa-init/references/deterministic-tooling.md +34 -32
  105. package/skills/qa-init/references/evidence-and-reporting.md +5 -5
  106. package/skills/qa-init/scripts/qa-tool.mjs +162 -0
  107. package/skills/qa-report/SKILL.md +7 -7
  108. package/skills/qa-report/references/deterministic-tooling.md +34 -32
  109. package/skills/qa-report/references/diagnostic-engine.md +3 -3
  110. package/skills/qa-report/references/evidence-and-reporting.md +5 -5
  111. package/skills/qa-report/scripts/qa-tool.mjs +162 -0
  112. package/skills/qa-review/references/evidence-and-reporting.md +5 -5
  113. package/skills/qa-run/SKILL.md +6 -6
  114. package/skills/qa-run/references/deterministic-tooling.md +34 -32
  115. package/skills/qa-run/references/evidence-and-reporting.md +5 -5
  116. package/skills/qa-run/scripts/qa-tool.mjs +162 -0
  117. package/shared/analysis/lib/qa_analysis/__init__.py +0 -11
  118. package/shared/analysis/lib/qa_analysis/branding.py +0 -175
  119. package/shared/analysis/lib/qa_analysis/cli.py +0 -144
  120. package/shared/analysis/lib/qa_analysis/context.py +0 -233
  121. package/shared/analysis/lib/qa_analysis/contracts.py +0 -158
  122. package/shared/analysis/lib/qa_analysis/diff_guard.py +0 -327
  123. package/shared/analysis/lib/qa_analysis/discovery.py +0 -113
  124. package/shared/analysis/lib/qa_analysis/evidence.py +0 -128
  125. package/shared/analysis/lib/qa_analysis/har.py +0 -87
  126. package/shared/analysis/lib/qa_analysis/junit.py +0 -104
  127. package/shared/analysis/lib/qa_analysis/redaction.py +0 -107
  128. package/shared/analysis/lib/qa_analysis/report_html.py +0 -781
  129. package/shared/analysis/lib/qa_analysis/taxonomy.py +0 -98
  130. package/shared/diagnostics/lib/qa_diagnostics/__init__.py +0 -13
  131. package/shared/diagnostics/lib/qa_diagnostics/cli.py +0 -124
  132. package/shared/diagnostics/lib/qa_diagnostics/engine.py +0 -143
  133. package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +0 -75
  134. package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +0 -101
  135. package/shared/diagnostics/lib/qa_diagnostics/repair.py +0 -72
  136. package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +0 -89
  137. package/shared/diagnostics/lib/qa_diagnostics/timeline.py +0 -71
  138. package/shared/frameworks/cypress/lib/cypress_analysis.py +0 -27
  139. package/shared/frameworks/playwright/lib/playwright_analysis.py +0 -167
  140. package/shared/frameworks/selenium/lib/selenium_analysis.py +0 -28
  141. package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +0 -24
  142. package/shared/tooling/qa_tool.py +0 -127
  143. /package/{shared/analysis/lib/qa_analysis → packages/engine/lib/analysis}/branding.json +0 -0
  144. /package/{shared → packages/engine/lib}/analysis/schemas/context.schema.json +0 -0
  145. /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/analysis-result.schema.json +0 -0
  146. /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/diagnosis.schema.json +0 -0
  147. /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/execution-result-min.schema.json +0 -0
@@ -4,9 +4,9 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { LOCKFILE, SHARED_SKILLS_DIR, CLAUDE_SKILLS_DIR } from '../constants.mjs';
6
6
  import { readLock } from './lockfile.mjs';
7
- import { hashFile } from './hash.mjs';
8
- import { findPython, verifyImports, packHasBundles } from './bundle.mjs';
9
- import { BUNDLE_DEST, BUNDLE_MANIFEST, bundlePackagesForSkill } from './manifest.mjs';
7
+ import { entryDigest } from './integrity.mjs';
8
+ import { verifyEngine, packHasBundles } from './bundle.mjs';
9
+ import { BUNDLE_DEST, BUNDLE_MANIFEST } from './manifest.mjs';
10
10
 
11
11
  /**
12
12
  * @typedef {{ id: string, ok: boolean, hard: boolean, message: string, hint?: string }} CheckResult
@@ -17,17 +17,18 @@ import { BUNDLE_DEST, BUNDLE_MANIFEST, bundlePackagesForSkill } from './manifest
17
17
  * @param {string} projectRoot
18
18
  * @returns {{ ok: boolean, checks: CheckResult[] }}
19
19
  */
20
- export function validateInstall(projectRoot) {
20
+ export function validateInstall(projectRoot, { scope = null } = {}) {
21
21
  /** @type {CheckResult[]} */
22
22
  const checks = [];
23
23
 
24
- const lock = readLock(projectRoot);
24
+ const lockRelative = scope?.lockfile ?? LOCKFILE;
25
+ const lock = readLock(projectRoot, lockRelative);
25
26
  if (!lock) {
26
27
  checks.push({
27
28
  id: 'lockfile',
28
29
  ok: false,
29
30
  hard: true,
30
- message: `${LOCKFILE} missing`,
31
+ message: `${lockRelative} missing`,
31
32
  hint: 'run: qa install',
32
33
  });
33
34
  return { ok: false, checks };
@@ -37,17 +38,12 @@ export function validateInstall(projectRoot) {
37
38
  id: 'lockfile',
38
39
  ok: true,
39
40
  hard: true,
40
- message: `${LOCKFILE} present (${lock.files.length} files, pack ${lock.pack?.version ?? '?'})`,
41
+ message: `${lockRelative} present (${lock.files.length} files, pack ${lock.pack?.version ?? '?'})`,
41
42
  });
42
43
 
43
44
  let drift = 0;
44
45
  for (const entry of lock.files) {
45
- const abs = path.join(projectRoot, entry.path);
46
- if (!fs.existsSync(abs)) {
47
- drift += 1;
48
- continue;
49
- }
50
- if (hashFile(abs) !== entry.sha256) drift += 1;
46
+ if (entryDigest(projectRoot, entry) !== entry.sha256) drift += 1;
51
47
  }
52
48
  checks.push({
53
49
  id: 'integrity',
@@ -92,7 +88,36 @@ export function validateInstall(projectRoot) {
92
88
  : 'no contract schemas found (optional for minimal installs)',
93
89
  });
94
90
 
95
- if (packHasBundles()) {
91
+ // Where the engine should be depends on how it was installed: a shared scope has
92
+ // exactly one copy in its qaRoot, a project install has one inside each bundling
93
+ // skill. Looking only for the second is what made the first report a broken install.
94
+ const sharedScope = scope?.shareEngine || lock.scope?.sharedEngine;
95
+ if (sharedScope) {
96
+ const qaRootRelative = scope?.qaRootRelative ?? lock.scope?.qaRoot ?? '.';
97
+ const libDir = path.join(projectRoot, qaRootRelative, 'engine');
98
+ const bundleOk = fs.existsSync(path.join(libDir, 'bin', 'qa-engine.mjs'));
99
+ checks.push({
100
+ id: 'engine',
101
+ ok: bundleOk,
102
+ hard: true,
103
+ message: bundleOk
104
+ ? `deterministic engine shared at ${path.relative(projectRoot, libDir) || '.'}`
105
+ : 'shared deterministic engine missing',
106
+ hint: bundleOk ? undefined : 'run: qa repair',
107
+ });
108
+ if (bundleOk) {
109
+ const result = verifyEngine({ libDir });
110
+ checks.push({
111
+ id: 'engine-runs',
112
+ ok: result.ok,
113
+ hard: true,
114
+ message: result.ok
115
+ ? `shared engine runs (node ${process.versions.node})`
116
+ : `shared engine failed to run: ${result.stderr || 'unknown error'}`,
117
+ hint: result.ok ? undefined : 'run: qa repair',
118
+ });
119
+ }
120
+ } else if (packHasBundles()) {
96
121
  const bundledSkill = Object.keys(BUNDLE_MANIFEST)[0];
97
122
  let libDir = null;
98
123
  for (const base of [SHARED_SKILLS_DIR, CLAUDE_SKILLS_DIR]) {
@@ -114,28 +139,21 @@ export function validateInstall(projectRoot) {
114
139
  hint: bundleOk ? undefined : 'run: qa repair',
115
140
  });
116
141
 
117
- const python = findPython();
118
- if (bundleOk && python) {
119
- const packages = bundlePackagesForSkill(bundledSkill);
120
- const result = verifyImports({ pythonBin: python.bin, libDir, packages });
142
+ // The engine runs under the Node that is already here — no second runtime to
143
+ // find, and nothing to skip when it is absent.
144
+ if (bundleOk) {
145
+ const result = verifyEngine({ libDir });
121
146
  checks.push({
122
- id: 'python-imports',
147
+ id: 'engine-runs',
123
148
  ok: result.ok,
124
- hard: false,
149
+ hard: true,
125
150
  message: result.ok
126
- ? `Python imports OK (${python.bin} ${python.version})`
127
- : `Python import check failed: ${result.stderr || 'unknown error'}`,
128
- hint: result.ok ? undefined : 'install Python 3.8+ or run: qa doctor',
129
- });
130
- } else if (bundleOk && !python) {
131
- checks.push({
132
- id: 'python-imports',
133
- ok: false,
134
- hard: false,
135
- message: 'Python not found — analysis engine import check skipped',
136
- hint: 'install Python 3.8+ for full diagnostics',
151
+ ? `bundled engine runs (node ${process.versions.node})`
152
+ : `bundled engine failed to run: ${result.stderr || 'unknown error'}`,
153
+ hint: result.ok ? undefined : 'run: qa repair',
137
154
  });
138
155
  }
156
+
139
157
  }
140
158
 
141
159
  const major = Number(process.versions.node.split('.')[0]);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qa-engineer/installer",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "description": "The qa-engineer installer: agent detection, copy-based install, lockfile integrity, and wrapper generation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -5,7 +5,14 @@
5
5
  "description": "qa-lock.json records exactly what was installed and a hash per file, so an install can be verified, updated, or removed without guessing. Written by the installer; not hand-edited.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["lockfileVersion", "pack", "installer", "generatedAt", "agents", "files"],
8
+ "required": [
9
+ "lockfileVersion",
10
+ "pack",
11
+ "installer",
12
+ "generatedAt",
13
+ "agents",
14
+ "files"
15
+ ],
9
16
  "properties": {
10
17
  "lockfileVersion": {
11
18
  "type": "integer",
@@ -14,11 +21,22 @@
14
21
  "pack": {
15
22
  "type": "object",
16
23
  "additionalProperties": false,
17
- "required": ["name", "version", "specRevision"],
24
+ "required": [
25
+ "name",
26
+ "version",
27
+ "specRevision"
28
+ ],
18
29
  "properties": {
19
- "name": { "type": "string" },
20
- "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+" },
21
- "specRevision": { "type": "string" }
30
+ "name": {
31
+ "type": "string"
32
+ },
33
+ "version": {
34
+ "type": "string",
35
+ "pattern": "^\\d+\\.\\d+\\.\\d+"
36
+ },
37
+ "specRevision": {
38
+ "type": "string"
39
+ }
22
40
  }
23
41
  },
24
42
  "installer": {
@@ -35,17 +53,31 @@
35
53
  "items": {
36
54
  "type": "object",
37
55
  "additionalProperties": false,
38
- "required": ["id", "name", "tier"],
56
+ "required": [
57
+ "id",
58
+ "name",
59
+ "tier"
60
+ ],
39
61
  "properties": {
40
- "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
41
- "name": { "type": "string" },
62
+ "id": {
63
+ "type": "string",
64
+ "pattern": "^[a-z][a-z0-9-]*$"
65
+ },
66
+ "name": {
67
+ "type": "string"
68
+ },
42
69
  "tier": {
43
- "type": ["integer", "null"],
70
+ "type": [
71
+ "integer",
72
+ "null"
73
+ ],
44
74
  "minimum": 1,
45
75
  "maximum": 2,
46
- "description": "Support tier, or null when the host is unknown the shared Agent Skills path was installed without identifying a product."
76
+ "description": "Support tier, or null when the host is unknown \u2014 the shared Agent Skills path was installed without identifying a product."
77
+ },
78
+ "skillsDir": {
79
+ "type": "string"
47
80
  },
48
- "skillsDir": { "type": "string" },
49
81
  "detected": {
50
82
  "type": "boolean",
51
83
  "description": "True only when project markers identified this host. False when it was requested explicitly or stands in as the unknown-agent path. Optional for lockfiles written before this field existed."
@@ -59,7 +91,12 @@
59
91
  "items": {
60
92
  "type": "object",
61
93
  "additionalProperties": false,
62
- "required": ["path", "sha256", "bytes", "owner"],
94
+ "required": [
95
+ "path",
96
+ "sha256",
97
+ "bytes",
98
+ "owner"
99
+ ],
63
100
  "properties": {
64
101
  "path": {
65
102
  "type": "string",
@@ -67,11 +104,36 @@
67
104
  "description": "Project-relative POSIX path. Must not be absolute and must not contain a '..' segment: a lockfile travels with a cloned repository, and uninstall deletes what it lists.",
68
105
  "pattern": "^(?!/)(?!.*(^|/)\\.\\.($|/)).+$"
69
106
  },
70
- "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
71
- "bytes": { "type": "integer", "minimum": 0 },
72
- "owner": { "type": "string", "enum": ["skill", "wrapper", "config"] },
73
- "skill": { "type": "string" },
74
- "agent": { "type": "string" }
107
+ "sha256": {
108
+ "type": "string",
109
+ "pattern": "^[0-9a-f]{64}$"
110
+ },
111
+ "bytes": {
112
+ "type": "integer",
113
+ "minimum": 0
114
+ },
115
+ "owner": {
116
+ "type": "string",
117
+ "enum": [
118
+ "skill",
119
+ "wrapper",
120
+ "config",
121
+ "engine",
122
+ "link"
123
+ ],
124
+ "description": "What created the entry. `engine` is the shared deterministic engine, present once per scope. `link` is a symbolic link into the canonical skill tree, whose sha256 is the hash of its scope-relative target rather than of file content \u2014 a link has no content, and hashing the target is what lets verify catch one that has been repointed."
125
+ },
126
+ "skill": {
127
+ "type": "string"
128
+ },
129
+ "agent": {
130
+ "type": "string"
131
+ },
132
+ "linkTarget": {
133
+ "type": "string",
134
+ "minLength": 1,
135
+ "description": "Absolute path a `link` entry points at. Absolute because the link is created with it verbatim; the hashed form in sha256 is scope-relative so the lockfile stays machine-independent."
136
+ }
75
137
  }
76
138
  }
77
139
  },
@@ -79,10 +141,46 @@
79
141
  "type": "object",
80
142
  "additionalProperties": false,
81
143
  "properties": {
82
- "skills": { "type": "integer", "minimum": 0 },
83
- "files": { "type": "integer", "minimum": 0 },
84
- "wrappers": { "type": "integer", "minimum": 0 },
85
- "directories": { "type": "integer", "minimum": 0 }
144
+ "skills": {
145
+ "type": "integer",
146
+ "minimum": 0
147
+ },
148
+ "files": {
149
+ "type": "integer",
150
+ "minimum": 0
151
+ },
152
+ "wrappers": {
153
+ "type": "integer",
154
+ "minimum": 0
155
+ },
156
+ "directories": {
157
+ "type": "integer",
158
+ "minimum": 0
159
+ }
160
+ }
161
+ },
162
+ "scope": {
163
+ "type": "object",
164
+ "additionalProperties": false,
165
+ "required": [
166
+ "kind"
167
+ ],
168
+ "description": "Which installation mode wrote this lockfile. Absent means `project`: before 0.11 that was the only mode, so its absence is a fact rather than a gap.",
169
+ "properties": {
170
+ "kind": {
171
+ "enum": [
172
+ "global",
173
+ "workspace",
174
+ "project"
175
+ ]
176
+ },
177
+ "qaRoot": {
178
+ "type": "string",
179
+ "description": "Scope-relative directory holding the shared engine and canonical skills. Absent for a project install, which shares nothing."
180
+ },
181
+ "sharedEngine": {
182
+ "type": "boolean"
183
+ }
86
184
  }
87
185
  }
88
186
  }
@@ -0,0 +1,161 @@
1
+ // The launcher every skill invokes to reach the deterministic engine.
2
+ //
3
+ // A skill's SKILL.md documents exactly one command shape:
4
+ //
5
+ // node <SKILL_DIR>/scripts/qa-tool.mjs <tool> <subcommand> [args]
6
+ //
7
+ // and this file finds the engine, wherever it happens to be. That indirection
8
+ // exists because the pack is installed three different ways and the engine lands in
9
+ // a different place each time:
10
+ //
11
+ // 1. `qa install` bundles the engine into the skill, at ./lib/. Offline, fastest,
12
+ // and pinned to the version that was installed.
13
+ // 2. `npx skills add <owner>/<repo>` — or any generic file copier — copies the
14
+ // skill directory out of git and bundles nothing. The engine is then resolved
15
+ // from node_modules if the project happens to depend on the pack.
16
+ // 3. Neither: fall back to `npx qa-engineer`, which fetches the published package
17
+ // on first use and is served from the npm cache afterwards.
18
+ //
19
+ // Because this file is committed rather than generated, path 2 works at all — which
20
+ // is what makes the pack installable by the wider Agent Skills ecosystem. The
21
+ // command a skill runs never changes; only where the engine came from does.
22
+ //
23
+ // Exit codes pass through unchanged: 0 success, 1 an invalid contract, 2 unreadable
24
+ // input or bad usage.
25
+ //
26
+ // No shebang: the synced copies carry a provenance marker on line one, which would
27
+ // sit above it and stop the kernel seeing it anyway. Every documented invocation is
28
+ // `node qa-tool.mjs …`, which needs none.
29
+
30
+ import fs from 'node:fs';
31
+ import os from 'node:os';
32
+ import path from 'node:path';
33
+ import { spawnSync } from 'node:child_process';
34
+ import { fileURLToPath } from 'node:url';
35
+
36
+ const here = path.dirname(fileURLToPath(import.meta.url));
37
+
38
+ const USAGE = `usage: node qa-tool.mjs <tool> <subcommand> [args]
39
+
40
+ analysis parse artifacts, classify errors, validate contracts, diff-guard,
41
+ read .qa/context.md, render an HTML report, print the footer
42
+ diagnostics root cause, timeline, priority, repair plans, release readiness
43
+ playwright normalize a Playwright report or summarize a trace
44
+
45
+ --where print how the engine was resolved, and stop
46
+
47
+ examples:
48
+ node qa-tool.mjs analysis junit test-results/results.xml
49
+ node qa-tool.mjs analysis report-html qa-artifacts/explore-result.json --out report.html
50
+ node qa-tool.mjs diagnostics report --execution-result qa-artifacts/run.json
51
+ `;
52
+
53
+ /**
54
+ * Where the engine is, and how we found it.
55
+ *
56
+ * Ordered by cost: a bundled copy needs no resolution and no network, a shared copy
57
+ * needs one stat per ancestor, a node_modules copy needs no network, and npx needs both
58
+ * on first use. Reporting *which* one answered matters when a skill degrades — "the
59
+ * engine is missing" and "the engine is being fetched" are different problems.
60
+ *
61
+ * The shared lookup is what lets one skill directory serve a project, a workspace, and a
62
+ * machine-wide install without knowing which installed it. Walking up for
63
+ * `.qa-engineer/engine` finds a workspace install from a skill inside the repository,
64
+ * and finds a global install from a skill linked into an agent's user-level directory —
65
+ * `~/.claude/skills/qa-explore` walks up to `~`, where `~/.qa-engineer/engine` is.
66
+ */
67
+ function resolveEngine() {
68
+ const bundled = path.join(here, 'lib', 'bin', 'qa-engine.mjs');
69
+ if (fs.existsSync(bundled)) return { kind: 'bundled', command: [process.execPath, bundled] };
70
+
71
+ for (const root of candidateSharedRoots()) {
72
+ const shared = path.join(root, 'bin', 'qa-engine.mjs');
73
+ if (fs.existsSync(shared)) return { kind: 'shared', command: [process.execPath, shared] };
74
+ }
75
+
76
+ for (const base of candidateModuleRoots()) {
77
+ const installed = path.join(base, 'qa-engineer', 'packages', 'engine', 'bin', 'qa-engine.mjs');
78
+ if (fs.existsSync(installed)) {
79
+ return { kind: 'node_modules', command: [process.execPath, installed] };
80
+ }
81
+ }
82
+
83
+ return {
84
+ kind: 'npx',
85
+ command: [npxCommand(), '--yes', 'qa-engineer', 'engine'],
86
+ };
87
+ }
88
+
89
+ /** Shared engine directories worth checking, most specific first. */
90
+ function candidateSharedRoots() {
91
+ const roots = [];
92
+
93
+ // An explicit home wins over anything discovered, so a user who moved the install can
94
+ // rely on it rather than on whatever the walk happens to find first.
95
+ const override = process.env.QA_ENGINEER_HOME;
96
+ if (override && override.trim()) roots.push(path.join(path.resolve(override.trim()), 'engine'));
97
+
98
+ let dir = here;
99
+ for (let depth = 0; depth < 12; depth += 1) {
100
+ roots.push(path.join(dir, '.qa-engineer', 'engine'));
101
+ const parent = path.dirname(dir);
102
+ if (parent === dir) break;
103
+ dir = parent;
104
+ }
105
+
106
+ // The default machine home, for the case where the skill lives outside it entirely.
107
+ const home = os.homedir();
108
+ if (home) roots.push(path.join(home, '.qa-engineer', 'engine'));
109
+
110
+ return roots;
111
+ }
112
+
113
+ /** node_modules directories worth checking, nearest first. */
114
+ function candidateModuleRoots() {
115
+ const roots = [];
116
+ let dir = here;
117
+ for (let depth = 0; depth < 12; depth += 1) {
118
+ roots.push(path.join(dir, 'node_modules'));
119
+ const parent = path.dirname(dir);
120
+ if (parent === dir) break;
121
+ dir = parent;
122
+ }
123
+ if (process.cwd() !== here) roots.push(path.join(process.cwd(), 'node_modules'));
124
+ return roots;
125
+ }
126
+
127
+ // `npx` is a shell script on POSIX and a .cmd shim on Windows; spawnSync needs the
128
+ // exact name, and `shell: true` would put user-supplied arguments through a shell.
129
+ function npxCommand() {
130
+ return process.platform === 'win32' ? 'npx.cmd' : 'npx';
131
+ }
132
+
133
+ function main(argv) {
134
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
135
+ process.stdout.write(USAGE);
136
+ return argv.length === 0 ? 2 : 0;
137
+ }
138
+
139
+ const engine = resolveEngine();
140
+
141
+ if (argv[0] === '--where') {
142
+ process.stdout.write(`${JSON.stringify({ resolved: engine.kind, command: engine.command }, null, 2)}\n`);
143
+ return 0;
144
+ }
145
+
146
+ const [program, ...prefix] = engine.command;
147
+ const run = spawnSync(program, [...prefix, ...argv], { stdio: 'inherit' });
148
+
149
+ if (run.error) {
150
+ // Say which path was tried and what to do, because a skill's fallback prose
151
+ // cannot diagnose this and the user is the one who has to fix it.
152
+ const advice = engine.kind === 'npx'
153
+ ? 'the engine is not bundled and npx is unavailable — run `npx qa-engineer install` in this project, or install Node 18+'
154
+ : `could not execute ${program}`;
155
+ process.stderr.write(`qa-tool: ${advice}\n${run.error.message}\n`);
156
+ return 2;
157
+ }
158
+ return run.status ?? 2;
159
+ }
160
+
161
+ process.exitCode = main(process.argv.slice(2));
@@ -53,15 +53,15 @@ Do not use it to generate API tests (`/qa-generate`) or to run a suite (`/qa-run
53
53
 
54
54
  ## Tooling
55
55
 
56
- Invoke the bundled engine through its launcher, as documented in [references/deterministic-tooling.md](references/deterministic-tooling.md). `SKILL_DIR` below is this skill's own directory — `.agents/skills/qa-api` or `.claude/skills/qa-api`, whichever exists. The command shape is the same in bash, zsh, PowerShell, and cmd.exe; on Windows use `python` if `python3` is not on PATH.
56
+ Invoke the bundled engine through its launcher, as documented in [references/deterministic-tooling.md](references/deterministic-tooling.md). `SKILL_DIR` below is this skill's own directory — `.agents/skills/qa-api` or `.claude/skills/qa-api`, whichever exists. The command shape is the same in bash, zsh, PowerShell, and cmd.exe, and it runs under the same Node that installed the pack — there is no second runtime to find.
57
57
 
58
58
  | Tool | Invocation | Output | Fallback |
59
59
  | --- | --- | --- | --- |
60
- | HAR analyzer | `python3 <SKILL_DIR>/scripts/qa_tool.py analysis har <file.har> [--slow-ms N]` | Redacted request/response summary, failures, slow calls | Assess the tests without traffic and say so |
61
- | Error classifier | `python3 <SKILL_DIR>/scripts/qa_tool.py analysis classify "<message>" --http-status <N>` | Taxonomy classification for a failing call | Classify from the REST/GraphQL modules and lower confidence |
62
- | Redaction | `python3 <SKILL_DIR>/scripts/qa_tool.py analysis redact <file>` | The file's text with credentials masked, before anything is quoted | Do not quote captured traffic at all |
60
+ | HAR analyzer | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis har <file.har> [--slow-ms N]` | Redacted request/response summary, failures, slow calls | Assess the tests without traffic and say so |
61
+ | Error classifier | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis classify "<message>" --http-status <N>` | Taxonomy classification for a failing call | Classify from the REST/GraphQL modules and lower confidence |
62
+ | Redaction | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis redact <file>` | The file's text with credentials masked, before anything is quoted | Do not quote captured traffic at all |
63
63
 
64
- A missing `qa_tool.py` means the engine is not installed. Never paste raw captured traffic into a report — redaction is deterministic and must not be undone.
64
+ A missing `qa-tool.mjs` means the engine is not installed. Never paste raw captured traffic into a report — redaction is deterministic and must not be undone.
65
65
 
66
66
  ## Output
67
67
 
@@ -17,7 +17,7 @@ resolves its own location. There is nothing to set up and no shell features are
17
17
  involved:
18
18
 
19
19
  ```bash
20
- python3 <skill-dir>/scripts/qa_tool.py <tool> <subcommand> [args]
20
+ node <skill-dir>/scripts/qa-tool.mjs <tool> <subcommand> [args]
21
21
  ```
22
22
 
23
23
  `<skill-dir>` is wherever the host installed this skill — usually
@@ -25,19 +25,21 @@ python3 <skill-dir>/scripts/qa_tool.py <tool> <subcommand> [args]
25
25
  whichever exists.
26
26
 
27
27
  ```bash
28
- python3 .agents/skills/qa-run/scripts/qa_tool.py analysis junit test-results/results.xml
28
+ node .agents/skills/qa-run/scripts/qa-tool.mjs analysis junit test-results/results.xml
29
29
  ```
30
30
 
31
- That line is identical in bash, zsh, PowerShell, and cmd.exe. **On Windows, use
32
- `python` if `python3` is not on PATH** that is the only platform difference.
31
+ That line is identical in bash, zsh, PowerShell, and cmd.exe, with no platform
32
+ difference at all: it runs under the same Node the user already had to install the
33
+ pack.
33
34
 
34
- An earlier version of this contract used a shell recipe
35
- (`QA_LIB="$(ls -d … | head -1)"` with a `PYTHONPATH=` prefix). It was POSIX-only,
36
- so on Windows every deterministic call failed, each skill fell back to its manual
37
- path, and the user silently got guesswork while believing the tooling had run.
38
- Never reintroduce a shell-dependent invocation.
35
+ Two earlier versions of this contract failed the same way. The first was a shell
36
+ recipe (`QA_LIB="$(ls -d … | head -1)"` with a `PYTHONPATH=` prefix), POSIX-only, so
37
+ on Windows every deterministic call failed and each skill fell back to guesswork
38
+ while appearing to have run its tooling. The second was portable but needed a Python
39
+ interpreter the user never agreed to install. Never reintroduce a shell-dependent
40
+ invocation, and never add a runtime the install did not already require.
39
41
 
40
- If `qa_tool.py` is missing, the engine is not installed: say so, recommend
42
+ If `qa-tool.mjs` is missing, the engine is not installed: say so, recommend
41
43
  `qa repair`, use the skill's documented fallback, and mark the result degraded.
42
44
 
43
45
  Every tool writes JSON to stdout. Exit `0` means success; exit `1` means an
@@ -45,35 +47,35 @@ invalid contract; exit `2` means unreadable input, a malformed artifact, or a
45
47
  payload that failed its seam contract, and the JSON body carries `error` and
46
48
  `detail`. Treat a non-zero exit as missing evidence, never as a value to guess.
47
49
 
48
- Standard-library Python 3.8+ only — nothing to install.
50
+ Dependency-free Node — nothing to install beyond the Node that ran `npx`.
49
51
 
50
- ## 2. Analysis core — `qa_tool.py analysis`
52
+ ## 2. Analysis core — `qa-tool.mjs analysis`
51
53
 
52
54
  Framework-agnostic parsing, redaction, and validation.
53
55
 
54
56
  | Subcommand | Invocation | Returns |
55
57
  | --- | --- | --- |
56
- | `junit` | `python3 <skill-dir>/scripts/qa_tool.py analysis junit <report.xml>` | `{tests: {...}, executed: [...]}` normalized counts and per-test outcomes |
57
- | `har` | `python3 <skill-dir>/scripts/qa_tool.py analysis har <file.har> [--slow-ms N]` | Redacted request/response summary, failures, slow calls |
58
- | `discover` | `python3 <skill-dir>/scripts/qa_tool.py analysis discover [--root DIR] [--path P]` | Artifacts found, by type, with presence flags |
59
- | `diff-guard` | `python3 <skill-dir>/scripts/qa_tool.py analysis diff-guard <diff-file>` | `{issues: [...], safe: bool}` — `safe:false` blocks the change |
60
- | `redact` | `python3 <skill-dir>/scripts/qa_tool.py analysis redact <file>` | The file's text with credentials masked |
61
- | `validate` | `python3 <skill-dir>/scripts/qa_tool.py analysis validate <instance.json> <schema.json>` | `{valid: bool, errors: [...]}`; exit 1 when invalid |
62
- | `classify` | `python3 <skill-dir>/scripts/qa_tool.py analysis classify "<error message>" [--http-status N]` | `{classification, confidence, reason}` from the shared taxonomy |
63
- | `context` | `python3 <skill-dir>/scripts/qa_tool.py analysis context [--root DIR] [--path .qa/context.md]` | The parsed, schema-validated project context as JSON |
64
- | `report-html` | `python3 <skill-dir>/scripts/qa_tool.py analysis report-html <result.json> [--out report.html]` | The result rendered as one self-contained HTML report — every required field, footer included. Run it instead of writing HTML |
65
- | `branding` | `python3 <skill-dir>/scripts/qa_tool.py analysis branding --format markdown\|html\|text` | The exact attribution footer bytes for a **rendered** report (never for a JSON artifact) |
66
-
67
- ## 3. Diagnostic engine — `qa_tool.py diagnostics`
58
+ | `junit` | `node <skill-dir>/scripts/qa-tool.mjs analysis junit <report.xml>` | `{tests: {...}, executed: [...]}` normalized counts and per-test outcomes |
59
+ | `har` | `node <skill-dir>/scripts/qa-tool.mjs analysis har <file.har> [--slow-ms N]` | Redacted request/response summary, failures, slow calls |
60
+ | `discover` | `node <skill-dir>/scripts/qa-tool.mjs analysis discover [--root DIR] [--path P]` | Artifacts found, by type, with presence flags |
61
+ | `diff-guard` | `node <skill-dir>/scripts/qa-tool.mjs analysis diff-guard <diff-file>` | `{issues: [...], safe: bool}` — `safe:false` blocks the change |
62
+ | `redact` | `node <skill-dir>/scripts/qa-tool.mjs analysis redact <file>` | The file's text with credentials masked |
63
+ | `validate` | `node <skill-dir>/scripts/qa-tool.mjs analysis validate <instance.json> <schema.json>` | `{valid: bool, errors: [...]}`; exit 1 when invalid |
64
+ | `classify` | `node <skill-dir>/scripts/qa-tool.mjs analysis classify "<error message>" [--http-status N]` | `{classification, confidence, reason}` from the shared taxonomy |
65
+ | `context` | `node <skill-dir>/scripts/qa-tool.mjs analysis context [--root DIR] [--path .qa/context.md]` | The parsed, schema-validated project context as JSON |
66
+ | `report-html` | `node <skill-dir>/scripts/qa-tool.mjs analysis report-html <result.json> [--out report.html]` | The result rendered as one self-contained HTML report — every required field, footer included. Run it instead of writing HTML |
67
+ | `branding` | `node <skill-dir>/scripts/qa-tool.mjs analysis branding --format markdown\|html\|text` | The exact attribution footer bytes for a **rendered** report (never for a JSON artifact) |
68
+
69
+ ## 3. Diagnostic engine — `qa-tool.mjs diagnostics`
68
70
 
69
71
  One engine, consumed by the diagnostic skills. Reasoning lives here once.
70
72
 
71
73
  | Subcommand | Invocation | Returns |
72
74
  | --- | --- | --- |
73
- | `diagnose` | `python3 <skill-dir>/scripts/qa_tool.py diagnostics diagnose --execution-result <path> [--analysis-result <path>]` | `{entries: [...], timeline: [...], recommendations: [...]}` |
74
- | `plan-repairs` | `python3 <skill-dir>/scripts/qa_tool.py diagnostics plan-repairs --diagnosis <path>` | `{plans: [...]}` — one plan per entry, escalations included |
75
- | `summarize` | `python3 <skill-dir>/scripts/qa_tool.py diagnostics summarize --execution-result <path> --diagnosis <path>` | `{totals, byClassification, topPriority, releaseReadiness}` |
76
- | `report` | `python3 <skill-dir>/scripts/qa_tool.py diagnostics report --execution-result <path> [--analysis-result <path>]` | `{diagnosis, plans, summary}` — all three in one call |
75
+ | `diagnose` | `node <skill-dir>/scripts/qa-tool.mjs diagnostics diagnose --execution-result <path> [--analysis-result <path>]` | `{entries: [...], timeline: [...], recommendations: [...]}` |
76
+ | `plan-repairs` | `node <skill-dir>/scripts/qa-tool.mjs diagnostics plan-repairs --diagnosis <path>` | `{plans: [...]}` — one plan per entry, escalations included |
77
+ | `summarize` | `node <skill-dir>/scripts/qa-tool.mjs diagnostics summarize --execution-result <path> --diagnosis <path>` | `{totals, byClassification, topPriority, releaseReadiness}` |
78
+ | `report` | `node <skill-dir>/scripts/qa-tool.mjs diagnostics report --execution-result <path> [--analysis-result <path>]` | `{diagnosis, plans, summary}` — all three in one call |
77
79
 
78
80
  **Inputs.** `--execution-result` takes a `qa-run` execution result, or the minimal
79
81
  subset (`tests` counts plus `executed[]` entries carrying `status`).
@@ -101,7 +103,7 @@ contract names:
101
103
 
102
104
  This mapping is not busywork: the strictness is what stops a skill from shipping a
103
105
  result whose shape nobody checked. Validate before completion —
104
- `python3 <skill-dir>/scripts/qa_tool.py analysis validate <result.json> <schema.json>` — and fix the
106
+ `node <skill-dir>/scripts/qa-tool.mjs analysis validate <result.json> <schema.json>` — and fix the
105
107
  result, never the claim.
106
108
 
107
109
  ## 4. Framework adapters
@@ -111,11 +113,11 @@ a `--framework` flag.
111
113
 
112
114
  | Adapter | Invocation | Returns |
113
115
  | --- | --- | --- |
114
- | Playwright report | `python3 <skill-dir>/scripts/qa_tool.py playwright report <results.json>` | The same `{tests, executed}` shape as `junit` |
115
- | Playwright trace | `python3 <skill-dir>/scripts/qa_tool.py playwright trace <trace.zip>` | Actions, console/network counts, errors, classification |
116
+ | Playwright report | `node <skill-dir>/scripts/qa-tool.mjs playwright report <results.json>` | The same `{tests, executed}` shape as `junit` |
117
+ | Playwright trace | `node <skill-dir>/scripts/qa-tool.mjs playwright trace <trace.zip>` | Actions, console/network counts, errors, classification |
116
118
 
117
119
  For Selenium, Cypress, and WebdriverIO, normalize through
118
- `qa_tool.py analysis junit` — those adapters have no richer artifact than JUnit, and
120
+ `qa-tool.mjs analysis junit` — those adapters have no richer artifact than JUnit, and
119
121
  the skill says so rather than implying trace-grade depth.
120
122
 
121
123
  ## 5. Reporting what ran