qa-engineer 0.10.0 → 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 (63) hide show
  1. package/README.md +167 -59
  2. package/package.json +1 -1
  3. package/packages/engine/bin/qa-engine.mjs +232 -8
  4. package/packages/engine/lib/analysis/har.mjs +34 -0
  5. package/packages/engine/lib/analysis/network.mjs +237 -0
  6. package/packages/engine/lib/analysis/report-html.mjs +47 -734
  7. package/packages/engine/lib/artifacts/manager.mjs +453 -0
  8. package/packages/engine/lib/artifacts/mime.mjs +109 -0
  9. package/packages/engine/lib/artifacts/zip-write.mjs +143 -0
  10. package/packages/engine/lib/report/components/charts.mjs +424 -0
  11. package/packages/engine/lib/report/components/evidence.mjs +207 -0
  12. package/packages/engine/lib/report/components/findings.mjs +258 -0
  13. package/packages/engine/lib/report/components/nav.mjs +99 -0
  14. package/packages/engine/lib/report/components/primitives.mjs +246 -0
  15. package/packages/engine/lib/report/components/runtime.mjs +246 -0
  16. package/packages/engine/lib/report/components/timeline.mjs +65 -0
  17. package/packages/engine/lib/report/core/model.mjs +270 -0
  18. package/packages/engine/lib/report/core/normalize.mjs +226 -0
  19. package/packages/engine/lib/report/core/sections.mjs +978 -0
  20. package/packages/engine/lib/report/export/bundle.mjs +293 -0
  21. package/packages/engine/lib/report/export/html.mjs +183 -0
  22. package/packages/engine/lib/report/export/machine.mjs +290 -0
  23. package/packages/engine/lib/report/export/markdown.mjs +323 -0
  24. package/packages/engine/lib/report/schemas/qa-report.schema.json +555 -0
  25. package/packages/engine/lib/report/theme/css.mjs +529 -0
  26. package/packages/engine/lib/report/theme/tokens.mjs +137 -0
  27. package/packages/engine/lib/report/version.mjs +78 -0
  28. package/packages/engine/package.json +2 -2
  29. package/packages/installer/lib/agents/targets.mjs +90 -0
  30. package/packages/installer/lib/agents/user-level.mjs +80 -0
  31. package/packages/installer/lib/cli/flags.mjs +20 -2
  32. package/packages/installer/lib/commands/doctor.mjs +6 -4
  33. package/packages/installer/lib/commands/install.mjs +134 -93
  34. package/packages/installer/lib/commands/repair.mjs +10 -6
  35. package/packages/installer/lib/commands/self-test.mjs +4 -3
  36. package/packages/installer/lib/commands/uninstall.mjs +14 -8
  37. package/packages/installer/lib/commands/update.mjs +9 -4
  38. package/packages/installer/lib/commands/verify.mjs +13 -12
  39. package/packages/installer/lib/constants.mjs +13 -0
  40. package/packages/installer/lib/core/conflict.mjs +5 -4
  41. package/packages/installer/lib/core/fs-safe.mjs +146 -6
  42. package/packages/installer/lib/core/integrity.mjs +59 -0
  43. package/packages/installer/lib/core/lockfile.mjs +19 -3
  44. package/packages/installer/lib/core/plan.mjs +213 -0
  45. package/packages/installer/lib/core/qa-home.mjs +145 -0
  46. package/packages/installer/lib/core/scope.mjs +274 -0
  47. package/packages/installer/lib/core/validate-install.mjs +37 -12
  48. package/packages/installer/package.json +1 -1
  49. package/packages/installer/schemas/qa-lock.schema.json +119 -21
  50. package/shared/tooling/qa-tool.mjs +41 -5
  51. package/skills/qa-api/scripts/qa-tool.mjs +41 -5
  52. package/skills/qa-audit/scripts/qa-tool.mjs +41 -5
  53. package/skills/qa-debug/scripts/qa-tool.mjs +41 -5
  54. package/skills/qa-explore/SKILL.md +26 -10
  55. package/skills/qa-explore/contracts/explore-result.schema.json +517 -11
  56. package/skills/qa-explore/references/api-replay.md +40 -1
  57. package/skills/qa-explore/references/report-pipeline.md +265 -95
  58. package/skills/qa-explore/scripts/qa-tool.mjs +41 -5
  59. package/skills/qa-fix/scripts/qa-tool.mjs +41 -5
  60. package/skills/qa-flaky/scripts/qa-tool.mjs +41 -5
  61. package/skills/qa-init/scripts/qa-tool.mjs +41 -5
  62. package/skills/qa-report/scripts/qa-tool.mjs +41 -5
  63. package/skills/qa-run/scripts/qa-tool.mjs +41 -5
