qa-engineer 0.9.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 (232) hide show
  1. package/COMPATIBILITY.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +508 -0
  4. package/package.json +90 -0
  5. package/packages/installer/README.md +46 -0
  6. package/packages/installer/bin/qa.mjs +136 -0
  7. package/packages/installer/lib/agents/registry.mjs +209 -0
  8. package/packages/installer/lib/cli/commands.mjs +29 -0
  9. package/packages/installer/lib/cli/flags.mjs +64 -0
  10. package/packages/installer/lib/commands/doctor.mjs +190 -0
  11. package/packages/installer/lib/commands/install.mjs +291 -0
  12. package/packages/installer/lib/commands/onboard.mjs +163 -0
  13. package/packages/installer/lib/commands/repair.mjs +68 -0
  14. package/packages/installer/lib/commands/self-test.mjs +45 -0
  15. package/packages/installer/lib/commands/uninstall.mjs +167 -0
  16. package/packages/installer/lib/commands/update.mjs +69 -0
  17. package/packages/installer/lib/commands/verify.mjs +62 -0
  18. package/packages/installer/lib/constants.mjs +34 -0
  19. package/packages/installer/lib/core/bundle.mjs +124 -0
  20. package/packages/installer/lib/core/config.mjs +73 -0
  21. package/packages/installer/lib/core/conflict.mjs +29 -0
  22. package/packages/installer/lib/core/errors.mjs +29 -0
  23. package/packages/installer/lib/core/fs-safe.mjs +236 -0
  24. package/packages/installer/lib/core/hash.mjs +19 -0
  25. package/packages/installer/lib/core/lockfile.mjs +75 -0
  26. package/packages/installer/lib/core/logger.mjs +46 -0
  27. package/packages/installer/lib/core/manifest.mjs +89 -0
  28. package/packages/installer/lib/core/paths.mjs +74 -0
  29. package/packages/installer/lib/core/schema-validate.mjs +142 -0
  30. package/packages/installer/lib/core/skill-meta.mjs +75 -0
  31. package/packages/installer/lib/core/validate-install.mjs +152 -0
  32. package/packages/installer/lib/core/wrappers.mjs +91 -0
  33. package/packages/installer/lib/detect/environment.mjs +54 -0
  34. package/packages/installer/lib/detect/frameworks.mjs +94 -0
  35. package/packages/installer/lib/detect/project.mjs +126 -0
  36. package/packages/installer/lib/detect/recommend.mjs +85 -0
  37. package/packages/installer/lib/detect/scan.mjs +48 -0
  38. package/packages/installer/lib/ui/progress.mjs +32 -0
  39. package/packages/installer/lib/ui/theme.mjs +81 -0
  40. package/packages/installer/lib/version.mjs +47 -0
  41. package/packages/installer/package.json +28 -0
  42. package/packages/installer/schemas/qa-lock.schema.json +89 -0
  43. package/packages/installer/schemas/qa.config.schema.json +91 -0
  44. package/shared/analysis/lib/qa_analysis/__init__.py +11 -0
  45. package/shared/analysis/lib/qa_analysis/branding.json +9 -0
  46. package/shared/analysis/lib/qa_analysis/branding.py +175 -0
  47. package/shared/analysis/lib/qa_analysis/cli.py +129 -0
  48. package/shared/analysis/lib/qa_analysis/context.py +233 -0
  49. package/shared/analysis/lib/qa_analysis/contracts.py +158 -0
  50. package/shared/analysis/lib/qa_analysis/diff_guard.py +327 -0
  51. package/shared/analysis/lib/qa_analysis/discovery.py +113 -0
  52. package/shared/analysis/lib/qa_analysis/evidence.py +128 -0
  53. package/shared/analysis/lib/qa_analysis/har.py +58 -0
  54. package/shared/analysis/lib/qa_analysis/junit.py +80 -0
  55. package/shared/analysis/lib/qa_analysis/redaction.py +99 -0
  56. package/shared/analysis/lib/qa_analysis/taxonomy.py +98 -0
  57. package/shared/analysis/schemas/context.schema.json +82 -0
  58. package/shared/diagnostics/lib/qa_diagnostics/__init__.py +13 -0
  59. package/shared/diagnostics/lib/qa_diagnostics/cli.py +124 -0
  60. package/shared/diagnostics/lib/qa_diagnostics/engine.py +143 -0
  61. package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +75 -0
  62. package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +101 -0
  63. package/shared/diagnostics/lib/qa_diagnostics/repair.py +72 -0
  64. package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +89 -0
  65. package/shared/diagnostics/lib/qa_diagnostics/timeline.py +71 -0
  66. package/shared/diagnostics/schemas/internal/analysis-result.schema.json +28 -0
  67. package/shared/diagnostics/schemas/internal/diagnosis.schema.json +55 -0
  68. package/shared/diagnostics/schemas/internal/execution-result-min.schema.json +34 -0
  69. package/shared/frameworks/cypress/lib/cypress_analysis.py +27 -0
  70. package/shared/frameworks/playwright/lib/playwright_analysis.py +167 -0
  71. package/shared/frameworks/registry.json +150 -0
  72. package/shared/frameworks/registry.mjs +37 -0
  73. package/shared/frameworks/registry.schema.json +74 -0
  74. package/shared/frameworks/selenium/lib/selenium_analysis.py +28 -0
  75. package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +24 -0
  76. package/shared/tooling/qa_tool.py +127 -0
  77. package/skills/README.md +31 -0
  78. package/skills/qa/README.md +19 -0
  79. package/skills/qa/SKILL.md +52 -0
  80. package/skills/qa/examples/routing.md +86 -0
  81. package/skills/qa/references/routing-map.md +33 -0
  82. package/skills/qa-api/README.md +19 -0
  83. package/skills/qa-api/SKILL.md +68 -0
  84. package/skills/qa-api/contracts/api-result.schema.json +51 -0
  85. package/skills/qa-api/examples/graphql-review.md +43 -0
  86. package/skills/qa-api/references/authentication.md +45 -0
  87. package/skills/qa-api/references/deterministic-tooling.md +128 -0
  88. package/skills/qa-api/references/evidence-and-reporting.md +66 -0
  89. package/skills/qa-api/references/graphql.md +44 -0
  90. package/skills/qa-api/references/rest.md +45 -0
  91. package/skills/qa-api/references/websocket.md +44 -0
  92. package/skills/qa-audit/README.md +19 -0
  93. package/skills/qa-audit/SKILL.md +70 -0
  94. package/skills/qa-audit/contracts/audit-result.schema.json +63 -0
  95. package/skills/qa-audit/examples/accessibility-audit.md +46 -0
  96. package/skills/qa-audit/references/accessibility.md +44 -0
  97. package/skills/qa-audit/references/deterministic-tooling.md +128 -0
  98. package/skills/qa-audit/references/evidence-and-reporting.md +66 -0
  99. package/skills/qa-audit/references/performance.md +44 -0
  100. package/skills/qa-audit/references/security.md +43 -0
  101. package/skills/qa-audit/references/visual-testing.md +44 -0
  102. package/skills/qa-debug/README.md +19 -0
  103. package/skills/qa-debug/SKILL.md +76 -0
  104. package/skills/qa-debug/contracts/debug-result.schema.json +121 -0
  105. package/skills/qa-debug/examples/failed-login.md +61 -0
  106. package/skills/qa-debug/examples/locator-break.md +59 -0
  107. package/skills/qa-debug/examples/network-timeout.md +61 -0
  108. package/skills/qa-debug/examples/successful-debug.md +62 -0
  109. package/skills/qa-debug/references/confidence-model.md +26 -0
  110. package/skills/qa-debug/references/deterministic-tooling.md +128 -0
  111. package/skills/qa-debug/references/diagnostic-engine.md +36 -0
  112. package/skills/qa-debug/references/evidence-and-reporting.md +66 -0
  113. package/skills/qa-debug/references/evidence-model.md +41 -0
  114. package/skills/qa-debug/references/failure-taxonomy.md +44 -0
  115. package/skills/qa-debug/references/finding-prioritization.md +43 -0
  116. package/skills/qa-debug/references/investigation-workflow.md +48 -0
  117. package/skills/qa-debug/references/recommendation-ranking.md +34 -0
  118. package/skills/qa-debug/references/root-cause-analysis.md +49 -0
  119. package/skills/qa-debug/references/timeline-builder.md +37 -0
  120. package/skills/qa-example/README.md +19 -0
  121. package/skills/qa-example/SKILL.md +60 -0
  122. package/skills/qa-example/contracts/self-check-report.schema.json +65 -0
  123. package/skills/qa-example/examples/self-check.md +53 -0
  124. package/skills/qa-example/references/example-domain.md +28 -0
  125. package/skills/qa-example/references/skill-format-notes.md +26 -0
  126. package/skills/qa-explore/README.md +22 -0
  127. package/skills/qa-explore/SKILL.md +87 -0
  128. package/skills/qa-explore/contracts/explore-result.schema.json +249 -0
  129. package/skills/qa-explore/examples/attached-test-cases.md +70 -0
  130. package/skills/qa-explore/examples/screenshot-proof-finding.md +50 -0
  131. package/skills/qa-explore/examples/url-smoke-explore.md +80 -0
  132. package/skills/qa-explore/references/accessibility.md +44 -0
  133. package/skills/qa-explore/references/api-replay.md +48 -0
  134. package/skills/qa-explore/references/browser-adapters.md +51 -0
  135. package/skills/qa-explore/references/evidence-and-reporting.md +66 -0
  136. package/skills/qa-explore/references/evidence-capture.md +54 -0
  137. package/skills/qa-explore/references/exploratory-qa.md +52 -0
  138. package/skills/qa-explore/references/finding-taxonomy.md +47 -0
  139. package/skills/qa-explore/references/performance.md +44 -0
  140. package/skills/qa-explore/references/pipeline.md +74 -0
  141. package/skills/qa-explore/references/report-pipeline.md +69 -0
  142. package/skills/qa-explore/references/security.md +43 -0
  143. package/skills/qa-explore/references/test-case-intake.md +44 -0
  144. package/skills/qa-fix/README.md +19 -0
  145. package/skills/qa-fix/SKILL.md +70 -0
  146. package/skills/qa-fix/contracts/fix-result.schema.json +132 -0
  147. package/skills/qa-fix/examples/repair-plan.md +56 -0
  148. package/skills/qa-fix/references/deterministic-tooling.md +128 -0
  149. package/skills/qa-fix/references/diagnostic-engine.md +36 -0
  150. package/skills/qa-fix/references/evidence-and-reporting.md +66 -0
  151. package/skills/qa-fix/references/repair-strategy.md +48 -0
  152. package/skills/qa-fix/references/root-cause-analysis.md +49 -0
  153. package/skills/qa-fix/references/suite-extension.md +73 -0
  154. package/skills/qa-flaky/README.md +19 -0
  155. package/skills/qa-flaky/SKILL.md +67 -0
  156. package/skills/qa-flaky/contracts/flaky-result.schema.json +62 -0
  157. package/skills/qa-flaky/examples/flaky-locator.md +46 -0
  158. package/skills/qa-flaky/references/deterministic-tooling.md +128 -0
  159. package/skills/qa-flaky/references/evidence-and-reporting.md +66 -0
  160. package/skills/qa-flaky/references/flakiness.md +48 -0
  161. package/skills/qa-flaky/references/retry.md +44 -0
  162. package/skills/qa-flaky/references/waiting-strategies.md +44 -0
  163. package/skills/qa-generate/README.md +42 -0
  164. package/skills/qa-generate/SKILL.md +83 -0
  165. package/skills/qa-generate/contracts/generation-result.schema.json +124 -0
  166. package/skills/qa-generate/examples/bootstrap-new-framework.md +24 -0
  167. package/skills/qa-generate/examples/extend-existing-suite.md +26 -0
  168. package/skills/qa-generate/references/code-style.md +29 -0
  169. package/skills/qa-generate/references/evidence-and-reporting.md +66 -0
  170. package/skills/qa-generate/references/framework-selection.md +88 -0
  171. package/skills/qa-generate/references/generation-strategy.md +48 -0
  172. package/skills/qa-generate/references/naming-conventions.md +27 -0
  173. package/skills/qa-generate/references/playwright-conventions.md +23 -0
  174. package/skills/qa-generate/references/playwright-generation.md +41 -0
  175. package/skills/qa-generate/references/playwright-project-discovery.md +42 -0
  176. package/skills/qa-generate/references/project-bootstrap.md +54 -0
  177. package/skills/qa-generate/references/repository-analysis.md +43 -0
  178. package/skills/qa-generate/references/suite-extension.md +73 -0
  179. package/skills/qa-generate/references/template-selection.md +48 -0
  180. package/skills/qa-generate/templates/playwright/api.ts +36 -0
  181. package/skills/qa-generate/templates/playwright/base-page.ts +14 -0
  182. package/skills/qa-generate/templates/playwright/data.ts +25 -0
  183. package/skills/qa-generate/templates/playwright/env.example +16 -0
  184. package/skills/qa-generate/templates/playwright/example.spec.ts +14 -0
  185. package/skills/qa-generate/templates/playwright/fixtures.ts +28 -0
  186. package/skills/qa-generate/templates/playwright/framework-readme.md +39 -0
  187. package/skills/qa-generate/templates/playwright/login.page.ts +26 -0
  188. package/skills/qa-generate/templates/playwright/playwright.config.ts +28 -0
  189. package/skills/qa-generate/templates/playwright/utils.ts +18 -0
  190. package/skills/qa-init/README.md +20 -0
  191. package/skills/qa-init/SKILL.md +72 -0
  192. package/skills/qa-init/examples/initialize-a-repo.md +91 -0
  193. package/skills/qa-init/references/detection-guide.md +76 -0
  194. package/skills/qa-init/references/deterministic-tooling.md +128 -0
  195. package/skills/qa-init/references/evidence-and-reporting.md +66 -0
  196. package/skills/qa-init/templates/context.md +68 -0
  197. package/skills/qa-report/README.md +19 -0
  198. package/skills/qa-report/SKILL.md +72 -0
  199. package/skills/qa-report/contracts/report-result.schema.json +186 -0
  200. package/skills/qa-report/examples/release-report.md +68 -0
  201. package/skills/qa-report/references/deterministic-tooling.md +128 -0
  202. package/skills/qa-report/references/diagnostic-engine.md +36 -0
  203. package/skills/qa-report/references/evidence-and-reporting.md +66 -0
  204. package/skills/qa-report/references/finding-prioritization.md +43 -0
  205. package/skills/qa-report/references/recommendation-ranking.md +34 -0
  206. package/skills/qa-report/references/report-aggregation.md +44 -0
  207. package/skills/qa-review/README.md +19 -0
  208. package/skills/qa-review/SKILL.md +58 -0
  209. package/skills/qa-review/contracts/review-result.schema.json +59 -0
  210. package/skills/qa-review/examples/suite-review.md +51 -0
  211. package/skills/qa-review/references/anti-patterns.md +49 -0
  212. package/skills/qa-review/references/assertion-patterns.md +45 -0
  213. package/skills/qa-review/references/evidence-and-reporting.md +66 -0
  214. package/skills/qa-review/references/fixtures.md +45 -0
  215. package/skills/qa-review/references/page-objects.md +45 -0
  216. package/skills/qa-run/README.md +22 -0
  217. package/skills/qa-run/SKILL.md +92 -0
  218. package/skills/qa-run/contracts/execution-plan.schema.json +132 -0
  219. package/skills/qa-run/contracts/execution-result.schema.json +204 -0
  220. package/skills/qa-run/examples/execute-playwright.md +89 -0
  221. package/skills/qa-run/examples/plan-a-run.md +83 -0
  222. package/skills/qa-run/references/artifact-collector.md +40 -0
  223. package/skills/qa-run/references/browser-launch.md +39 -0
  224. package/skills/qa-run/references/command-builder.md +39 -0
  225. package/skills/qa-run/references/deterministic-tooling.md +128 -0
  226. package/skills/qa-run/references/environment-detection.md +37 -0
  227. package/skills/qa-run/references/evidence-and-reporting.md +66 -0
  228. package/skills/qa-run/references/execution-strategy.md +54 -0
  229. package/skills/qa-run/references/playwright-artifacts.md +29 -0
  230. package/skills/qa-run/references/playwright-execution.md +48 -0
  231. package/skills/qa-run/references/playwright-project-discovery.md +42 -0
  232. package/skills/qa-run/references/report-normalization.md +49 -0