@@ -29,6 +29,7 @@
29
29
  // `node qa-tool.mjs …`, which needs none.
30
30
 
31
31
  import fs from 'node:fs';
32
+ import os from 'node:os';
32
33
  import path from 'node:path';
33
34
  import { spawnSync } from 'node:child_process';
34
35
  import { fileURLToPath } from 'node:url';
@@ -38,7 +39,7 @@ const here = path.dirname(fileURLToPath(import.meta.url));
38
39
  const USAGE = `usage: node qa-tool.mjs <tool> <subcommand> [args]
39
40
 
40
41
  analysis parse artifacts, classify errors, validate contracts, diff-guard,
41
- read .qa/context.md, render an HTML report, print the footer
42
+ read .qa/context.md, render an HTML report, print the footer
42
43
  diagnostics root cause, timeline, priority, repair plans, release readiness
43
44
  playwright normalize a Playwright report or summarize a trace
44
45
 
@@ -53,15 +54,26 @@ examples:
53
54
  /**
54
55
  * Where the engine is, and how we found it.
55
56
  *
56
- * Ordered by cost: a bundled copy needs no resolution and no network, a
57
- * node_modules copy needs no network, and npx needs both on first use. Reporting
58
- * *which* one answered matters when a skill degrades — "the engine is missing" and
59
- * "the engine is being fetched" are different problems.
57
+ * Ordered by cost: a bundled copy needs no resolution and no network, a shared copy
58
+ * needs one stat per ancestor, a node_modules copy needs no network, and npx needs both
59
+ * on first use. Reporting *which* one answered matters when a skill degrades — "the
60
+ * engine is missing" and "the engine is being fetched" are different problems.
61
+ *
62
+ * The shared lookup is what lets one skill directory serve a project, a workspace, and a
63
+ * machine-wide install without knowing which installed it. Walking up for
64
+ * `.qa-engineer/engine` finds a workspace install from a skill inside the repository,
65
+ * and finds a global install from a skill linked into an agent's user-level directory —
66
+ * `~/.claude/skills/qa-explore` walks up to `~`, where `~/.qa-engineer/engine` is.
60
67
  */
61
68
  function resolveEngine() {
62
69
  const bundled = path.join(here, 'lib', 'bin', 'qa-engine.mjs');
63
70
  if (fs.existsSync(bundled)) return { kind: 'bundled', command: [process.execPath, bundled] };
64
71
 
72
+ for (const root of candidateSharedRoots()) {
73
+ const shared = path.join(root, 'bin', 'qa-engine.mjs');
74
+ if (fs.existsSync(shared)) return { kind: 'shared', command: [process.execPath, shared] };
75
+ }
76
+
65
77
  for (const base of candidateModuleRoots()) {
66
78
  const installed = path.join(base, 'qa-engineer', 'packages', 'engine', 'bin', 'qa-engine.mjs');
67
79
  if (fs.existsSync(installed)) {
@@ -75,6 +87,30 @@ function resolveEngine() {
75
87
  };
76
88
  }
77
89
 
90
+ /** Shared engine directories worth checking, most specific first. */
91
+ function candidateSharedRoots() {
92
+ const roots = [];
93
+
94
+ // An explicit home wins over anything discovered, so a user who moved the install can
95
+ // rely on it rather than on whatever the walk happens to find first.
96
+ const override = process.env.QA_ENGINEER_HOME;
97
+ if (override && override.trim()) roots.push(path.join(path.resolve(override.trim()), 'engine'));
98
+
99
+ let dir = here;
100
+ for (let depth = 0; depth < 12; depth += 1) {
101
+ roots.push(path.join(dir, '.qa-engineer', 'engine'));
102
+ const parent = path.dirname(dir);
103
+ if (parent === dir) break;
104
+ dir = parent;
105
+ }
106
+
107
+ // The default machine home, for the case where the skill lives outside it entirely.
108
+ const home = os.homedir();
109
+ if (home) roots.push(path.join(home, '.qa-engineer', 'engine'));
110
+
111
+ return roots;
112
+ }
113
+
78
114
  /** node_modules directories worth checking, nearest first. */
79
115
  function candidateModuleRoots() {
80
116
  const roots = [];
@@ -29,6 +29,7 @@
29
29
  // `node qa-tool.mjs …`, which needs none.
30
30
 
31
31
  import fs from 'node:fs';
32
+ import os from 'node:os';
32
33
  import path from 'node:path';
33
34
  import { spawnSync } from 'node:child_process';
34
35
  import { fileURLToPath } from 'node:url';
@@ -38,7 +39,7 @@ const here = path.dirname(fileURLToPath(import.meta.url));
38
39
  const USAGE = `usage: node qa-tool.mjs <tool> <subcommand> [args]
39
40
 
40
41
  analysis parse artifacts, classify errors, validate contracts, diff-guard,
41
- read .qa/context.md, render an HTML report, print the footer
42
+ read .qa/context.md, render an HTML report, print the footer
42
43
  diagnostics root cause, timeline, priority, repair plans, release readiness
43
44
  playwright normalize a Playwright report or summarize a trace
44
45
 
@@ -53,15 +54,26 @@ examples:
53
54
  /**
54
55
  * Where the engine is, and how we found it.
55
56
  *
56
- * Ordered by cost: a bundled copy needs no resolution and no network, a
57
- * node_modules copy needs no network, and npx needs both on first use. Reporting
58
- * *which* one answered matters when a skill degrades — "the engine is missing" and
59
- * "the engine is being fetched" are different problems.
57
+ * Ordered by cost: a bundled copy needs no resolution and no network, a shared copy
58
+ * needs one stat per ancestor, a node_modules copy needs no network, and npx needs both
59
+ * on first use. Reporting *which* one answered matters when a skill degrades — "the
60
+ * engine is missing" and "the engine is being fetched" are different problems.
61
+ *
62
+ * The shared lookup is what lets one skill directory serve a project, a workspace, and a
63
+ * machine-wide install without knowing which installed it. Walking up for
64
+ * `.qa-engineer/engine` finds a workspace install from a skill inside the repository,
65
+ * and finds a global install from a skill linked into an agent's user-level directory —
66
+ * `~/.claude/skills/qa-explore` walks up to `~`, where `~/.qa-engineer/engine` is.
60
67
  */