@@ -0,0 +1,142 @@
1
+ // A dependency-free validator for the JSON Schema subset this project uses.
2
+ // It is the JavaScript twin of the Python contract validator in
3
+ // shared/analysis/lib/qa_analysis/contracts.py: same supported subset, same
4
+ // semantics, so a document that passes one passes the other. That promise is
5
+ // tested, not asserted — tests/parity/validator-cases.json runs through both.
6
+ //
7
+ // Anything outside the subset is a programming error in the schema, reported
8
+ // rather than silently ignored. `allOf`/`if`/`then`/`else` are in the subset
9
+ // because the pack's safety invariants are cross-field implications.
10
+
11
+ // Keep in sync with SUPPORTED_KEYWORDS in
12
+ // shared/analysis/lib/qa_analysis/contracts.py and the table in
13
+ // docs/skills/output-contracts.md (checked by scripts/check-spec-code-sync.mjs).
14
+ const SUPPORTED = new Set([
15
+ '$schema', '$id', 'title', 'description', 'type', 'properties', 'required',
16
+ 'additionalProperties', 'items', 'enum', 'const', 'pattern', 'minimum',
17
+ 'maximum', 'minItems', 'maxItems', 'minLength', 'maxLength', 'default',
18
+ 'examples', 'format', 'allOf', 'if', 'then', 'else',
19
+ ]);
20
+
21
+ // RFC 3339 date-time — the same rule as the Python twin.
22
+ const DATE_TIME = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
23
+
24
+ function typeOf(value) {
25
+ if (value === null) return 'null';
26
+ if (Array.isArray(value)) return 'array';
27
+ if (Number.isInteger(value)) return 'integer';
28
+ return typeof value; // string | number | boolean | object
29
+ }
30
+
31
+ function matchesType(value, type) {
32
+ if (type === 'integer') return Number.isInteger(value) && typeof value !== 'boolean';
33
+ if (type === 'number') return typeof value === 'number';
34
+ if (type === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
35
+ if (type === 'array') return Array.isArray(value);
36
+ return typeOf(value) === type;
37
+ }
38
+
39
+ /** True when value satisfies schema; used for if/then branch selection. */
40
+ function satisfies(value, schema) {
41
+ const probe = [];
42
+ validateNode(value, schema, '', probe);
43
+ return probe.length === 0;
44
+ }
45
+
46
+ function validateNode(value, schema, pointer, errors) {
47
+ for (const key of Object.keys(schema)) {
48
+ if (!SUPPORTED.has(key)) {
49
+ errors.push(`${pointer || '/'}: schema uses unsupported keyword "${key}"`);
50
+ }
51
+ }
52
+
53
+ if (schema.type) {
54
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
55
+ if (!types.some((t) => matchesType(value, t))) {
56
+ errors.push(`${pointer || '/'}: expected ${types.join(' | ')}, got ${typeOf(value)}`);
57
+ return; // further checks assume the type held
58
+ }
59
+ }
60
+
61
+ if (Object.prototype.hasOwnProperty.call(schema, 'const') && JSON.stringify(value) !== JSON.stringify(schema.const)) {
62
+ errors.push(`${pointer || '/'}: must equal ${JSON.stringify(schema.const)}`);
63
+ }
64
+
65
+ if (schema.enum && !schema.enum.some((option) => JSON.stringify(option) === JSON.stringify(value))) {
66
+ errors.push(`${pointer || '/'}: ${JSON.stringify(value)} is not one of ${JSON.stringify(schema.enum)}`);
67
+ }
68
+
69
+ if (typeof value === 'string') {
70
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
71
+ errors.push(`${pointer || '/'}: "${value}" does not match /${schema.pattern}/`);
72
+ }
73
+ if (schema.format === 'date-time' && !DATE_TIME.test(value)) {
74
+ errors.push(`${pointer || '/'}: "${value}" is not a valid date-time`);
75
+ }
76
+ if (schema.minLength != null && value.length < schema.minLength) {
77
+ errors.push(`${pointer || '/'}: shorter than minLength ${schema.minLength}`);
78
+ }
79
+ if (schema.maxLength != null && value.length > schema.maxLength) {
80
+ errors.push(`${pointer || '/'}: longer than maxLength ${schema.maxLength}`);
81
+ }
82
+ }
83
+
84
+ if (typeof value === 'number') {
85
+ if (schema.minimum != null && value < schema.minimum) {
86
+ errors.push(`${pointer || '/'}: ${value} is below minimum ${schema.minimum}`);
87
+ }
88
+ if (schema.maximum != null && value > schema.maximum) {
89
+ errors.push(`${pointer || '/'}: ${value} is above maximum ${schema.maximum}`);
90
+ }
91
+ }
92
+
93
+ if (Array.isArray(value)) {
94
+ if (schema.minItems != null && value.length < schema.minItems) {
95
+ errors.push(`${pointer || '/'}: has ${value.length} items, needs at least ${schema.minItems}`);
96
+ }
97
+ if (schema.maxItems != null && value.length > schema.maxItems) {
98
+ errors.push(`${pointer || '/'}: has ${value.length} items, allows at most ${schema.maxItems}`);
99
+ }
100
+ if (schema.items) {
101
+ value.forEach((item, index) => validateNode(item, schema.items, `${pointer}/${index}`, errors));
102
+ }
103
+ }
104
+
105
+ if (matchesType(value, 'object') && (schema.properties || schema.required || schema.additionalProperties === false)) {
106
+ const properties = schema.properties ?? {};
107
+ for (const name of schema.required ?? []) {
108
+ if (!Object.prototype.hasOwnProperty.call(value, name)) {
109
+ errors.push(`${pointer || '/'}: missing required property "${name}"`);
110
+ }
111
+ }
112
+ for (const [name, child] of Object.entries(value)) {
113
+ if (properties[name]) {
114
+ validateNode(child, properties[name], `${pointer}/${name}`, errors);
115
+ } else if (schema.additionalProperties === false) {
116
+ errors.push(`${pointer || '/'}: unexpected property "${name}"`);
117
+ }
118
+ }
119
+ }
120
+
121
+ // Applicators last: the invariant layer. `additionalProperties` deliberately
122
+ // does not see properties introduced by these subschemas (JSON Schema
123
+ // 2020-12 rule, matching the Python twin).
124
+ for (const sub of schema.allOf ?? []) {
125
+ validateNode(value, sub, pointer, errors);
126
+ }
127
+
128
+ if (Object.prototype.hasOwnProperty.call(schema, 'if')) {
129
+ const branch = satisfies(value, schema.if) ? schema.then : schema.else;
130
+ if (branch != null) validateNode(value, branch, pointer, errors);
131
+ }
132
+ }
133
+
134
+ /** Validate `value` against `schema`; returns an array of error strings (empty = valid). */
135
+ export function validate(value, schema) {
136
+ const errors = [];
137
+ validateNode(value, schema, '', errors);
138
+ return errors;
139
+ }
140
+
141
+ /** The supported keyword subset, exported so tooling can assert parity. */
142
+ export const SUPPORTED_KEYWORDS = Object.freeze([...SUPPORTED].sort());
@@ -0,0 +1,75 @@
1
+ // Reading the handful of frontmatter fields the installer needs from a
2
+ // SKILL.md: name, description, and the optional argument-hint. Parses the same
3
+ // restricted YAML subset the skill specification allows (plain scalars, folded
4
+ // ">-"/">" blocks, one flat metadata map). Self-contained so the installer can
5
+ // publish without the repository's dev tooling.
6
+
7
+ import fs from 'node:fs';
8
+
9
+ function stripQuotes(value) {
10
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
11
+ return value.slice(1, -1);
12
+ }
13
+ return value;
14
+ }
15
+
16
+ /** Parse a SKILL.md string; returns { name, description, argumentHint } or throws. */
17
+ export function parseSkillMeta(text) {
18
+ const lines = text.split('\n');
19
+ if (lines[0] !== '---') throw new Error('SKILL.md frontmatter must open with "---"');
20
+ let end = -1;
21
+ for (let i = 1; i < lines.length; i += 1) {
22
+ if (lines[i] === '---') {
23
+ end = i;
24
+ break;
25
+ }
26
+ }
27
+ if (end === -1) throw new Error('SKILL.md frontmatter is not closed');
28
+
29
+ const data = {};
30
+ let i = 1;
31
+ while (i < end) {
32
+ const line = lines[i];
33
+ if (line.trim() === '') {
34
+ i += 1;
35
+ continue;
36
+ }
37
+ const top = line.match(/^([A-Za-z][A-Za-z0-9-]*):(?:[ \t]+(.*))?$/);
38
+ if (!top) {
39
+ i += 1;
40
+ continue;
41
+ }
42
+ const key = top[1];
43
+ const raw = (top[2] ?? '').trim();
44
+ i += 1;
45
+ if (raw === '>-' || raw === '>') {
46
+ const parts = [];
47
+ while (i < end && (lines[i].startsWith(' ') || lines[i].trim() === '')) {
48
+ if (lines[i].trim() !== '') parts.push(lines[i].trim());
49
+ i += 1;
50
+ }
51
+ data[key] = parts.join(' ');
52
+ } else if (raw === '' && key === 'metadata') {
53
+ while (i < end && /^ {2}\S/.test(lines[i])) i += 1; // skip the metadata map; not needed here
54
+ } else if (raw !== '') {
55
+ data[key] = stripQuotes(raw);
56
+ }
57
+ }
58
+ return {
59
+ name: data.name,
60
+ description: data.description ?? '',
61
+ argumentHint: data['argument-hint'] ?? null,
62
+ };
63
+ }
64
+
65
+ export function readSkillMeta(skillMdPath) {
66
+ return parseSkillMeta(fs.readFileSync(skillMdPath, 'utf8'));
67
+ }
68
+
69
+ /** A single-line summary for a wrapper: first sentence, whitespace-collapsed, capped. */
70
+ export function oneLineDescription(description, cap = 220) {
71
+ const collapsed = description.replace(/\s+/g, ' ').trim();
72
+ const firstSentence = collapsed.match(/^(.*?\.)(\s|$)/);
73
+ const text = firstSentence ? firstSentence[1] : collapsed;
74
+ return text.length > cap ? `${text.slice(0, cap - 1).trimEnd()}…` : text;
75
+ }
@@ -0,0 +1,152 @@
1
+ // Post-install validation shared by install, onboard, self-test, and doctor.
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { LOCKFILE, SHARED_SKILLS_DIR, CLAUDE_SKILLS_DIR } from '../constants.mjs';
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';
10
+
11
+ /**
12
+ * @typedef {{ id: string, ok: boolean, hard: boolean, message: string, hint?: string }} CheckResult
13
+ */
14
+
15
+ /**
16
+ * Validate an installed project. Does not throw — returns structured checks.
17
+ * @param {string} projectRoot
18
+ * @returns {{ ok: boolean, checks: CheckResult[] }}
19
+ */
20
+ export function validateInstall(projectRoot) {
21
+ /** @type {CheckResult[]} */
22
+ const checks = [];
23
+
24
+ const lock = readLock(projectRoot);
25
+ if (!lock) {
26
+ checks.push({
27
+ id: 'lockfile',
28
+ ok: false,
29
+ hard: true,
30
+ message: `${LOCKFILE} missing`,
31
+ hint: 'run: qa install',
32
+ });
33
+ return { ok: false, checks };
34
+ }
35
+
36
+ checks.push({
37
+ id: 'lockfile',
38
+ ok: true,
39
+ hard: true,
40
+ message: `${LOCKFILE} present (${lock.files.length} files, pack ${lock.pack?.version ?? '?'})`,
41
+ });
42
+
43
+ let drift = 0;
44
+ 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;
51
+ }
52
+ checks.push({
53
+ id: 'integrity',
54
+ ok: drift === 0,
55
+ hard: true,
56
+ message: drift === 0 ? 'all installed files match lockfile hashes' : `${drift} file(s) missing or drifted`,
57
+ hint: drift === 0 ? undefined : 'run: qa repair',
58
+ });
59
+
60
+ const skillDirs = [SHARED_SKILLS_DIR, CLAUDE_SKILLS_DIR]
61
+ .map((d) => path.join(projectRoot, d))
62
+ .filter((d) => fs.existsSync(d));
63
+
64
+ let skillCount = 0;
65
+ let contractCount = 0;
66
+ for (const dir of skillDirs) {
67
+ for (const name of fs.readdirSync(dir)) {
68
+ const skillMd = path.join(dir, name, 'SKILL.md');
69
+ if (fs.existsSync(skillMd)) skillCount += 1;
70
+ const contracts = path.join(dir, name, 'contracts');
71
+ if (fs.existsSync(contracts)) {
72
+ contractCount += fs.readdirSync(contracts).filter((f) => f.endsWith('.json')).length;
73
+ }
74
+ }
75
+ }
76
+
77
+ checks.push({
78
+ id: 'skills',
79
+ ok: skillCount > 0,
80
+ hard: true,
81
+ message: skillCount > 0 ? `${skillCount} skill(s) installed` : 'no skills found under discovery paths',
82
+ hint: skillCount > 0 ? undefined : 'run: qa install',
83
+ });
84
+
85
+ checks.push({
86
+ id: 'contracts',
87
+ ok: contractCount > 0,
88
+ hard: false,
89
+ message:
90
+ contractCount > 0
91
+ ? `${contractCount} contract schema(s) present`
92
+ : 'no contract schemas found (optional for minimal installs)',
93
+ });
94
+
95
+ if (packHasBundles()) {
96
+ const bundledSkill = Object.keys(BUNDLE_MANIFEST)[0];
97
+ let libDir = null;
98
+ for (const base of [SHARED_SKILLS_DIR, CLAUDE_SKILLS_DIR]) {
99
+ const candidate = path.join(projectRoot, base, bundledSkill, BUNDLE_DEST);
100
+ if (fs.existsSync(candidate)) {
101
+ libDir = candidate;
102
+ break;
103
+ }
104
+ }
105
+
106
+ const bundleOk = Boolean(libDir);
107
+ checks.push({
108
+ id: 'engine',
109
+ ok: bundleOk,
110
+ hard: true,
111
+ message: bundleOk
112
+ ? `deterministic engine bundled under ${path.relative(projectRoot, libDir)}`
113
+ : 'deterministic engine bundle missing',
114
+ hint: bundleOk ? undefined : 'run: qa repair',
115
+ });
116
+
117
+ const python = findPython();
118
+ if (bundleOk && python) {
119
+ const packages = bundlePackagesForSkill(bundledSkill);
120
+ const result = verifyImports({ pythonBin: python.bin, libDir, packages });
121
+ checks.push({
122
+ id: 'python-imports',
123
+ ok: result.ok,
124
+ hard: false,
125
+ 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',
137
+ });
138
+ }
139
+ }
140
+
141
+ const major = Number(process.versions.node.split('.')[0]);
142
+ checks.push({
143
+ id: 'node',
144
+ ok: major >= 18,
145
+ hard: true,
146
+ message: `Node ${process.version}`,
147
+ hint: major >= 18 ? undefined : 'upgrade to Node.js 18.18+',
148
+ });
149
+
150
+ const ok = checks.filter((c) => c.hard).every((c) => c.ok);
151
+ return { ok, checks };
152
+ }
@@ -0,0 +1,91 @@
1
+ // Generating per-agent invocation wrappers. A wrapper is a thin companion file
2
+ // that gives an agent a slash command mapping to a skill, for agents whose
3
+ // slash ergonomics need one. Per ADR-0002 a wrapper is rendered from skill
4
+ // frontmatter ALONE, contains no knowledge, and is at most 15 lines. Agents
5
+ // that invoke skills natively (Claude Code, Codex CLI, Cursor) get no wrapper.
6
+
7
+ import { GENERATED_MARKER } from '../constants.mjs';
8
+ import { oneLineDescription } from './skill-meta.mjs';
9
+
10
+ const MAX_WRAPPER_LINES = 15;
11
+
12
+ // YAML/TOML-safe double-quoted string.
13
+ function quote(value) {
14
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
15
+ }
16
+
17
+ // Each renderer forwards the user's trailing text using the target agent's own
18
+ // documented argument convention, resolving open question #4 (trailing args)
19
+ // for the agents that need a wrapper.
20
+ const RENDERERS = {
21
+ // Gemini CLI custom command (TOML). {{args}} injects the trailing text.
22
+ 'command-toml': (name, desc) => ({
23
+ filename: `${name}.toml`,
24
+ content:
25
+ `# ${GENERATED_MARKER}\n` +
26
+ `description = ${quote(desc)}\n` +
27
+ 'prompt = """\n' +
28
+ `Apply the ${name} skill from the QA Automation Pack skills, following its procedure.\n` +
29
+ 'Treat the following as the user request:\n\n' +
30
+ '{{args}}\n' +
31
+ '"""\n',
32
+ }),
33
+
34
+ // OpenCode custom command (Markdown). $ARGUMENTS injects the trailing text.
35
+ 'command-md': (name, desc) => ({
36
+ filename: `${name}.md`,
37
+ content:
38
+ '---\n' +
39
+ `description: ${quote(desc)}\n` +
40
+ '---\n' +
41
+ `<!-- ${GENERATED_MARKER} -->\n\n` +
42
+ `Apply the \`${name}\` skill from the QA Automation Pack to the request below, following its procedure.\n\n` +
43
+ '$ARGUMENTS\n',
44
+ }),
45
+
46
+ // GitHub Copilot prompt file. Copilot supplies the chat request as context.
47
+ 'prompt-md': (name, desc) => ({
48
+ filename: `${name}.prompt.md`,
49
+ content:
50
+ '---\n' +
51
+ `description: ${quote(desc)}\n` +
52
+ '---\n' +
53
+ `<!-- ${GENERATED_MARKER} -->\n\n` +
54
+ `Apply the \`${name}\` skill from the QA Automation Pack to the current request, following its procedure.\n`,
55
+ }),
56
+
57
+ // Antigravity workflow (Markdown). Format is not yet verified against primary
58
+ // documentation (ADR-0002 open question #3), so this is generated only on
59
+ // explicit opt-in; the default is auto-activation.
60
+ 'workflow-md': (name, desc) => ({
61
+ filename: `${name}.md`,
62
+ content:
63
+ '---\n' +
64
+ `description: ${quote(desc)}\n` +
65
+ '---\n' +
66
+ `<!-- ${GENERATED_MARKER} -->\n\n` +
67
+ `Apply the \`${name}\` skill from the QA Automation Pack to the request below, following its procedure.\n\n` +
68
+ '$ARGUMENTS\n',
69
+ }),
70
+ };
71
+
72
+ export function supportsFormat(format) {
73
+ return Object.prototype.hasOwnProperty.call(RENDERERS, format);
74
+ }
75
+
76
+ /**
77
+ * Render one wrapper. Returns { filename, content }. Throws if the result would
78
+ * exceed the 15-line ceiling — a guard against a wrapper accreting knowledge.
79
+ */
80
+ export function renderWrapper(format, meta) {
81
+ const render = RENDERERS[format];
82
+ if (!render) throw new Error(`unknown wrapper format: ${format}`);
83
+ const out = render(meta.name, oneLineDescription(meta.description));
84
+ const lines = out.content.replace(/\n$/, '').split('\n').length;
85
+ if (lines > MAX_WRAPPER_LINES) {
86
+ throw new Error(`wrapper for ${meta.name} is ${lines} lines (max ${MAX_WRAPPER_LINES})`);
87
+ }
88
+ return out;
89
+ }
90
+
91
+ export { MAX_WRAPPER_LINES };
@@ -0,0 +1,54 @@
1
+ // Detect OS, shell, git, and package manager for a project root.
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import os from 'node:os';
7
+
8
+ function hasGit(root) {
9
+ if (fs.existsSync(path.join(root, '.git'))) return true;
10
+ const probe = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
11
+ cwd: root,
12
+ encoding: 'utf8',
13
+ });
14
+ return probe.status === 0 && (probe.stdout || '').trim() === 'true';
15
+ }
16
+
17
+ function detectPackageManager(root) {
18
+ if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm';
19
+ if (fs.existsSync(path.join(root, 'yarn.lock'))) return 'yarn';
20
+ if (fs.existsSync(path.join(root, 'bun.lockb')) || fs.existsSync(path.join(root, 'bun.lock'))) {
21
+ return 'bun';
22
+ }
23
+ if (fs.existsSync(path.join(root, 'package-lock.json'))) return 'npm';
24
+ if (fs.existsSync(path.join(root, 'package.json'))) return 'npm';
25
+ return null;
26
+ }
27
+
28
+ function detectShell() {
29
+ const shell = process.env.SHELL || process.env.ComSpec || '';
30
+ if (!shell) return null;
31
+ return path.basename(shell);
32
+ }
33
+
34
+ /**
35
+ * @param {string} projectRoot
36
+ * @returns {{
37
+ * os: string,
38
+ * platform: NodeJS.Platform,
39
+ * shell: string|null,
40
+ * git: boolean,
41
+ * packageManager: string|null,
42
+ * node: string,
43
+ * }}
44
+ */
45
+ export function detectEnvironment(projectRoot) {
46
+ return {
47
+ os: `${os.type()} ${os.release()}`,
48
+ platform: process.platform,
49
+ shell: detectShell(),
50
+ git: hasGit(projectRoot),
51
+ packageManager: detectPackageManager(projectRoot),
52
+ node: process.version,
53
+ };
54
+ }
@@ -0,0 +1,94 @@
1
+ // Detect test frameworks and languages. Framework identity comes from the
2
+ // canonical registry (shared/frameworks/registry.json) — do not hard-code
3
+ // framework lists here.
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { listFrameworks } from '../../../../shared/frameworks/registry.mjs';
8
+
9
+ function readPackageJson(root) {
10
+ const p = path.join(root, 'package.json');
11
+ if (!fs.existsSync(p)) return null;
12
+ try {
13
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function depsOf(pkg) {
20
+ if (!pkg) return new Set();
21
+ return new Set([
22
+ ...Object.keys(pkg.dependencies ?? {}),
23
+ ...Object.keys(pkg.devDependencies ?? {}),
24
+ ...Object.keys(pkg.peerDependencies ?? {}),
25
+ ]);
26
+ }
27
+
28
+ function anyExists(root, names) {
29
+ return names.some((n) => fs.existsSync(path.join(root, n)));
30
+ }
31
+
32
+ function hasConfigPrefix(root, prefixes) {
33
+ if (!prefixes.length || !fs.existsSync(root)) return false;
34
+ const entries = fs.readdirSync(root);
35
+ return prefixes.some((prefix) => entries.some((name) => name.startsWith(prefix)));
36
+ }
37
+
38
+ function hasPythonMarkers(root) {
39
+ return anyExists(root, [
40
+ 'requirements.txt',
41
+ 'pyproject.toml',
42
+ 'setup.py',
43
+ 'Pipfile',
44
+ 'pytest.ini',
45
+ ]);
46
+ }
47
+
48
+ function detectOne(root, fw, deps) {
49
+ const d = fw.detection;
50
+ if (d.configFiles.length && anyExists(root, d.configFiles)) return true;
51
+ if (d.configPrefixes.length && hasConfigPrefix(root, d.configPrefixes)) return true;
52
+ if (d.dependencies.some((name) => deps.has(name))) return true;
53
+ if (d.directories.some((dir) => fs.existsSync(path.join(root, dir)))) return true;
54
+ return false;
55
+ }
56
+
57
+ /**
58
+ * @param {string} projectRoot
59
+ * @returns {{
60
+ * frameworks: string[],
61
+ * languages: string[],
62
+ * details: Record<string, boolean>,
63
+ * }}
64
+ */
65
+ export function detectFrameworks(projectRoot) {
66
+ const pkg = readPackageJson(projectRoot);
67
+ const deps = depsOf(pkg);
68
+ const registry = listFrameworks().filter((f) => f.supportLevel !== 'Planning');
69
+
70
+ /** @type {Record<string, boolean>} */
71
+ const details = {};
72
+ const frameworks = [];
73
+ for (const fw of registry) {
74
+ const hit = detectOne(projectRoot, fw, deps);
75
+ details[fw.id] = hit;
76
+ if (hit) frameworks.push(fw.id);
77
+ }
78
+
79
+ const typescript =
80
+ anyExists(projectRoot, ['tsconfig.json', 'tsconfig.base.json']) || deps.has('typescript');
81
+ const javascript = Boolean(pkg) || anyExists(projectRoot, ['jsconfig.json']);
82
+ const python = hasPythonMarkers(projectRoot);
83
+
84
+ const languages = [];
85
+ if (typescript) languages.push('typescript');
86
+ else if (javascript) languages.push('javascript');
87
+ if (python) languages.push('python');
88
+
89
+ details.typescript = typescript;
90
+ details.javascript = javascript;
91
+ details.python = python;
92
+
93
+ return { frameworks, languages, details };
94
+ }