61
68
  function resolveEngine() {
62
69
  const bundled = path.join(here, 'lib', 'bin', 'qa-engine.mjs');
63
70
  if (fs.existsSync(bundled)) return { kind: 'bundled', command: [process.execPath, bundled] };
64
71
 
72
+ for (const root of candidateSharedRoots()) {
73
+ const shared = path.join(root, 'bin', 'qa-engine.mjs');
74
+ if (fs.existsSync(shared)) return { kind: 'shared', command: [process.execPath, shared] };
75
+ }
76
+
65
77
  for (const base of candidateModuleRoots()) {
66
78
  const installed = path.join(base, 'qa-engineer', 'packages', 'engine', 'bin', 'qa-engine.mjs');
67
79
  if (fs.existsSync(installed)) {
@@ -75,6 +87,30 @@ function resolveEngine() {
75
87
  };
76
88
  }
77
89
 
90
+ /** Shared engine directories worth checking, most specific first. */
91
+ function candidateSharedRoots() {
92
+ const roots = [];
93
+
94
+ // An explicit home wins over anything discovered, so a user who moved the install can
95
+ // rely on it rather than on whatever the walk happens to find first.
96
+ const override = process.env.QA_ENGINEER_HOME;
97
+ if (override && override.trim()) roots.push(path.join(path.resolve(override.trim()), 'engine'));
98
+
99
+ let dir = here;
100
+ for (let depth = 0; depth < 12; depth += 1) {
101
+ roots.push(path.join(dir, '.qa-engineer', 'engine'));
102
+ const parent = path.dirname(dir);
103
+ if (parent === dir) break;
104
+ dir = parent;
105
+ }
106
+
107
+ // The default machine home, for the case where the skill lives outside it entirely.
108
+ const home = os.homedir();
109
+ if (home) roots.push(path.join(home, '.qa-engineer', 'engine'));
110
+
111
+ return roots;
112
+ }
113
+
78
114
  /** node_modules directories worth checking, nearest first. */
79
115
  function candidateModuleRoots() {
80
116
  const roots = [];
@@ -29,6 +29,7 @@
29
29
  // `node qa-tool.mjs …`, which needs none.
30
30
 
31
31
  import fs from 'node:fs';
32
+ import os from 'node:os';
32
33
  import path from 'node:path';
33
34
  import { spawnSync } from 'node:child_process';
34
35
  import { fileURLToPath } from 'node:url';
@@ -38,7 +39,7 @@ const here = path.dirname(fileURLToPath(import.meta.url));
38
39
  const USAGE = `usage: node qa-tool.mjs <tool> <subcommand> [args]
39
40
 
40
41
  analysis parse artifacts, classify errors, validate contracts, diff-guard,
41
- read .qa/context.md, render an HTML report, print the footer
42
+ read .qa/context.md, render an HTML report, print the footer
42
43
  diagnostics root cause, timeline, priority, repair plans, release readiness
43
44
  playwright normalize a Playwright report or summarize a trace
44
45
 
@@ -53,15 +54,26 @@ examples:
53
54
  /**
54
55
  * Where the engine is, and how we found it.
55
56
  *
56
- * Ordered by cost: a bundled copy needs no resolution and no network, a
57
- * node_modules copy needs no network, and npx needs both on first use. Reporting
58
- * *which* one answered matters when a skill degrades — "the engine is missing" and
59
- * "the engine is being fetched" are different problems.
57
+ * Ordered by cost: a bundled copy needs no resolution and no network, a shared copy
58
+ * needs one stat per ancestor, a node_modules copy needs no network, and npx needs both
59
+ * on first use. Reporting *which* one answered matters when a skill degrades — "the
60
+ * engine is missing" and "the engine is being fetched" are different problems.
61
+ *
62
+ * The shared lookup is what lets one skill directory serve a project, a workspace, and a
63
+ * machine-wide install without knowing which installed it. Walking up for
64
+ * `.qa-engineer/engine` finds a workspace install from a skill inside the repository,
65
+ * and finds a global install from a skill linked into an agent's user-level directory —
66
+ * `~/.claude/skills/qa-explore` walks up to `~`, where `~/.qa-engineer/engine` is.
60
67
  */
61
68
  function resolveEngine() {
62
69
  const bundled = path.join(here, 'lib', 'bin', 'qa-engine.mjs');
63
70
  if (fs.existsSync(bundled)) return { kind: 'bundled', command: [process.execPath, bundled] };
64
71
 
72
+ for (const root of candidateSharedRoots()) {
73
+ const shared = path.join(root, 'bin', 'qa-engine.mjs');
74
+ if (fs.existsSync(shared)) return { kind: 'shared', command: [process.execPath, shared] };
75
+ }
76
+
65
77
  for (const base of candidateModuleRoots()) {
66
78
  const installed = path.join(base, 'qa-engineer', 'packages', 'engine', 'bin', 'qa-engine.mjs');
67
79
  if (fs.existsSync(installed)) {
@@ -75,6 +87,30 @@ function resolveEngine() {
75
87
  };
76
88
  }
77
89
 
90
+ /** Shared engine directories worth checking, most specific first. */
91
+ function candidateSharedRoots() {
92
+ const roots = [];
93
+
94
+ // An explicit home wins over anything discovered, so a user who moved the install can
95
+ // rely on it rather than on whatever the walk happens to find first.
96
+ const override = process.env.QA_ENGINEER_HOME;
97
+ if (override && override.trim()) roots.push(path.join(path.resolve(override.trim()), 'engine'));
98
+
99
+ let dir = here;
100
+ for (let depth = 0; depth < 12; depth += 1) {
101
+ roots.push(path.join(dir, '.qa-engineer', 'engine'));
102
+ const parent = path.dirname(dir);
103
+ if (parent === dir) break;
104
+ dir = parent;
105
+ }
106
+
107
+ // The default machine home, for the case where the skill lives outside it entirely.
108
+ const home = os.homedir();
109
+ if (home) roots.push(path.join(home, '.qa-engineer', 'engine'));
110
+
111
+ return roots;
112
+ }
113
+
78
114
  /** node_modules directories worth checking, nearest first. */
79
115
  function candidateModuleRoots() {
80
116
  const roots = [];
@@ -58,12 +58,18 @@ Load only what the situation requires:
58
58
  1. **Intake.** Confirm URL. Parse attached test cases into a numbered checklist ([test-case-intake.md](references/test-case-intake.md)). Note known bugs as hypotheses to validate. Create a run id and artifact directory `qa-artifacts/explore-<run-id>/`.
59
59
  2. **Session.** Select a browser adapter ([browser-adapters.md](references/browser-adapters.md)). Navigate to the URL. If a login wall appears, **stop** and ask the user to sign in; never type credentials or OTPs. Baseline console errors and resource entries.
60
60
  3. **Functional.** Execute attached cases first, then surface-by-surface exploration per [pipeline.md](references/pipeline.md). After every action: DOM-verify, then capture evidence on failure or notable finding ([evidence-capture.md](references/evidence-capture.md)).
61
- 4. **API audit.** Collect resource entries; replay exact app URLs in-page; classify and record findings ([api-replay.md](references/api-replay.md)).
61
+ 4. **API audit.** Capture a HAR, then run `analysis network` on it — **never count requests, time them, or spot duplicates by eye.** Replay exact app URLs in-page where a call needs confirming. The parser says what happened; you decide which of its flags matter to this product and write those as findings ([api-replay.md](references/api-replay.md)).
62
62
  5. **Performance.** Measure payloads, cold vs warm, long tasks / vitals signals under controlled conditions ([performance.md](references/performance.md)).
63
63
  6. **Security (client).** Run the client-side pass only — token storage, PII in URLs/payloads, error leakage, headers, optional read-only IDOR probe when in scope ([security.md](references/security.md)). No destructive tests.
64
64
  7. **UI / UX.** Check empty/loading/error states, consistency, mobile viewport spot-check; optional persona lens if the user named a role.
65
65
  8. **Optional DB.** Only if the user provided access: capture UI values with timestamps; query; separate data vs presentation bugs. Skip entirely otherwise and note "DB validation not in scope".
66
- 9. **Report.** Assign stable IDs and severities ([finding-taxonomy.md](references/finding-taxonomy.md)). Every finding must include proof. Write `explore-result.json` **first** — including `scope` (what you set out to check, what you touched, and every boundary of the run with its reason) and findings written for a reader who has never seen the product — then validate it against the contract and **render** the HTML from it with the report renderer (see Tooling); never type the HTML. Write the Markdown from the same result ([report-pipeline.md](references/report-pipeline.md)). Include "what works well" and a prioritized fix order.
66
+ 9. **Report.** Assign stable IDs and severities ([finding-taxonomy.md](references/finding-taxonomy.md)). Every finding must include proof. Then, in this order ([report-pipeline.md](references/report-pipeline.md)):
67
+ 1. Write `explore-result.json` — `artifacts[]` for every captured file, `scope` (what you set out to check, what you touched, and every boundary of the run with its reason), `executive` when a non-engineer will read it, and findings written for a reader who has never seen the product.
68
+ 2. **Validate** it against the contract.
69
+ 3. **Verify the artifacts** — `artifacts verify` must pass before anything is rendered. A missing or zero-byte file is reported, never quietly dropped.
70
+ 4. **Build the bundle** with `report-bundle` — a portable folder that opens offline, with every link verified. That folder is the deliverable; point the user at its `index.html`. Never type HTML.
71
+
72
+ Include "what works well" and a prioritized fix order. Only report a score for a dimension the run actually measured.
67
73
  10. **Iterate.** On user feedback: validate live, add evidence, bump report version, never renumber IDs. After three stuck browser attempts on the same blocker, stop and escalate with findings.
68
74
 
69
75
  ## Guardrails
@@ -84,21 +90,31 @@ Invoke the bundled engine through its launcher, as documented in [references/det
84
90
  | Tool | Invocation | Output | Fallback |
85
91
  | --- | --- | --- | --- |
86
92
  | Contract self-check | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis validate <explore-result.json> <SKILL_DIR>/contracts/explore-result.schema.json` | `{valid, errors}` — run this before rendering | None: an invalid result is not a report |
87
- | HTML report renderer | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis report-html <explore-result.json> --out explore-report.html` | The complete self-contained report: every finding's current vs expected behaviour, repro, fix direction, evidence, and the attribution footer | Write the HTML by hand from the contract fields, rendering all of them, and say the report was not machine-rendered |
93
+ | Artifact check | `node <SKILL_DIR>/scripts/qa-tool.mjs artifacts verify <explore-result.json>` | `{ok, stats, missing}`; exit 1 when a file a finding points at is absent or empty. Run it **before** rendering | List the missing files in the report yourself; never delete the evidence entry to make it pass |
94
+ | Portable report bundle | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis report-bundle <explore-result.json> --out report --zip` | **The canonical output.** A folder — `index.html` + `assets/` — that opens offline in any browser, with every link verified to resolve inside it, plus a `.zip` to send. | **None.** Report that the engine is missing and stop |
95
+ | Single-file HTML | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis report-html <explore-result.json> --embed --out explore-report.html` | The complete self-contained report — nav, search, charts, timeline, expandable findings, evidence, attribution. `--embed` inlines images; `--mode full\|executive\|developer\|artifact` picks the audience | **None.** Report that the engine is missing and stop — a hand-written report is the failure this design removes |
96
+ | Other renderings | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis report-export <explore-result.json> --format <markdown\|sarif\|junit\|csv\|json\|bundle> --out <file>` | The same validated result in the format the destination reads | Write the Markdown by hand; skip the machine formats |
97
+ | Canonical schema | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis report-schema` · `… analysis report-versions` | The producer-neutral contract any agent writes to, and the schema/theme/renderer versions in force | Use the skill's own `contracts/explore-result.schema.json` |
98
+ | Network analysis | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis network <capture.har> [--slow-ms N]` | The report's `network` block, counted and flagged by the parser: totals, per-endpoint timing and size, and an `issue` on each — `failed`, `slow`, `polling`, `duplicate`, `n-plus-one`, `large-payload`, `uncached`. Drop it straight into the result | Read `performance.getEntriesByType('resource')` and say in the report that counts were observed rather than parsed |
88
99
  | Secret redaction | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis redact <file>` | The file with credentials and tokens masked, for evidence excerpts | Redact by hand before the excerpt is written |
89
100
  | Failure classification | `node <SKILL_DIR>/scripts/qa-tool.mjs analysis classify "<error message>"` | `{classification, confidence, reason}` for a console or network error | Classify per [finding-taxonomy.md](references/finding-taxonomy.md) |
90
101
 
91
102
  A missing `qa-tool.mjs` means the engine is not installed; run `qa doctor`.
92
103
 
93
- **The HTML is rendered, not written.** The result JSON is the source of truth for the report, and `report-html` reads it. Hand-typing the page is how the first live run silently dropped `actual`, `expected`, and `fixDirection` from every finding while the JSON held all three.
104
+ **You produce structured data; the pack produces every document.** The result JSON is the source of truth and every format is rendered from it. You never write HTML, CSS, or styling of any kind — the contract has no field to carry a presentation hint, which is what makes a report from this skill identical whichever agent ran it. Hand-typing the page is how the first live run silently dropped `actual`, `expected`, and `fixDirection` from every finding while the JSON held all three.
105
+
106
+ **Register artifacts; do not inline paths.** Every captured file gets an `artifacts[]` entry, and evidence points at it by `artifactId`. Paths are relative to the result JSON. That is what makes `artifacts verify` meaningful and what stopped the second live run's screenshots from rendering as broken images.
94
107
 
95
108
  ## Output
96
109
 
97
- Write under `qa-artifacts/explore-<run-id>/`:
110
+ Write under `qa-artifacts/explore-<run-id>/`, so the whole run folder moves as a unit:
111
+
112
+ - `explore-result.json` — machine-readable result conforming to [contracts/explore-result.schema.json](contracts/explore-result.schema.json); written first, and the source of every rendering below
113
+ - `report/` — **the deliverable.** A portable bundle written by `report-bundle`: `index.html`, `report.json`, `report.md`, `manifest.json`, and `assets/`. It opens offline in any browser and every link is verified to resolve inside it. `--zip` adds `report.zip` for sending.
114
+ - `screenshots/`, `network/`, `console/`, `dom/` — proof, registered in `artifacts[]` and referenced by `artifactId`; the bundle copies these into its own `assets/` tree
115
+
116
+ Point the user at `report/index.html`. A hosted preview — a Claude Artifact, a Cursor preview, any cloud viewer — is an optional convenience that expires; it must never be presented as the deliverable.
98
117
 
99
- - `screenshots/` proof images referenced by findings
100
- - `explore-result.json` — machine-readable result conforming to [contracts/explore-result.schema.json](contracts/explore-result.schema.json); written first, and the source of the two renderings below
101
- - `explore-report.html` — the report a person reads, **rendered** from the JSON by `report-html`
102
- - `explore-report.md` — the same content as Markdown, with the rendered attribution footer appended
118
+ On request, also `report-html --embed` for one self-contained file, or `report-export` to SARIF (code-scanning viewers), JUnit (CI), or CSV (a tracking sheet). There is no PDF writer: the HTML has a print stylesheet that forces findings open and prints link targets, so **Print → Save as PDF** is the supported path.
103
119
 
104
- Validate the JSON against the schema before rendering, and again before declaring completion. Present a short prose verdict (severity counts + top findings) in the conversation, and point to the artifact paths.
120
+ Validate the JSON and run `artifacts verify` before rendering, and again before declaring completion. Present a short prose verdict (severity counts + top findings) in the conversation, and point to the artifact paths.